Repository: comic/grand-challenge.org

Issue:
Test sometimes fails when run in parallel
The test `TestETDRSAnnotationViewSet.test_list_view` is flakey and has spuriously failed a couple of times:

https://travis-ci.org/comic/grand-challenge.org/builds/526871002

Might be due to running the tests in parallel.


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/annotations/migrations/0002_auto_20190510_1046.py b/app/grandchallenge/annotations/migrations/0002_auto_20190510_1046.py
new file mode 100644
--- /dev/null
+++ b/app/grandchallenge/annotations/migrations/0002_auto_20190510_1046.py
@@ -0,0 +1,47 @@
+# Generated by Django 2.1.8 on 2019-05-10 10:46
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [("annotations", "0001_initial")]
+
+    operations = [
+        migrations.AlterModelOptions(
+            name="booleanclassificationannotation",
+            options={"get_latest_by": "created", "ordering": ["-created"]},
+        ),
+        migrations.AlterModelOptions(
+            name="coordinatelistannotation",
+            options={"get_latest_by": "created", "ordering": ["-created"]},
+        ),
+        migrations.AlterModelOptions(
+            name="etdrsgridannotation",
+            options={"get_latest_by": "created", "ordering": ["-created"]},
+        ),
+        migrations.AlterModelOptions(
+            name="integerclassificationannotation",
+            options={"get_latest_by": "created", "ordering": ["-created"]},
+        ),
+        migrations.AlterModelOptions(
+            name="landmarkannotationset",
+            options={"get_latest_by": "created", "ordering": ["-created"]},
+        ),
+        migrations.AlterModelOptions(
+            name="measurementannotation",
+            options={"get_latest_by": "created", "ordering": ["-created"]},
+        ),
+        migrations.AlterModelOptions(
+            name="polygonannotationset",
+            options={"get_latest_by": "created", "ordering": ["-created"]},
+        ),
+        migrations.AlterModelOptions(
+            name="singlelandmarkannotation",
+            options={"get_latest_by": "created", "ordering": ["-created"]},
+        ),
+        migrations.AlterModelOptions(
+            name="singlepolygonannotation",
+            options={"get_latest_by": "created", "ordering": ["-created"]},
+        ),
+    ]
diff --git a/app/grandchallenge/annotations/models.py b/app/grandchallenge/annotations/models.py
--- a/app/grandchallenge/annotations/models.py
+++ b/app/grandchallenge/annotations/models.py
@@ -20,14 +20,16 @@ class AbstractAnnotationModel(UUIDModel):
     # Override inherited 'created' attribute to allow setting of value
     created = models.DateTimeField(default=timezone.now)
 
-    class Meta:
+    class Meta(UUIDModel.Meta):
         abstract = True
+        get_latest_by = "created"
+        ordering = ["-created"]
 
     def save(self, *args, **kwargs):
         """ Override save method to enable setting of permissions for retina users """
         created = self._state.adding
 
-        super(AbstractAnnotationModel, self).save(*args, **kwargs)
+        super().save(*args, **kwargs)
 
         if not created:
             return
@@ -55,7 +57,7 @@ def save(self, *args, **kwargs):
         """ Override save method to enable setting of permissions for retina users """
         created = self._state.adding
 
-        super(AbstractSingleAnnotationModel, self).save(*args, **kwargs)
+        super().save(*args, **kwargs)
 
         if not created:
             return
@@ -77,8 +79,10 @@ def save(self, *args, **kwargs):
                 assign_perm(permission_name, self.annotation_set.grader, self)
                 assign_perm(permission_name, admins_group, self)
 
-    class Meta:
+    class Meta(UUIDModel.Meta):
         abstract = True
+        get_latest_by = "created"
+        ordering = ["-created"]
 
 
 class AbstractImageAnnotationModel(AbstractAnnotationModel):
@@ -96,7 +100,7 @@ def __str__(self):
             self.image,
         )
 
-    class Meta:
+    class Meta(AbstractAnnotationModel.Meta):
         abstract = True
 
 
@@ -108,7 +112,7 @@ class AbstractNamedImageAnnotationModel(AbstractImageAnnotationModel):
 
     name = models.CharField(max_length=255)
 
-    class Meta:
+    class Meta(AbstractImageAnnotationModel.Meta):
         # Create unique together constraint to disallow duplicates
         unique_together = ("image", "grader", "created", "name")
         abstract = True
@@ -123,7 +127,7 @@ class MeasurementAnnotation(AbstractImageAnnotationModel):
     start_voxel = ArrayField(models.FloatField(), size=2)
     end_voxel = ArrayField(models.FloatField(), size=2)
 
-    class Meta:
+    class Meta(AbstractImageAnnotationModel.Meta):
         # Create unique together constraint to disallow duplicates
         unique_together = (
             "image",
@@ -186,7 +190,7 @@ class LandmarkAnnotationSet(AbstractAnnotationModel):
     Contains only the fields from AbstractAnnotationModel
     """
 
-    class Meta:
+    class Meta(AbstractAnnotationModel.Meta):
         unique_together = ("grader", "created")
 
 
@@ -204,7 +208,7 @@ class SingleLandmarkAnnotation(AbstractSingleAnnotationModel):
     # General form: [[x1,y1],[x2,y2],...]
     landmarks = ArrayField(ArrayField(models.FloatField(), size=2))
 
-    class Meta:
+    class Meta(AbstractSingleAnnotationModel.Meta):
         # Allow only one LandmarkAnnotation for a specific image in a set
         unique_together = ("image", "annotation_set")
 
@@ -219,5 +223,5 @@ class ETDRSGridAnnotation(AbstractImageAnnotationModel):
     fovea = ArrayField(models.FloatField(), size=2)
     optic_disk = ArrayField(models.FloatField(), size=2)
 
-    class Meta:
+    class Meta(AbstractImageAnnotationModel.Meta):
         unique_together = ("image", "grader", "created")
diff --git a/app/grandchallenge/annotations/serializers.py b/app/grandchallenge/annotations/serializers.py
--- a/app/grandchallenge/annotations/serializers.py
+++ b/app/grandchallenge/annotations/serializers.py
@@ -66,7 +66,7 @@ class SinglePolygonAnnotationSerializer(AbstractSingleAnnotationSerializer):
 
     class Meta:
         model = SinglePolygonAnnotation
-        fields = ("id", "value", "annotation_set")
+        fields = ("id", "value", "annotation_set", "created")
 
 
 class PolygonAnnotationSetSerializer(AbstractAnnotationSerializer):


Test patch:
diff --git a/app/tests/annotations_tests/test_serializers.py b/app/tests/annotations_tests/test_serializers.py
--- a/app/tests/annotations_tests/test_serializers.py
+++ b/app/tests/annotations_tests/test_serializers.py
@@ -64,7 +64,7 @@ class TestAnnotationSerializers:
         "unique": True,
         "factory": SinglePolygonAnnotationFactory,
         "serializer": SinglePolygonAnnotationSerializer,
-        "fields": ("id", "value", "annotation_set"),
+        "fields": ("id", "value", "annotation_set", "created"),
     },
     "landmark": {
         "unique": True,
diff --git a/app/tests/retina_api_tests/test_viewsets.py b/app/tests/retina_api_tests/test_viewsets.py
--- a/app/tests/retina_api_tests/test_viewsets.py
+++ b/app/tests/retina_api_tests/test_viewsets.py
@@ -129,6 +129,18 @@ def test_list_view(self, TwoRetinaPolygonAnnotationSets, rf, user_type):
                 TwoRetinaPolygonAnnotationSets.polygonset1
             ).data
             assert response.data[0] == serialized_data
+        if user_type == "retina_admin":
+            serialized_data = PolygonAnnotationSetSerializer(
+                [
+                    TwoRetinaPolygonAnnotationSets.polygonset1,
+                    TwoRetinaPolygonAnnotationSets.polygonset2,
+                ],
+                many=True,
+            ).data
+            serialized_data_sorted = sorted(
+                serialized_data, key=lambda k: k["created"], reverse=True
+            )
+            assert response.data == serialized_data_sorted
 
     def test_create_view(self, TwoRetinaPolygonAnnotationSets, rf, user_type):
         model_build = PolygonAnnotationSetFactory.build()
@@ -373,8 +385,7 @@ def test_list_view(self, TwoRetinaPolygonAnnotationSets, rf, user_type):
                 many=True,
             ).data
             assert len(response.data) == len(serialized_data)
-            response.data.sort(key=lambda k: k["id"])
-            serialized_data.sort(key=lambda k: k["id"])
+            serialized_data.sort(key=lambda k: k["created"], reverse=True)
             assert response.data == serialized_data
         elif user_type == "retina_admin":
             serialized_data = SinglePolygonAnnotationSerializer(
@@ -382,6 +393,7 @@ def test_list_view(self, TwoRetinaPolygonAnnotationSets, rf, user_type):
                 | TwoRetinaPolygonAnnotationSets.polygonset2.singlepolygonannotation_set.all(),
                 many=True,
             ).data
+            serialized_data.sort(key=lambda k: k["created"], reverse=True)
             assert response.data == serialized_data
 
     def test_create_view(self, TwoRetinaPolygonAnnotationSets, rf, user_type):
@@ -405,11 +417,7 @@ def test_create_view(self, TwoRetinaPolygonAnnotationSets, rf, user_type):
             model_json,
         )
         if user_type in ("retina_grader", "retina_admin"):
-            model_serialized["id"] = response.data["id"]
-            response.data["annotation_set"] = str(
-                response.data["annotation_set"]
-            )
-            assert response.data == model_serialized
+            assert response.data["value"] == model_serialized["value"]
 
     def test_create_view_wrong_user_id(
         self, TwoRetinaPolygonAnnotationSets, rf, user_type
@@ -434,11 +442,7 @@ def test_create_view_wrong_user_id(
             check_response_status_code=False,
         )
         if user_type == "retina_admin":
-            model_serialized["id"] = response.data["id"]
-            response.data["annotation_set"] = str(
-                response.data["annotation_set"]
-            )
-            assert response.data == model_serialized
+            assert response.data["value"] == model_serialized["value"]
         elif user_type == "retina_grader":
             assert response.status_code == status.HTTP_400_BAD_REQUEST
             assert (
@@ -1002,8 +1006,7 @@ def test_list_view(self, MultipleRetinaETDRSAnnotations, rf, user_type):
                 MultipleRetinaETDRSAnnotations.etdrss1, many=True
             ).data
             assert len(response.data) == len(serialized_data)
-            response.data.sort(key=lambda k: k["id"])
-            serialized_data.sort(key=lambda k: k["id"])
+            serialized_data.sort(key=lambda k: k["created"], reverse=True)
             assert response.data == serialized_data
         elif user_type == "retina_admin":
             serialized_data = ETDRSGridAnnotationSerializer(
@@ -1013,6 +1016,8 @@ def test_list_view(self, MultipleRetinaETDRSAnnotations, rf, user_type):
                 ],
                 many=True,
             ).data
+            serialized_data.sort(key=lambda k: k["created"], reverse=True)
+            assert len(response.data) == len(serialized_data)
             assert response.data == serialized_data
 
     def test_create_view(self, MultipleRetinaETDRSAnnotations, rf, user_type):