Repository: comic/grand-challenge.org

Issue:
Remove editors from algorithm editors group
Algorithm editors should be able to remove other editors from the group, but not themselves. Similar to reader studies and challenges.


Discussion:

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
@@ -149,7 +149,12 @@ def get_context_data(self, **kwargs):
 
         form = UsersForm()
         form.fields["action"].initial = UsersForm.REMOVE
-        context.update({"form": form})
+        editor_remove_form = EditorsForm()
+        editor_remove_form.fields["action"].initial = EditorsForm.REMOVE
+
+        context.update(
+            {"form": form, "editor_remove_form": editor_remove_form}
+        )
 
         pending_permission_requests = AlgorithmPermissionRequest.objects.filter(
             algorithm=context["object"],
diff --git a/app/grandchallenge/archives/views.py b/app/grandchallenge/archives/views.py
--- a/app/grandchallenge/archives/views.py
+++ b/app/grandchallenge/archives/views.py
@@ -141,12 +141,16 @@ def get_context_data(self, **kwargs):
         uploader_remove_form = UploadersForm()
         uploader_remove_form.fields["action"].initial = UploadersForm.REMOVE
 
+        editor_remove_form = EditorsForm()
+        editor_remove_form.fields["action"].initial = EditorsForm.REMOVE
+
         limit = 1000
 
         context.update(
             {
                 "user_remove_form": user_remove_form,
                 "uploader_remove_form": uploader_remove_form,
+                "editor_remove_form": editor_remove_form,
                 "now": now().isoformat(),
                 "limit": limit,
                 "offsets": range(0, context["object"].images.count(), limit),


Test patch:
diff --git a/app/tests/archives_tests/factories.py b/app/tests/archives_tests/factories.py
--- a/app/tests/archives_tests/factories.py
+++ b/app/tests/archives_tests/factories.py
@@ -1,7 +1,7 @@
 import factory
 
-from grandchallenge.archives.models import Archive
-from tests.cases_tests.factories import ImageFactory
+from grandchallenge.archives.models import Archive, ArchivePermissionRequest
+from tests.factories import ImageFactory, UserFactory
 
 
 class ArchiveFactory(factory.django.DjangoModelFactory):
@@ -20,3 +20,11 @@ def images(self, create, extracted, **kwargs):
                 self.images.add(image)
         if create and not extracted:
             self.images.add(ImageFactory())
+
+
+class ArchivePermissionRequestFactory(factory.django.DjangoModelFactory):
+    class Meta:
+        model = ArchivePermissionRequest
+
+    archive = factory.SubFactory(ArchiveFactory)
+    user = factory.SubFactory(UserFactory)
diff --git a/app/tests/archives_tests/test_forms.py b/app/tests/archives_tests/test_forms.py
new file mode 100644
--- /dev/null
+++ b/app/tests/archives_tests/test_forms.py
@@ -0,0 +1,149 @@
+import pytest
+from django.contrib.auth.models import Permission
+
+from grandchallenge.archives.models import (
+    Archive,
+    ArchivePermissionRequest,
+)
+from grandchallenge.core.management.commands.init_gc_demo import (
+    get_temporary_image,
+)
+from tests.archives_tests.factories import (
+    ArchiveFactory,
+    ArchivePermissionRequestFactory,
+)
+from tests.factories import UserFactory, WorkstationFactory
+from tests.utils import get_view_for_user
+
+
+@pytest.mark.django_db
+def test_editor_update_form(client):
+    archive = ArchiveFactory()
+
+    editor = UserFactory()
+    archive.editors_group.user_set.add(editor)
+
+    assert archive.editors_group.user_set.count() == 1
+
+    new_editor = UserFactory()
+    assert not archive.is_editor(user=new_editor)
+    response = get_view_for_user(
+        viewname="archives:editors-update",
+        client=client,
+        method=client.post,
+        data={"user": new_editor.pk, "action": "ADD"},
+        reverse_kwargs={"slug": archive.slug},
+        follow=True,
+        user=editor,
+    )
+    assert response.status_code == 200
+
+    archive.refresh_from_db()
+    assert archive.editors_group.user_set.count() == 2
+    assert archive.is_editor(user=new_editor)
+
+    response = get_view_for_user(
+        viewname="archives:editors-update",
+        client=client,
+        method=client.post,
+        data={"user": new_editor.pk, "action": "REMOVE"},
+        reverse_kwargs={"slug": archive.slug},
+        follow=True,
+        user=editor,
+    )
+    assert response.status_code == 200
+
+    archive.refresh_from_db()
+    assert archive.editors_group.user_set.count() == 1
+    assert not archive.is_editor(user=new_editor)
+
+
+@pytest.mark.django_db
+def test_user_update_form(client):
+    archive = ArchiveFactory()
+
+    editor = UserFactory()
+    archive.editors_group.user_set.add(editor)
+
+    assert archive.users_group.user_set.count() == 0
+
+    new_user = UserFactory()
+    pr = ArchivePermissionRequestFactory(user=new_user, archive=archive)
+
+    assert not archive.is_user(user=new_user)
+    assert pr.status == ArchivePermissionRequest.PENDING
+    response = get_view_for_user(
+        viewname="archives:users-update",
+        client=client,
+        method=client.post,
+        data={"user": new_user.pk, "action": "ADD"},
+        reverse_kwargs={"slug": archive.slug},
+        follow=True,
+        user=editor,
+    )
+    assert response.status_code == 200
+
+    archive.refresh_from_db()
+    pr.refresh_from_db()
+    assert archive.users_group.user_set.count() == 1
+    assert archive.is_user(user=new_user)
+    assert pr.status == ArchivePermissionRequest.ACCEPTED
+
+    response = get_view_for_user(
+        viewname="archives:users-update",
+        client=client,
+        method=client.post,
+        data={"user": new_user.pk, "action": "REMOVE"},
+        reverse_kwargs={"slug": archive.slug},
+        follow=True,
+        user=editor,
+    )
+    assert response.status_code == 200
+
+    archive.refresh_from_db()
+    pr.refresh_from_db()
+    assert archive.users_group.user_set.count() == 0
+    assert not archive.is_user(user=new_user)
+    assert pr.status == ArchivePermissionRequest.REJECTED
+
+
+@pytest.mark.django_db
+def test_archive_create(client):
+    # The archive creator should automatically get added to the editors group
+    creator = UserFactory()
+    add_archive_perm = Permission.objects.get(
+        codename=f"add_{Archive._meta.model_name}"
+    )
+    creator.user_permissions.add(add_archive_perm)
+
+    ws = WorkstationFactory()
+
+    def try_create_archive():
+        return get_view_for_user(
+            viewname="archives:create",
+            client=client,
+            method=client.post,
+            data={
+                "title": "foo bar",
+                "logo": get_temporary_image(),
+                "workstation": ws.pk,
+            },
+            follow=True,
+            user=creator,
+        )
+
+    response = try_create_archive()
+    assert "error_1_id_workstation" in response.rendered_content
+
+    # The editor must have view permissions for the workstation to add it
+    ws.add_user(user=creator)
+
+    response = try_create_archive()
+    assert "error_1_id_workstation" not in response.rendered_content
+    assert response.status_code == 200
+
+    archive = Archive.objects.get(title="foo bar")
+
+    assert archive.slug == "foo-bar"
+    assert archive.is_editor(user=creator)
+    assert not archive.is_user(user=creator)