Repository: comic/grand-challenge.org

Issue:
Add support for .obj files
**Is your feature request related to a problem? Please describe.**
Many challenges (SLCN, 3D Teeth Seg) use surface meshes, I think we're at the point where they should be supported. No need for viewer integration right now.

**Describe the solution you'd like**
As [discussed on teams](https://teams.microsoft.com/l/message/19:376eeffc772144cabed3236a2cbb3ad8@thread.tacv2/1650442890564?tenantId=b208fe69-471e-48c4-8d87-025e9b9a157f&groupId=45c973c4-5543-45ad-a8b8-79ea596e6c69&parentMessageId=1650442890564&teamName=Research%20Software%20Engineering&channelName=cirrus-dev&createdTime=1650442890564) it would be good to add support for single `.obj` files in the same way that we do for `.pdf`.

We do not need to do any validation of the file, beyond the mime-type for now.

**Describe alternatives you've considered**
STL, doesn't seem as widely supported.


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/components/migrations/0010_auto_20220602_0902.py b/app/grandchallenge/components/migrations/0010_auto_20220602_0902.py
new file mode 100644
--- /dev/null
+++ b/app/grandchallenge/components/migrations/0010_auto_20220602_0902.py
@@ -0,0 +1,94 @@
+# Generated by Django 3.2.13 on 2022-06-02 09:02
+
+from django.db import migrations, models
+
+import grandchallenge.components.models
+import grandchallenge.core.storage
+import grandchallenge.core.validators
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ("components", "0009_alter_componentinterface_kind"),
+    ]
+
+    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"),
+                    ("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",
+                        )
+                    ),
+                    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",
+                        )
+                    ),
+                ],
+            ),
+        ),
+    ]
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
@@ -96,6 +96,7 @@ class InterfaceKindChoices(models.TextChoices):
     SQREG = "SQREG", _("SQREG file")
     THUMBNAIL_JPG = "JPEG", _("Thumbnail jpg")
     THUMBNAIL_PNG = "PNG", _("Thumbnail png")
+    OBJ = "OBJ", _("OBJ file")
 
     # Legacy support
     CSV = "CSV", _("CSV file")
@@ -461,6 +462,7 @@ def interface_type_file():
         * SQREG file
         * Thumbnail JPG
         * Thumbnail PNG
+        * OBJ file
         """
         return {
             InterfaceKind.InterfaceKindChoices.CSV,
@@ -469,6 +471,7 @@ def interface_type_file():
             InterfaceKind.InterfaceKindChoices.SQREG,
             InterfaceKind.InterfaceKindChoices.THUMBNAIL_JPG,
             InterfaceKind.InterfaceKindChoices.THUMBNAIL_PNG,
+            InterfaceKind.InterfaceKindChoices.OBJ,
         }
 
     @classmethod
@@ -514,6 +517,8 @@ def get_file_mimetypes(cls, *, kind):
                 "application/x-sqlite3",
                 "application/vnd.sqlite3",
             )
+        elif kind == InterfaceKind.InterfaceKindChoices.OBJ:
+            return ("text/plain",)
         else:
             raise RuntimeError(f"Unknown kind {kind}")
 
@@ -737,6 +742,7 @@ class ComponentInterfaceValue(models.Model):
                     ".jpeg",
                     ".pdf",
                     ".sqreg",
+                    ".obj",
                 )
             ),
             MimeTypeValidator(


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
@@ -116,6 +116,7 @@ def test_average_duration_filtering():
         (InterfaceKindChoices.SQREG, True, False),
         (InterfaceKindChoices.THUMBNAIL_JPG, True, False),
         (InterfaceKindChoices.THUMBNAIL_PNG, True, False),
+        (InterfaceKindChoices.OBJ, True, False),
     ),
 )
 def test_saved_in_object_store(kind, object_store_required, is_image):