internlm/SWE-Fixer-Train-110K huggingface dataset:
Repository: comic/grand-challenge.org

Issue:
Add permissions to AlgorithmEvaluations
Algorithm editors should be able to view `AlgorithmEvaluations`.


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/evaluation/models.py b/app/grandchallenge/evaluation/models.py
--- a/app/grandchallenge/evaluation/models.py
+++ b/app/grandchallenge/evaluation/models.py
@@ -10,6 +10,7 @@
 from django.db.models import BooleanField
 from django.utils.text import get_valid_filename
 from django_extensions.db.fields import AutoSlugField
+from guardian.shortcuts import assign_perm
 
 from grandchallenge.algorithms.models import (
     AlgorithmExecutor,
@@ -580,6 +581,21 @@ class AlgorithmEvaluation(ComponentJob):
     modified = models.DateTimeField(auto_now=True)
     submission = models.ForeignKey(Submission, on_delete=models.CASCADE)
 
+    def save(self, *args, **kwargs):
+        adding = self._state.adding
+
+        super().save(*args, **kwargs)
+
+        if adding:
+            self.update_permissions()
+
+    def update_permissions(self):
+        assign_perm(
+            "view_algorithmevaluation",
+            self.submission.phase.challenge.admins_group,
+            self,
+        )
+
     @property
     def container(self):
         return self.submission.algorithm_image


Test patch:
diff --git a/app/tests/evaluation_tests/test_permissions.py b/app/tests/evaluation_tests/test_permissions.py
new file mode 100644
--- /dev/null
+++ b/app/tests/evaluation_tests/test_permissions.py
@@ -0,0 +1,22 @@
+from django.test import TestCase
+from guardian.shortcuts import get_groups_with_perms, get_users_with_perms
+
+from grandchallenge.evaluation.models import AlgorithmEvaluation
+from tests.evaluation_tests.factories import AlgorithmEvaluationFactory
+
+
+class TestAlgorithmEvaluationPermissions(TestCase):
+    def test_algorithm_evaluation_permissions(self):
+        """
+        Only the challenge admins should be able to view algorithm evaluations
+        The submission creator, algorithm groups and participants should not
+        have view permissions
+        """
+        ae: AlgorithmEvaluation = AlgorithmEvaluationFactory()
+
+        assert get_groups_with_perms(ae, attach_perms=True) == {
+            ae.submission.phase.challenge.admins_group: [
+                "view_algorithmevaluation"
+            ]
+        }
+        assert get_users_with_perms(ae, with_group_users=False).count() == 0
Repository: comic/grand-challenge.org

Issue:
Participants cannot view their submissions
Check the permissions on the submission view page


Discussion:
Now available at `/evaluation/submissions/` - need to update the links and add more information to that page

Resolving patch:
diff --git a/app/grandchallenge/evaluation/views/__init__.py b/app/grandchallenge/evaluation/views/__init__.py
--- a/app/grandchallenge/evaluation/views/__init__.py
+++ b/app/grandchallenge/evaluation/views/__init__.py
@@ -275,7 +275,11 @@ class SubmissionList(LoginRequiredMixin, PermissionListMixin, ListView):
 
     def get_queryset(self):
         queryset = super().get_queryset()
-        return queryset.filter(phase__challenge=self.request.challenge)
+        return (
+            queryset.filter(phase__challenge=self.request.challenge)
+            .select_related("creator__user_profile", "phase__challenge")
+            .prefetch_related("evaluation_set")
+        )
 
 
 class SubmissionDetail(
diff --git a/app/grandchallenge/subdomains/middleware.py b/app/grandchallenge/subdomains/middleware.py
--- a/app/grandchallenge/subdomains/middleware.py
+++ b/app/grandchallenge/subdomains/middleware.py
@@ -44,9 +44,9 @@ def middleware(request):
             request.challenge = None
         else:
             try:
-                request.challenge = Challenge.objects.get(
-                    short_name__iexact=subdomain
-                )
+                request.challenge = Challenge.objects.prefetch_related(
+                    "phase_set"
+                ).get(short_name__iexact=subdomain)
             except Challenge.DoesNotExist:
                 logger.warning(f"Could not find challenge {subdomain}")
                 domain = request.site.domain.lower()


Test patch:
Repository: comic/grand-challenge.org

Issue:
Algorithm results list AJAX view is slow
Following #1553 generating a page of results is slow, 5s for 100 results. Likely an n+1 queries resulting from `interface = ComponentInterface.objects.get(slug="results-json-file")`. Might need a similar solution as in #1477. My guess is that we need to run with the HistoryPanel of Django Debug Toolbar.


Discussion:
The n+1 queries have been reduced but this is still slow, almost 40ms per row with a template.
Yes I notice this also as a user of grand-challenge.org

Resolving patch:
diff --git a/app/grandchallenge/algorithms/views.py b/app/grandchallenge/algorithms/views.py
--- a/app/grandchallenge/algorithms/views.py
+++ b/app/grandchallenge/algorithms/views.py
@@ -480,7 +480,11 @@ class AlgorithmImageViewSet(ReadOnlyModelViewSet):
 
 
 class JobViewSet(ReadOnlyModelViewSet):
-    queryset = Job.objects.all()
+    queryset = (
+        Job.objects.all()
+        .prefetch_related("outputs__interface", "inputs__interface")
+        .select_related("algorithm_image__algorithm")
+    )
     serializer_class = JobSerializer
     permission_classes = [DjangoObjectPermissions]
     filter_backends = [DjangoFilterBackend, ObjectPermissionsFilter]


Test patch:
Repository: comic/grand-challenge.org

Issue:
Aggregation fails when many algorithm results are created
Causes ClientError, see https://sentry.io/organizations/grand-challenge/issues/1945299773/


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/components/tasks.py b/app/grandchallenge/components/tasks.py
--- a/app/grandchallenge/components/tasks.py
+++ b/app/grandchallenge/components/tasks.py
@@ -139,7 +139,7 @@ def get_model_instance(*, pk, app_label, model_name):
 @shared_task
 def execute_job(
     *_, job_pk: uuid.UUID, job_app_label: str, job_model_name: str
-) -> dict:
+) -> None:
     Job = apps.get_model(  # noqa: N806
         app_label=job_app_label, model_name=job_model_name
     )
@@ -188,7 +188,6 @@ def execute_job(
         )
         job.create_result(result=result)
         job.update_status(status=job.SUCCESS, output=logs)
-        return result
 
 
 @shared_task
diff --git a/app/grandchallenge/evaluation/models.py b/app/grandchallenge/evaluation/models.py
--- a/app/grandchallenge/evaluation/models.py
+++ b/app/grandchallenge/evaluation/models.py
@@ -562,7 +562,8 @@ def create_evaluation(self):
                 (
                     group(*jobs)
                     | set_evaluation_inputs.signature(
-                        kwargs={"evaluation_pk": evaluation.pk}
+                        kwargs={"evaluation_pk": evaluation.pk},
+                        immutable=True,
                     )
                 ).apply_async()
 
diff --git a/app/grandchallenge/evaluation/tasks.py b/app/grandchallenge/evaluation/tasks.py
--- a/app/grandchallenge/evaluation/tasks.py
+++ b/app/grandchallenge/evaluation/tasks.py
@@ -8,7 +8,7 @@
 
 
 @shared_task
-def set_evaluation_inputs(*_, evaluation_pk):
+def set_evaluation_inputs(evaluation_pk):
     """
     Sets the inputs to the Evaluation for a algorithm submission.
 


Test patch:
Repository: comic/grand-challenge.org

Issue:
Replace Memcached with Redis
The MemcachedCache backend is being deprecated (see https://code.djangoproject.com/ticket/32193), the `django-memcached` package should be removed and `"django.core.cache.backends.memcached.MemcachedCache"` replaced. We should use django-redis instead (be careful to use a different database to the development celery broker) https://github.com/jazzband/django-redis. Will also require changing the docker-compose files.


Discussion:

Resolving patch:
diff --git a/app/config/settings.py b/app/config/settings.py
--- a/app/config/settings.py
+++ b/app/config/settings.py
@@ -205,8 +205,9 @@ def strtobool(val) -> bool:
 
 CACHES = {
     "default": {
-        "BACKEND": "django.core.cache.backends.memcached.MemcachedCache",
-        "LOCATION": "memcached:11211",
+        "BACKEND": "django_redis.cache.RedisCache",
+        "LOCATION": "redis://redis:6379/1",
+        "OPTIONS": {"CLIENT_CLASS": "django_redis.client.DefaultClient"},
     },
     "machina_attachments": {
         "BACKEND": "django.core.cache.backends.filebased.FileBasedCache",


Test patch:
Repository: comic/grand-challenge.org

Issue:
Show domain name of the email address that was used to verify the account
Google Scholar shows on profiles instead of the entire email address only the domain name. Something like "Verified email address at radboudumc.nl". Would be a good feature for grand challenge as well, this would make it possible to check if users used an email address from their institution to verify their account. A similar text to what Google Scholar shows could be displayed when hovering over the check mark icon, for example.


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/profiles/templatetags/profiles.py b/app/grandchallenge/profiles/templatetags/profiles.py
--- a/app/grandchallenge/profiles/templatetags/profiles.py
+++ b/app/grandchallenge/profiles/templatetags/profiles.py
@@ -33,9 +33,15 @@ def user_profile_link(user: Union[AbstractUser, None]) -> str:
 
         try:
             if user.verification.is_verified:
-                verified = mark_safe(
+                email = (
+                    user.verification.email
+                    if user.verification.email
+                    else user.email
+                )
+                verified = format_html(
                     '<i class="fas fa-user-check text-success" '
-                    'title="Verified User"></i>'
+                    'title="Verified email address at {}"></i>',
+                    email.split("@")[1],
                 )
         except ObjectDoesNotExist:
             # No verification request


Test patch:
Repository: comic/grand-challenge.org

Issue:
Cannot save phase update form without changing the title of the phase




Discussion:

Resolving patch:
diff --git a/app/grandchallenge/evaluation/forms.py b/app/grandchallenge/evaluation/forms.py
--- a/app/grandchallenge/evaluation/forms.py
+++ b/app/grandchallenge/evaluation/forms.py
@@ -66,9 +66,14 @@ def __init__(self, *args, challenge, **kwargs):
         super().__init__(*args, **kwargs)
 
     def clean_title(self):
-        title = self.cleaned_data["title"]
+        title = self.cleaned_data["title"].strip()
 
-        if self.challenge.phase_set.filter(title=title).exists():
+        qs = self.challenge.phase_set.filter(title__iexact=title)
+
+        if self.instance:
+            qs = qs.exclude(pk=self.instance.pk)
+
+        if qs.exists():
             raise ValidationError(
                 "This challenge already has a phase with this title"
             )


Test patch:
Repository: comic/grand-challenge.org

Issue:
Verification field should be nullable
We need to represent accepted verification requests, rejected verification requests, and unreviewed verification requests. Use a nullable boolean field for this, with default None.


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/verifications/migrations/0002_auto_20201210_1040.py b/app/grandchallenge/verifications/migrations/0002_auto_20201210_1040.py
new file mode 100644
--- /dev/null
+++ b/app/grandchallenge/verifications/migrations/0002_auto_20201210_1040.py
@@ -0,0 +1,17 @@
+# Generated by Django 3.1.1 on 2020-12-10 10:40
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+    dependencies = [
+        ("verifications", "0001_initial"),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name="verification",
+            name="is_verified",
+            field=models.BooleanField(default=None, editable=False, null=True),
+        ),
+    ]
diff --git a/app/grandchallenge/verifications/models.py b/app/grandchallenge/verifications/models.py
--- a/app/grandchallenge/verifications/models.py
+++ b/app/grandchallenge/verifications/models.py
@@ -23,7 +23,7 @@ class Verification(models.Model):
         blank=True, null=True, editable=False
     )
 
-    is_verified = models.BooleanField(default=False, editable=False)
+    is_verified = models.BooleanField(default=None, null=True, editable=False)
     verified_at = models.DateTimeField(blank=True, null=True, editable=False)
 
     def __str__(self):


Test patch:
Repository: comic/grand-challenge.org

Issue:
RawImageFileAdmin has incorrect relation defined



Discussion:

Resolving patch:
diff --git a/app/grandchallenge/cases/admin.py b/app/grandchallenge/cases/admin.py
--- a/app/grandchallenge/cases/admin.py
+++ b/app/grandchallenge/cases/admin.py
@@ -114,7 +114,6 @@ def queryset(self, request, queryset):
 class RawImageFileAdmin(GuardedModelAdmin):
     list_filter = (DownloadableFilter,)
     list_display = ("filename", "upload_session", "download")
-    list_select_related = ("upload_session__archive",)
     readonly_fields = (
         "download",
         "upload_session",


Test patch:
Repository: comic/grand-challenge.org

Issue:
I can't add a "Multiple polygons (saved as mask)" type question 
Incorrect error about the image port:

![image](https://user-images.githubusercontent.com/51329283/106592495-dc6ec200-654f-11eb-9126-f1077f30c68d.png)



Discussion:

Resolving patch:
diff --git a/app/grandchallenge/reader_studies/models.py b/app/grandchallenge/reader_studies/models.py
--- a/app/grandchallenge/reader_studies/models.py
+++ b/app/grandchallenge/reader_studies/models.py
@@ -1337,6 +1337,7 @@ def annotation_types(self):
             self.ANSWER_TYPE_POLYGON,
             self.ANSWER_TYPE_POLYGON_IMAGE,
             self.ANSWER_TYPE_MULTIPLE_POLYGONS,
+            self.ANSWER_TYPE_MULTIPLE_POLYGONS_IMAGE,
         ]
 
     def is_answer_valid(self, *, answer):


Test patch:
Repository: comic/grand-challenge.org

Issue:
Image import job does not get marked as failed when the time limit is exceeded
Image import jobs should catch the celery soft time limit errors, mark the import job as failed and clean up before the hard time limit is reached. This is handled correctly for component jobs.



Discussion:

Resolving patch:
diff --git a/app/grandchallenge/cases/tasks.py b/app/grandchallenge/cases/tasks.py
--- a/app/grandchallenge/cases/tasks.py
+++ b/app/grandchallenge/cases/tasks.py
@@ -8,6 +8,7 @@
 from tempfile import TemporaryDirectory
 from typing import Callable, Dict, Iterable, List, Sequence, Set, Tuple
 
+from billiard.exceptions import SoftTimeLimitExceeded
 from celery import shared_task
 from django.conf import settings
 from django.contrib.auth.models import Group
@@ -312,6 +313,10 @@ def build_images(*, upload_session_pk):
             upload_session.status = upload_session.FAILURE
             upload_session.save()
             return
+        except SoftTimeLimitExceeded:
+            upload_session.error_message = "Time limit exceeded."
+            upload_session.status = upload_session.FAILURE
+            upload_session.save()
         except Exception:
             upload_session.error_message = "An unknown error occurred"
             upload_session.status = upload_session.FAILURE


Test patch:
diff --git a/app/tests/cases_tests/test_background_tasks.py b/app/tests/cases_tests/test_background_tasks.py
--- a/app/tests/cases_tests/test_background_tasks.py
+++ b/app/tests/cases_tests/test_background_tasks.py
@@ -2,9 +2,11 @@
 import shutil
 from pathlib import Path
 from typing import Dict, List, Tuple
+from unittest import mock
 
 import SimpleITK
 import pytest
+from billiard.exceptions import SoftTimeLimitExceeded
 from django.contrib.auth.models import Group
 from django.core.exceptions import ValidationError
 
@@ -19,10 +21,13 @@
     RawImageFile,
     RawImageUploadSession,
 )
-from grandchallenge.cases.tasks import check_compressed_and_extract
+from grandchallenge.cases.tasks import (
+    build_images,
+    check_compressed_and_extract,
+)
 from grandchallenge.jqfileupload.widgets.uploader import StagedAjaxFile
 from tests.cases_tests import RESOURCE_PATH
-from tests.factories import UserFactory
+from tests.factories import UploadSessionFactory, UserFactory
 from tests.jqfileupload_tests.external_test_support import (
     create_file_from_filepath,
 )
@@ -478,3 +483,18 @@ def test_failed_dicom_files_are_retained(settings):
     assert Image.objects.count() == 1
     assert all(RawImageFile.objects.values_list("staged_file_id", flat=True))
     RawImageFile.objects.all().delete()
+
+
+@pytest.mark.django_db
+@mock.patch(
+    "grandchallenge.cases.tasks._handle_raw_image_files",
+    side_effect=SoftTimeLimitExceeded(),
+)
+def test_soft_time_limit(_):
+    session = UploadSessionFactory()
+    session.status = session.REQUEUED
+    session.save()
+    build_images(upload_session_pk=session.pk)
+    session.refresh_from_db()
+    assert session.status == session.FAILURE
+    assert session.error_message == "Time limit exceeded."
Repository: comic/grand-challenge.org

Issue:
Annotation answers get parsed incorrectly in csv export
For annotation type answers, the csv export looks like this currently:
![Screenshot from 2021-02-18 16-26-04](https://user-images.githubusercontent.com/57257130/108379339-1b666e00-7206-11eb-8adf-629acca14f74.png)

It appears the annotation json gets part as part of the export. We should probably add some escaping.


Discussion:
Yeah, CSV does not handle nested data well at all. You can probably just override `flatten_item` in the `PaginatedCSVRenderer` we already define:

https://github.com/mjumbewu/django-rest-framework-csv/blob/master/rest_framework_csv/renderers.py#L127

and just never unwap, always return `{"": item}`. Please check the side effects of that...



Resolving patch:
diff --git a/app/grandchallenge/core/renderers.py b/app/grandchallenge/core/renderers.py
--- a/app/grandchallenge/core/renderers.py
+++ b/app/grandchallenge/core/renderers.py
@@ -1,3 +1,5 @@
+import json
+
 from rest_framework_csv.renderers import CSVRenderer
 
 
@@ -9,3 +11,19 @@ def render(self, data, *args, **kwargs):
             data = data[self.results_field]
 
         return super().render(data, *args, **kwargs)
+
+    def flatten_data(self, data):
+        """
+        Create a dictionary that is 1 level deep, with nested values serialized
+        as json. This means that the header rows are now consistent.
+        """
+        for row in data:
+            flat_row = {k: self._flatten_value(v) for k, v in row.items()}
+            yield flat_row
+
+    @staticmethod
+    def _flatten_value(value):
+        if isinstance(value, (dict, list)):
+            return json.dumps(value)
+        else:
+            return value


Test patch:
Repository: comic/grand-challenge.org

Issue:
Markdown preview fails CSRF validation checks
Caused by the name change of the CSRF cookie.


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/core/widgets.py b/app/grandchallenge/core/widgets.py
--- a/app/grandchallenge/core/widgets.py
+++ b/app/grandchallenge/core/widgets.py
@@ -26,23 +26,29 @@ class Media:
 
 
 class MarkdownEditorWidget(MarkdownxWidget):
-    class Media(MarkdownxWidget.Media):
-        js = [
-            *MarkdownxWidget.Media.js,
-            "vendor/js/markdown-toolbar-element/index.umd.js",
-        ]
+    @property
+    def media(self):
+        return forms.Media(
+            js=(
+                "js/markdownx.js",
+                "vendor/js/markdown-toolbar-element/index.umd.js",
+            )
+        )
 
 
 class MarkdownEditorAdminWidget(AdminMarkdownxWidget):
-    class Media(AdminMarkdownxWidget.Media):
-        css = {
-            "all": [
-                *AdminMarkdownxWidget.Media.css["all"],
-                "vendor/css/base.min.css",
-                "vendor/fa/css/all.css",
-            ]
-        }
-        js = [
-            *AdminMarkdownxWidget.Media.js,
-            "vendor/js/markdown-toolbar-element/index.umd.js",
-        ]
+    @property
+    def media(self):
+        return forms.Media(
+            css={
+                "all": [
+                    *AdminMarkdownxWidget.Media.css["all"],
+                    "vendor/css/base.min.css",
+                    "vendor/fa/css/all.css",
+                ]
+            },
+            js=[
+                "js/markdownx.js",
+                "vendor/js/markdown-toolbar-element/index.umd.js",
+            ],
+        )


Test patch:
Repository: comic/grand-challenge.org

Issue:
CSV export fails on converting uuid to json
Problematic JSON structure in https://grand-challenge.org/api/v1/cases/images/redacted_uuid/?format=csv

```
{"pk":"redacted_uuid","name":"redacted.png","study":null,"files":[{"pk":"redacted_uuid","image":"redacted_uuid","file":"https://grand-challenge.org/media/images/...mhd","image_type":"MHD"},{"pk":"redacted_uuid","image":"09b3b3d6-0994-43d2-b6a9-eaff634b8805","file":"https://grand-challenge.org/media/images/...zraw","image_type":"MHD"}],"reader_study_set":["https://grand-challenge.org/api/v1/reader-studies/redacted_uuid/","https://grand-challenge.org/api/v1/reader-studies/redacted_uuid/"],"archive_set":[],"job_set":[],"width":596,"height":596,"depth":null,"color_space":"RGB","modality":null,"eye_choice":"NA","stereoscopic_choice":null,"field_of_view":null,"shape_without_color":[596,596],"shape":[596,596,3],"voxel_width_mm":null,"voxel_height_mm":null,"voxel_depth_mm":null,"api_url":"https://grand-challenge.org/api/v1/cases/images/redacted_uuid/"}
```

Probably due to trying to serialize the list of files to json in https://github.com/comic/grand-challenge.org/blob/14bc3dd4002756e9cf4a32bb0f238859a9175252/app/grandchallenge/core/renderers.py#L26-L27


Discussion:

Resolving patch:
diff --git a/app/grandchallenge/core/renderers.py b/app/grandchallenge/core/renderers.py
--- a/app/grandchallenge/core/renderers.py
+++ b/app/grandchallenge/core/renderers.py
@@ -1,5 +1,7 @@
 import json
 
+from rest_framework.settings import api_settings
+from rest_framework.utils.encoders import JSONEncoder
 from rest_framework_csv.renderers import CSVRenderer
 
 
@@ -24,6 +26,11 @@ def flatten_data(self, data):
     @staticmethod
     def _flatten_value(value):
         if isinstance(value, (dict, list)):
-            return json.dumps(value)
+            return json.dumps(
+                value,
+                cls=JSONEncoder,
+                ensure_ascii=not api_settings.UNICODE_JSON,
+                allow_nan=not api_settings.STRICT_JSON,
+            )
         else:
             return value


Test patch: