From ff566b79dc36103a740c4694d1d983a2f6ed0581 Mon Sep 17 00:00:00 2001
From: Eliot Berriot <contact@eliotberriot.com>
Date: Tue, 17 Sep 2019 11:03:32 +0200
Subject: [PATCH] See #872: banner setting

---
 .../instance/dynamic_preferences_registry.py  | 26 ++++++++++++
 api/funkwhale_api/instance/nodeinfo.py        |  4 ++
 api/tests/instance/test_nodeinfo.py           |  6 ++-
 api/tests/instance/test_preferences.py        | 10 +++++
 front/src/components/admin/SettingsGroup.vue  | 41 ++++++++++++++++++-
 front/src/views/admin/Settings.vue            |  1 +
 6 files changed, 86 insertions(+), 2 deletions(-)

diff --git a/api/funkwhale_api/instance/dynamic_preferences_registry.py b/api/funkwhale_api/instance/dynamic_preferences_registry.py
index 1e6f3147c..ed2cef527 100644
--- a/api/funkwhale_api/instance/dynamic_preferences_registry.py
+++ b/api/funkwhale_api/instance/dynamic_preferences_registry.py
@@ -1,4 +1,6 @@
 from django.forms import widgets
+from django.core.validators import FileExtensionValidator
+
 from dynamic_preferences import types
 from dynamic_preferences.registries import global_preferences_registry
 
@@ -129,3 +131,27 @@ class CustomCSS(types.StringPreference):
     )
     widget = widgets.Textarea
     field_kwargs = {"required": False}
+
+
+class ImageWidget(widgets.ClearableFileInput):
+    pass
+
+
+class ImagePreference(types.FilePreference):
+    widget = ImageWidget
+    field_kwargs = {
+        "validators": [
+            FileExtensionValidator(allowed_extensions=["png", "jpg", "jpeg", "webp"])
+        ]
+    }
+
+
+@global_preferences_registry.register
+class Banner(ImagePreference):
+    show_in_api = True
+    section = instance
+    name = "banner"
+    verbose_name = "Banner image"
+    default = None
+    help_text = "This banner will be displayed on your pod's landing and about page. At least 600x100px recommended."
+    field_kwargs = {"required": False}
diff --git a/api/funkwhale_api/instance/nodeinfo.py b/api/funkwhale_api/instance/nodeinfo.py
index 1d401fbca..dc495108e 100644
--- a/api/funkwhale_api/instance/nodeinfo.py
+++ b/api/funkwhale_api/instance/nodeinfo.py
@@ -3,6 +3,7 @@ import memoize.djangocache
 import funkwhale_api
 from funkwhale_api.common import preferences
 from funkwhale_api.federation import actors, models as federation_models
+from funkwhale_api.federation import utils as federation_utils
 from funkwhale_api.moderation import models as moderation_models
 from funkwhale_api.music import utils as music_utils
 
@@ -17,6 +18,7 @@ def get():
     share_stats = all_preferences.get("instance__nodeinfo_stats_enabled")
     allow_list_enabled = all_preferences.get("moderation__allow_list_enabled")
     allow_list_public = all_preferences.get("moderation__allow_list_public")
+    banner = all_preferences.get("instance__banner")
     unauthenticated_report_types = all_preferences.get(
         "moderation__unauthenticated_report_types"
     )
@@ -42,6 +44,7 @@ def get():
             "longDescription": all_preferences.get("instance__long_description"),
             "terms": all_preferences.get("instance__terms"),
             "nodeName": all_preferences.get("instance__name"),
+            "banner": federation_utils.full_url(banner.url) if banner else None,
             "library": {
                 "federationEnabled": all_preferences.get("federation__enabled"),
                 "federationNeedsApproval": all_preferences.get(
@@ -59,6 +62,7 @@ def get():
             ],
         },
     }
+
     if share_stats:
         getter = memo(lambda: stats.get(), max_age=600)
         statistics = getter()
diff --git a/api/tests/instance/test_nodeinfo.py b/api/tests/instance/test_nodeinfo.py
index f85f34b25..d30873ec2 100644
--- a/api/tests/instance/test_nodeinfo.py
+++ b/api/tests/instance/test_nodeinfo.py
@@ -3,10 +3,12 @@ import pytest
 import funkwhale_api
 from funkwhale_api.instance import nodeinfo
 from funkwhale_api.federation import actors
+from funkwhale_api.federation import utils as federation_utils
 from funkwhale_api.music import utils as music_utils
 
 
-def test_nodeinfo_dump(preferences, mocker):
+def test_nodeinfo_dump(preferences, mocker, avatar):
+    preferences["instance__banner"] = avatar
     preferences["instance__nodeinfo_stats_enabled"] = True
     preferences["moderation__unauthenticated_report_types"] = [
         "takedown_request",
@@ -39,6 +41,7 @@ def test_nodeinfo_dump(preferences, mocker):
             "longDescription": preferences["instance__long_description"],
             "nodeName": preferences["instance__name"],
             "terms": preferences["instance__terms"],
+            "banner": federation_utils.full_url(preferences["instance__banner"].url),
             "library": {
                 "federationEnabled": preferences["federation__enabled"],
                 "federationNeedsApproval": preferences[
@@ -107,6 +110,7 @@ def test_nodeinfo_dump_stats_disabled(preferences, mocker):
             "longDescription": preferences["instance__long_description"],
             "nodeName": preferences["instance__name"],
             "terms": preferences["instance__terms"],
+            "banner": None,
             "library": {
                 "federationEnabled": preferences["federation__enabled"],
                 "federationNeedsApproval": preferences[
diff --git a/api/tests/instance/test_preferences.py b/api/tests/instance/test_preferences.py
index b465be9d3..06d1024fb 100644
--- a/api/tests/instance/test_preferences.py
+++ b/api/tests/instance/test_preferences.py
@@ -34,3 +34,13 @@ def test_instance_settings(pref, value, preferences):
     preferences[pref] = value
 
     assert preferences[pref] == value
+
+
+def test_instance_banner_assign_file_properly(preferences, avatar):
+    preferences["instance__banner"] = avatar
+
+    avatar.seek(0)
+    banner = preferences["instance__banner"].read()
+
+    assert len(banner) > 0
+    assert banner == avatar.read()
diff --git a/front/src/components/admin/SettingsGroup.vue b/front/src/components/admin/SettingsGroup.vue
index 0c0774bbe..afaebfc54 100644
--- a/front/src/components/admin/SettingsGroup.vue
+++ b/front/src/components/admin/SettingsGroup.vue
@@ -61,6 +61,14 @@
         class="ui search selection dropdown">
         <option v-for="v in setting.additional_data.choices" :value="v[0]">{{ v[1] }}</option>
       </select>
+      <div v-else-if="setting.field.widget.class === 'ImageWidget'">
+        <input type="file" :ref="setting.identifier">
+        <div v-if="values[setting.identifier]">
+          <div class="ui hidden divider"></div>
+          <h3 class="ui header"><translate translate-context="Content/Settings/Title/Noun">Current image</translate></h3>
+          <img class="ui image" v-if="values[setting.identifier]" :src="$store.getters['instance/absoluteUrl'](values[setting.identifier])" />
+        </div>
+      </div>
     </div>
     <button
       type="submit"
@@ -98,8 +106,34 @@ export default {
       this.isLoading = true
       self.errors = []
       self.result = null
-      axios.post('instance/admin/settings/bulk/', self.values).then((response) => {
+      let postData = self.values
+      let contentType = 'application/json'
+      let fileSettingsIDs = this.fileSettings.map((s) => {return s.identifier})
+      if (fileSettingsIDs.length > 0) {
+        contentType = 'multipart/form-data'
+        postData = new FormData()
+        this.settings.forEach((s) => {
+          if (fileSettingsIDs.indexOf(s.identifier) > -1) {
+            let input = self.$refs[s.identifier][0]
+            let files = input.files
+            console.log('ref', input, files)
+            if (files && files.length > 0) {
+              postData.append(s.identifier, files[0])
+            }
+          } else {
+            postData.append(s.identifier, self.values[s.identifier])
+          }
+        })
+      }
+      axios.post('instance/admin/settings/bulk/', postData, {
+        headers: {
+          'Content-Type': contentType,
+        },
+      }).then((response) => {
         self.result = true
+        response.data.forEach((s) => {
+          self.values[s.identifier] = s.value
+        })
         self.isLoading = false
         self.$store.dispatch('instance/fetchSettings')
       }, error => {
@@ -117,6 +151,11 @@ export default {
       return this.group.settings.map(e => {
         return byIdentifier[e]
       })
+    },
+    fileSettings () {
+      return this.settings.filter((s) => {
+        return s.field.widget.class === 'ImageWidget'
+      })
     }
   }
 }
diff --git a/front/src/views/admin/Settings.vue b/front/src/views/admin/Settings.vue
index a538329e3..6ae7e73a1 100644
--- a/front/src/views/admin/Settings.vue
+++ b/front/src/views/admin/Settings.vue
@@ -96,6 +96,7 @@ export default {
             "instance__short_description",
             "instance__long_description",
             "instance__terms",
+            "instance__banner",
           ]
         },
         {
-- 
GitLab