Repository: safe-global/safe-config-service

Issue:
Make logo fields required
**Is your feature request related to a problem? Please describe.**

When adding a Safe App (and maybe other entities), if a logo is not uploaded the model is invalid and can cause the interface to crash. The logo should be a required field (where relevant).

**Describe the solution you'd like**

Logo fields are required.


Discussion:

Resolving patch:
diff --git a/src/safe_apps/migrations/0013_assign_safeapp_icon_url.py b/src/safe_apps/migrations/0013_assign_safeapp_icon_url.py
new file mode 100644
--- /dev/null
+++ b/src/safe_apps/migrations/0013_assign_safeapp_icon_url.py
@@ -0,0 +1,32 @@
+# Associated with migration #0014, ensuring that all SafeApps have an icon_url
+# before making it non-nullable. Assigning a value and changing the field to
+# non-nullable in the same migration otherwise causes an error.
+
+from django.db import migrations
+from django.db.backends.base.schema import BaseDatabaseSchemaEditor
+from django.db.migrations.state import StateApps
+from django.db.models import Q
+
+
+def assign_missing_default_icon_url(
+    apps: StateApps, schema_editor: BaseDatabaseSchemaEditor
+) -> None:
+    SafeApp = apps.get_model("safe_apps", "SafeApp")
+    default_icon_url = "safe_apps/icon_url.jpg"
+
+    SafeApp.objects.filter(Q(icon_url__isnull=True) | Q(icon_url="")).update(
+        icon_url=default_icon_url
+    )
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("safe_apps", "0012_rename_visible_safeapp_listed"),
+    ]
+
+    operations = [
+        migrations.RunPython(
+            assign_missing_default_icon_url, reverse_code=migrations.RunPython.noop
+        ),
+    ]
diff --git a/src/safe_apps/migrations/0014_alter_safeapp_icon_url.py b/src/safe_apps/migrations/0014_alter_safeapp_icon_url.py
new file mode 100644
--- /dev/null
+++ b/src/safe_apps/migrations/0014_alter_safeapp_icon_url.py
@@ -0,0 +1,25 @@
+# Generated by Django 5.0.6 on 2024-05-31 14:46
+
+from django.db import migrations, models
+
+import safe_apps.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("safe_apps", "0013_assign_safeapp_icon_url"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="safeapp",
+            name="icon_url",
+            field=models.ImageField(
+                default="safe_apps/icon_url.jpg",
+                max_length=255,
+                upload_to=safe_apps.models.safe_app_icon_path,
+                validators=[safe_apps.models.validate_safe_app_icon_size],
+            ),
+        ),
+    ]
diff --git a/src/safe_apps/models.py b/src/safe_apps/models.py
--- a/src/safe_apps/models.py
+++ b/src/safe_apps/models.py
@@ -67,8 +67,7 @@ class AccessControlPolicy(str, Enum):
         validators=[validate_safe_app_icon_size],
         upload_to=safe_app_icon_path,
         max_length=255,
-        null=True,
-        blank=True,
+        default="safe_apps/icon_url.jpg",
     )
     description = models.CharField(max_length=200)
     chain_ids = ArrayField(models.PositiveBigIntegerField())


Test patch:
diff --git a/src/safe_apps/migrations/tests/__init__.py b/src/safe_apps/migrations/tests/__init__.py
new file mode 100644
diff --git a/src/safe_apps/migrations/tests/test_migration_0013.py b/src/safe_apps/migrations/tests/test_migration_0013.py
new file mode 100644
--- /dev/null
+++ b/src/safe_apps/migrations/tests/test_migration_0013.py
@@ -0,0 +1,50 @@
+from django.db.migrations.state import StateApps
+
+from chains.migrations.tests.utils import TestMigrations
+
+
+class Migration0013TestCase(TestMigrations):
+    migrate_from = "0012_rename_visible_safeapp_listed"
+    migrate_to = "0013_assign_safeapp_icon_url"
+
+    def setUpBeforeMigration(self, apps: StateApps) -> None:
+        SafeApp = apps.get_model("safe_apps", "SafeApp")
+        SafeApp.objects.create(
+            app_id=1,
+            chain_ids=[1],
+            description="No icon_url",
+        )
+
+        SafeApp.objects.create(
+            app_id=2,
+            chain_ids=[1],
+            icon_url=None,
+            description="Null icon_url",
+        )
+
+        SafeApp.objects.create(
+            app_id=3,
+            chain_ids=[1],
+            icon_url="",
+            description="Empty icon_url",
+        )
+
+        SafeApp.objects.create(
+            app_id=4,
+            chain_ids=[1],
+            icon_url="existing/icon_url.jpg",
+            description="Existing icon_url",
+        )
+
+    def test_icon_url_is_set(self) -> None:
+        SafeApp = self.apps_registry.get_model("safe_apps", "SafeApp")
+
+        app_1 = SafeApp.objects.get(app_id=1)
+        app_2 = SafeApp.objects.get(app_id=2)
+        app_3 = SafeApp.objects.get(app_id=3)
+        app_4 = SafeApp.objects.get(app_id=4)
+
+        self.assertEqual(app_1.icon_url.url, "/media/safe_apps/icon_url.jpg")
+        self.assertEqual(app_2.icon_url.url, "/media/safe_apps/icon_url.jpg")
+        self.assertEqual(app_3.icon_url.url, "/media/safe_apps/icon_url.jpg")
+        self.assertEqual(app_4.icon_url.url, "/media/existing/icon_url.jpg")