Repository: comic/grand-challenge.org

Issue:
Add Support for MP4 files
We have challenges that use videos for their data, encoded as MP4. We should support these in our interfaces.


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/components/migrations/0011_auto_20220610_1020.py b/app/grandchallenge/components/migrations/0011_auto_20220610_1020.py
new file mode 100644
--- /dev/null
+++ b/app/grandchallenge/components/migrations/0011_auto_20220610_1020.py
@@ -0,0 +1,97 @@
+# Generated by Django 3.2.13 on 2022-06-10 10:20
+
+from django.db import migrations, models
+
+import grandchallenge.components.models
+import grandchallenge.core.storage
+import grandchallenge.core.validators
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("components", "0010_auto_20220602_0902"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="componentinterface",
+            name="kind",
+            field=models.CharField(
+                choices=[
+                    ("STR", "String"),
+                    ("INT", "Integer"),
+                    ("FLT", "Float"),
+                    ("BOOL", "Bool"),
+                    ("JSON", "Anything"),
+                    ("CHART", "Chart"),
+                    ("2DBB", "2D bounding box"),
+                    ("M2DB", "Multiple 2D bounding boxes"),
+                    ("DIST", "Distance measurement"),
+                    ("MDIS", "Multiple distance measurements"),
+                    ("POIN", "Point"),
+                    ("MPOI", "Multiple points"),
+                    ("POLY", "Polygon"),
+                    ("MPOL", "Multiple polygons"),
+                    ("LINE", "Line"),
+                    ("MLIN", "Multiple lines"),
+                    ("CHOI", "Choice"),
+                    ("MCHO", "Multiple choice"),
+                    ("IMG", "Image"),
+                    ("SEG", "Segmentation"),
+                    ("HMAP", "Heat Map"),
+                    ("PDF", "PDF file"),
+                    ("SQREG", "SQREG file"),
+                    ("JPEG", "Thumbnail jpg"),
+                    ("PNG", "Thumbnail png"),
+                    ("OBJ", "OBJ file"),
+                    ("MP4", "MP4 file"),
+                    ("CSV", "CSV file"),
+                    ("ZIP", "ZIP file"),
+                ],
+                help_text="What is the type of this interface? Used to validate interface values and connections between components.",
+                max_length=5,
+            ),
+        ),
+        migrations.AlterField(
+            model_name="componentinterfacevalue",
+            name="file",
+            field=models.FileField(
+                blank=True,
+                null=True,
+                storage=grandchallenge.core.storage.ProtectedS3Storage(),
+                upload_to=grandchallenge.components.models.component_interface_value_path,
+                validators=[
+                    grandchallenge.core.validators.ExtensionValidator(
+                        allowed_extensions=(
+                            ".json",
+                            ".zip",
+                            ".csv",
+                            ".png",
+                            ".jpg",
+                            ".jpeg",
+                            ".pdf",
+                            ".sqreg",
+                            ".obj",
+                            ".mp4",
+                        )
+                    ),
+                    grandchallenge.core.validators.MimeTypeValidator(
+                        allowed_types=(
+                            "application/json",
+                            "application/zip",
+                            "text/plain",
+                            "application/csv",
+                            "application/pdf",
+                            "image/png",
+                            "image/jpeg",
+                            "application/octet-stream",
+                            "application/x-sqlite3",
+                            "application/vnd.sqlite3",
+                            "video/mp4",
+                        )
+                    ),
+                ],
+            ),
+        ),
+    ]
diff --git a/app/grandchallenge/components/models.py b/app/grandchallenge/components/models.py
--- a/app/grandchallenge/components/models.py
+++ b/app/grandchallenge/components/models.py
@@ -97,6 +97,7 @@ class InterfaceKindChoices(models.TextChoices):
     THUMBNAIL_JPG = "JPEG", _("Thumbnail jpg")
     THUMBNAIL_PNG = "PNG", _("Thumbnail png")
     OBJ = "OBJ", _("OBJ file")
+    MP4 = "MP4", _("MP4 file")
 
     # Legacy support
     CSV = "CSV", _("CSV file")
@@ -463,6 +464,7 @@ def interface_type_file():
         * Thumbnail JPG
         * Thumbnail PNG
         * OBJ file
+        * MP4 file
         """
         return {
             InterfaceKind.InterfaceKindChoices.CSV,
@@ -472,6 +474,7 @@ def interface_type_file():
             InterfaceKind.InterfaceKindChoices.THUMBNAIL_JPG,
             InterfaceKind.InterfaceKindChoices.THUMBNAIL_PNG,
             InterfaceKind.InterfaceKindChoices.OBJ,
+            InterfaceKind.InterfaceKindChoices.MP4,
         }
 
     @classmethod
@@ -519,6 +522,8 @@ def get_file_mimetypes(cls, *, kind):
             )
         elif kind == InterfaceKind.InterfaceKindChoices.OBJ:
             return ("text/plain",)
+        elif kind == InterfaceKind.InterfaceKindChoices.MP4:
+            return ("video/mp4",)
         else:
             raise RuntimeError(f"Unknown kind {kind}")
 
@@ -743,6 +748,7 @@ class ComponentInterfaceValue(models.Model):
                     ".pdf",
                     ".sqreg",
                     ".obj",
+                    ".mp4",
                 )
             ),
             MimeTypeValidator(
@@ -757,6 +763,7 @@ class ComponentInterfaceValue(models.Model):
                     "application/octet-stream",
                     "application/x-sqlite3",
                     "application/vnd.sqlite3",
+                    "video/mp4",
                 )
             ),
         ],
@@ -770,7 +777,7 @@ def title(self):
         if self.value is not None:
             return str(self.value)
         if self.file:
-            return self.file.name
+            return Path(self.file.name).name
         if self.image:
             return self.image.name
         return ""
@@ -834,7 +841,8 @@ def relative_path(self):
     def __str__(self):
         if self.value is None:
             return self.title
-        return f"Component Interface Value {self.pk} for {self.interface}"
+        else:
+            return f"Component Interface Value {self.pk} for {self.interface}"
 
     def clean(self):
         super().clean()


Test patch:
diff --git a/app/tests/components_tests/test_models.py b/app/tests/components_tests/test_models.py
--- a/app/tests/components_tests/test_models.py
+++ b/app/tests/components_tests/test_models.py
@@ -117,6 +117,7 @@ def test_average_duration_filtering():
         (InterfaceKindChoices.THUMBNAIL_JPG, True, False),
         (InterfaceKindChoices.THUMBNAIL_PNG, True, False),
         (InterfaceKindChoices.OBJ, True, False),
+        (InterfaceKindChoices.MP4, True, False),
     ),
 )
 def test_saved_in_object_store(kind, object_store_required, is_image):