Repository: carpentries/amy

Issue:
Implementation of auto assigning roles with a badge
Following #2194, implement the change that auto assigns a role when the corresponding badge is awarded. 

(EDIT - Wrong issue was mentioned)


Discussion:

Resolving patch:
diff --git a/amy/communityroles/forms.py b/amy/communityroles/forms.py
--- a/amy/communityroles/forms.py
+++ b/amy/communityroles/forms.py
@@ -3,8 +3,9 @@
 from typing import Any, Optional, Union
 
 from django import forms
+from django.contrib.contenttypes.models import ContentType
 from django.core.exceptions import ObjectDoesNotExist, ValidationError
-from django.db.models import Q
+from django.db.models import Q, QuerySet
 
 from workshops.fields import HeavySelect2Widget, ModelSelect2Widget
 from workshops.forms import SELECT2_SIDEBAR, BootstrapHelper, WidgetOverrideMixin
@@ -121,14 +122,11 @@ def clean(self) -> Union[dict[str, Any], None]:
                 ValidationError(f"URL is not supported for community role {config}")
             )
 
-        # Widget for `generic_relation_content_type` is disabled in HTML, which
-        # makes browsers not send it. The code below sets the default value to
-        # the same value as in related config.
-        generic_relation_content_type = config.generic_relation_content_type
-
         # Generic relation object must exist
-        if config.generic_relation_content_type and generic_relation_content_type:
-            model_class = generic_relation_content_type.model_class()
+        if config.generic_relation_content_type and cleaned_data.get(
+            "generic_relation_pk"
+        ):
+            model_class = config.generic_relation_content_type.model_class()
             try:
                 model_class._base_manager.get(
                     pk=cleaned_data.get("generic_relation_pk")
@@ -147,10 +145,17 @@ def clean(self) -> Union[dict[str, Any], None]:
                 ValidationError("Required when Reason for inactivation selected.")
             )
 
-        try:
-            self.check_concurrent_role(config, person, start_date, end_date)
-        except ValidationError as exc:
-            errors["person"].append(exc)
+        # Person should not have any concurrent Community Roles of the same type in the
+        # same time.
+        if concurrent_roles := self.find_concurrent_roles(
+            config, person, start_date, end_date
+        ):
+            errors["person"].append(
+                ValidationError(
+                    f"Person {person} has concurrent community roles: "
+                    f"{list(concurrent_roles)}."
+                )
+            )
 
         if errors:
             raise ValidationError(errors)  # type: ignore
@@ -165,31 +170,36 @@ def clean_end(self):
             raise ValidationError("Must not be earlier than start date.")
         return end
 
-    def check_concurrent_role(
-        self,
+    def clean_generic_relation_content_type(self) -> Optional[ContentType]:
+        """Copy content type from the Community Role Configuration."""
+
+        # Widget for `generic_relation_content_type` is disabled in HTML, which
+        # makes browsers not send it. The code below sets the default value to
+        # the same value as in related config.
+        config: Optional[CommunityRoleConfig] = self.cleaned_data.get("config")
+        if config:
+            return config.generic_relation_content_type
+        return None
+
+    @staticmethod
+    def find_concurrent_roles(
         config: Optional[CommunityRoleConfig] = None,
         person: Optional[Person] = None,
         start_date: Optional[date] = None,
         end_date: Optional[date] = None,
-    ) -> Optional[Person]:
-        """Person should not have any concurrent Community Roles of the same type in the
-        same time."""
+    ) -> Optional[QuerySet[CommunityRole]]:
+        """Lookup concurrent Community Roles."""
         # These are required fields in the form, so they should be present.
         if config and person and start_date:
             same_time = Q(end__gt=start_date) | Q(end__isnull=True)
             if end_date:
                 # if `end_date` is present, introduce additional condition
                 same_time &= Q(start__lt=end_date) | Q(start__isnull=True)
-            if roles := CommunityRole.objects.filter(
-                person=person, config=config
-            ).filter(same_time):
-                raise (
-                    ValidationError(
-                        f"Person {person} has concurrent community roles: "
-                        f"{list(roles)}."
-                    )
-                )
-        return person
+            roles = CommunityRole.objects.filter(person=person, config=config).filter(
+                same_time
+            )
+            return roles
+        return None
 
 
 class CommunityRoleUpdateForm(CommunityRoleForm):
@@ -210,12 +220,12 @@ def __init__(self, *args, community_role_config: CommunityRoleConfig, **kwargs):
             self.config.custom_key_labels
         )
 
-    def check_concurrent_role(
-        self,
+    @staticmethod
+    def find_concurrent_roles(
         config: Optional[CommunityRoleConfig] = None,
         person: Optional[Person] = None,
         start_date: Optional[date] = None,
         end_date: Optional[date] = None,
-    ) -> Optional[Person]:
+    ) -> Optional[QuerySet[CommunityRole]]:
         """When updating a CommunityRole, we shouldn't check for concurrent roles."""
-        return person
+        return None
diff --git a/amy/workshops/views.py b/amy/workshops/views.py
--- a/amy/workshops/views.py
+++ b/amy/workshops/views.py
@@ -34,7 +34,7 @@
     Value,
     When,
 )
-from django.forms import HiddenInput
+from django.forms import BaseForm, HiddenInput
 from django.http import Http404, HttpResponse, HttpResponseBadRequest, JsonResponse
 from django.shortcuts import get_object_or_404, redirect, render
 from django.urls import reverse, reverse_lazy
@@ -55,7 +55,7 @@
 from autoemails.base_views import ActionManageMixin
 from autoemails.models import Trigger
 from communityroles.forms import CommunityRoleForm
-from communityroles.models import CommunityRole
+from communityroles.models import CommunityRole, CommunityRoleConfig
 from consents.forms import ActiveTermConsentsForm
 from consents.models import Consent
 from dashboard.forms import AssignmentForm
@@ -130,7 +130,8 @@
 from workshops.utils.usernames import create_username
 from workshops.utils.views import failed_to_delete
 
-logger = logging.getLogger("amy.signals")
+logger = logging.getLogger("amy.server_logs")
+logger_signals = logging.getLogger("amy.signals")
 scheduler = django_rq.get_scheduler("default")
 redis_connection = django_rq.get_connection("default")
 
@@ -1106,7 +1107,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.add(
                 action_class=PostWorkshopAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object),
@@ -1121,7 +1122,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.add(
                 action_class=InstructorsHostIntroductionAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object),
@@ -1190,7 +1191,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.add(
                 action_class=PostWorkshopAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object),
@@ -1205,7 +1206,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.remove(
                 action_class=PostWorkshopAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -1220,7 +1221,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.add(
                 action_class=InstructorsHostIntroductionAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object),
@@ -1235,7 +1236,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.remove(
                 action_class=InstructorsHostIntroductionAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -1248,7 +1249,7 @@ def form_valid(self, form):
             triggers = Trigger.objects.filter(active=True, action="ask-for-website")
             ActionManageMixin.add(
                 action_class=AskForWebsiteAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object),
@@ -1261,7 +1262,7 @@ def form_valid(self, form):
             jobs = self.object.rq_jobs.filter(trigger__action="ask-for-website")
             ActionManageMixin.remove(
                 action_class=AskForWebsiteAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -1274,7 +1275,7 @@ def form_valid(self, form):
             triggers = Trigger.objects.filter(active=True, action="recruit-helpers")
             ActionManageMixin.add(
                 action_class=RecruitHelpersAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object),
@@ -1287,7 +1288,7 @@ def form_valid(self, form):
             jobs = self.object.rq_jobs.filter(trigger__action="recruit-helpers")
             ActionManageMixin.remove(
                 action_class=RecruitHelpersAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -1309,7 +1310,7 @@ def before_delete(self, *args, **kwargs):
         )
         ActionManageMixin.remove(
             action_class=PostWorkshopAction,
-            logger=logger,
+            logger=logger_signals,
             scheduler=scheduler,
             connection=redis_connection,
             jobs=jobs.values_list("job_id", flat=True),
@@ -1322,7 +1323,7 @@ def before_delete(self, *args, **kwargs):
         )
         ActionManageMixin.remove(
             action_class=InstructorsHostIntroductionAction,
-            logger=logger,
+            logger=logger_signals,
             scheduler=scheduler,
             connection=redis_connection,
             jobs=jobs.values_list("job_id", flat=True),
@@ -1335,7 +1336,7 @@ def before_delete(self, *args, **kwargs):
         jobs = self.object.rq_jobs.filter(trigger__action="ask-for-website")
         ActionManageMixin.remove(
             action_class=AskForWebsiteAction,
-            logger=logger,
+            logger=logger_signals,
             scheduler=scheduler,
             connection=redis_connection,
             jobs=jobs.values_list("job_id", flat=True),
@@ -1346,7 +1347,7 @@ def before_delete(self, *args, **kwargs):
         jobs = self.object.rq_jobs.filter(trigger__action="recruit-helpers")
         ActionManageMixin.remove(
             action_class=RecruitHelpersAction,
-            logger=logger,
+            logger=logger_signals,
             scheduler=scheduler,
             connection=redis_connection,
             jobs=jobs.values_list("job_id", flat=True),
@@ -1750,12 +1751,13 @@ def form_valid(self, form):
 
         # save the object
         res = super().form_valid(form)
+        self.object: Task  # created and saved to DB by super().form_valid()
 
         # check conditions for running a NewInstructorAction
         if NewInstructorAction.check(self.object):
             ActionManageMixin.add(
                 action_class=NewInstructorAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=Trigger.objects.filter(active=True, action="new-instructor"),
                 context_objects=dict(task=self.object, event=self.object.event),
@@ -1767,7 +1769,7 @@ def form_valid(self, form):
         if NewSupportingInstructorAction.check(self.object):
             ActionManageMixin.add(
                 action_class=NewSupportingInstructorAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=Trigger.objects.filter(
                     active=True, action="new-supporting-instructor"
@@ -1786,7 +1788,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.add(
                 action_class=InstructorsHostIntroductionAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object.event),
@@ -1799,7 +1801,7 @@ def form_valid(self, form):
             triggers = Trigger.objects.filter(active=True, action="ask-for-website")
             ActionManageMixin.add(
                 action_class=AskForWebsiteAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object.event),
@@ -1812,7 +1814,7 @@ def form_valid(self, form):
             triggers = Trigger.objects.filter(active=True, action="recruit-helpers")
             ActionManageMixin.add(
                 action_class=RecruitHelpersAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object.event),
@@ -1826,7 +1828,7 @@ def form_valid(self, form):
             jobs = self.object.event.rq_jobs.filter(trigger__action="recruit-helpers")
             ActionManageMixin.remove(
                 action_class=RecruitHelpersAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -1897,7 +1899,7 @@ def form_valid(self, form):
             triggers = Trigger.objects.filter(active=True, action="new-instructor")
             ActionManageMixin.add(
                 action_class=NewInstructorAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(task=self.object, event=self.object.event),
@@ -1910,7 +1912,7 @@ def form_valid(self, form):
             jobs = self.object.rq_jobs.filter(trigger__action="new-instructor")
             ActionManageMixin.remove(
                 action_class=NewInstructorAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -1925,7 +1927,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.add(
                 action_class=NewSupportingInstructorAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(task=self.object, event=self.object.event),
@@ -1940,7 +1942,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.remove(
                 action_class=NewSupportingInstructorAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -1955,7 +1957,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.add(
                 action_class=InstructorsHostIntroductionAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object.event),
@@ -1970,7 +1972,7 @@ def form_valid(self, form):
             )
             ActionManageMixin.remove(
                 action_class=InstructorsHostIntroductionAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -1983,7 +1985,7 @@ def form_valid(self, form):
             triggers = Trigger.objects.filter(active=True, action="ask-for-website")
             ActionManageMixin.add(
                 action_class=AskForWebsiteAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object.event),
@@ -1996,7 +1998,7 @@ def form_valid(self, form):
             jobs = self.object.event.rq_jobs.filter(trigger__action="ask-for-website")
             ActionManageMixin.remove(
                 action_class=AskForWebsiteAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -2009,7 +2011,7 @@ def form_valid(self, form):
             triggers = Trigger.objects.filter(active=True, action="recruit-helpers")
             ActionManageMixin.add(
                 action_class=RecruitHelpersAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.object.event),
@@ -2022,7 +2024,7 @@ def form_valid(self, form):
             jobs = self.object.event.rq_jobs.filter(trigger__action="recruit-helpers")
             ActionManageMixin.remove(
                 action_class=RecruitHelpersAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -2048,7 +2050,7 @@ def before_delete(self, *args, **kwargs):
         jobs = self.object.rq_jobs.filter(trigger__action="new-instructor")
         ActionManageMixin.remove(
             action_class=NewInstructorAction,
-            logger=logger,
+            logger=logger_signals,
             scheduler=scheduler,
             connection=redis_connection,
             jobs=jobs.values_list("job_id", flat=True),
@@ -2059,7 +2061,7 @@ def before_delete(self, *args, **kwargs):
         jobs = self.object.rq_jobs.filter(trigger__action="new-supporting-instructor")
         ActionManageMixin.remove(
             action_class=NewSupportingInstructorAction,
-            logger=logger,
+            logger=logger_signals,
             scheduler=scheduler,
             connection=redis_connection,
             jobs=jobs.values_list("job_id", flat=True),
@@ -2087,7 +2089,7 @@ def after_delete(self, *args, **kwargs):
             )
             ActionManageMixin.remove(
                 action_class=InstructorsHostIntroductionAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -2100,7 +2102,7 @@ def after_delete(self, *args, **kwargs):
             jobs = self.object.event.rq_jobs.filter(trigger__action="ask-for-website")
             ActionManageMixin.remove(
                 action_class=AskForWebsiteAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -2113,7 +2115,7 @@ def after_delete(self, *args, **kwargs):
             triggers = Trigger.objects.filter(active=True, action="recruit-helpers")
             ActionManageMixin.add(
                 action_class=RecruitHelpersAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 triggers=triggers,
                 context_objects=dict(event=self.event),
@@ -2126,7 +2128,7 @@ def after_delete(self, *args, **kwargs):
             jobs = self.object.event.rq_jobs.filter(trigger__action="recruit-helpers")
             ActionManageMixin.remove(
                 action_class=RecruitHelpersAction,
-                logger=logger,
+                logger=logger_signals,
                 scheduler=scheduler,
                 connection=redis_connection,
                 jobs=jobs.values_list("job_id", flat=True),
@@ -2170,6 +2172,46 @@ def get_initial(self, **kwargs):
     def get_success_url(self):
         return reverse("badge_details", args=[self.object.badge.name])
 
+    def form_valid(self, form: BaseForm) -> HttpResponse:
+        result = super().form_valid(form)
+        self.object: Award  # created and saved to DB by super().form_valid()
+
+        # Check for CommunityRoles that should automatically be created.
+        badge = self.object.badge
+        person = self.object.person
+        community_role_configs = CommunityRoleConfig.objects.filter(
+            award_badge_limit=badge,
+            autoassign_when_award_created=True,
+        )
+        if community_role_configs:
+            start_date = datetime.date.today()
+            roles = [
+                CommunityRole(
+                    config=config,
+                    person=person,
+                    award=self.object,
+                    start=start_date,
+                    end=None,
+                    inactivation=None,
+                    membership=None,
+                    url="",
+                )
+                for config in community_role_configs
+                if not CommunityRoleForm.find_concurrent_roles(
+                    config, person, start_date
+                )
+            ]
+            logger.debug(
+                f"Automatically creating community roles set up for badge {badge}"
+            )
+            roles_result = CommunityRole.objects.bulk_create(roles)
+            logger.debug(
+                f"Created {len(roles_result)} Community Roles for badge {badge} and "
+                f"person {person}"
+            )
+
+        return result
+
 
 class AwardCreate(RedirectSupportMixin, MockAwardCreate):
     pass


Test patch:
diff --git a/amy/communityroles/tests/test_autocreating_community_roles.py b/amy/communityroles/tests/test_autocreating_community_roles.py
new file mode 100644
--- /dev/null
+++ b/amy/communityroles/tests/test_autocreating_community_roles.py
@@ -0,0 +1,105 @@
+"""When an Award is created, it may automatically create Community Role(s)."""
+from datetime import date
+
+from django.urls import reverse
+
+from communityroles.models import CommunityRole, CommunityRoleConfig
+from workshops.models import Badge, Person
+from workshops.tests.base import TestBase
+
+
+class TestAutocreatingCommunityRoles(TestBase):
+    def setUp(self) -> None:
+        self.badge, _ = Badge.objects.get_or_create(name="instructor")
+        self.config1 = CommunityRoleConfig.objects.create(
+            name="test1",
+            display_name="Test config 1",
+            link_to_award=True,
+            award_badge_limit=self.badge,
+            autoassign_when_award_created=True,
+            link_to_membership=False,
+            additional_url=False,
+        )
+        self.config2 = CommunityRoleConfig.objects.create(
+            name="test2",
+            display_name="Test config 2",
+            link_to_award=True,
+            award_badge_limit=self.badge,
+            autoassign_when_award_created=True,
+            link_to_membership=False,
+            additional_url=False,
+        )
+
+        # This config will not be used since it doesn't have autoassign
+        self.config3 = CommunityRoleConfig.objects.create(
+            name="test3",
+            display_name="Test config 3",
+            link_to_award=True,
+            award_badge_limit=self.badge,
+            autoassign_when_award_created=False,
+            link_to_membership=False,
+            additional_url=False,
+        )
+
+        self.person = Person.objects.create(
+            personal="Test",
+            family="Family",
+            email="test.person@example.org",
+        )
+        self.url = reverse("award_add")
+
+        self._setUpUsersAndLogin()
+
+    def test_autoassign(self) -> None:
+        # Arrange
+        data = {
+            "award-person": self.person.pk,
+            "award-badge": self.badge.pk,
+            "award-awarded": "2022-09-19",
+        }
+
+        # Act
+        result = self.client.post(self.url, data)
+
+        # Assert
+        self.assertEqual(result.status_code, 302)
+        roles = CommunityRole.objects.filter(person=self.person)
+        self.assertEqual(len(roles), 2)
+        self.assertEqual(roles[0].config, self.config1)
+        self.assertEqual(roles[1].config, self.config2)
+        self.assertEqual(roles[0].start, date.today())
+        self.assertEqual(roles[1].start, date.today())
+        self.assertEqual(roles[0].end, None)
+        self.assertEqual(roles[1].end, None)
+
+    def test_autoassign_considers_concurrent_roles(self) -> None:
+        # Arrange
+        CommunityRole.objects.create(
+            config=self.config1,
+            person=self.person,
+            award=None,
+            start=date(2022, 9, 1),
+            end=None,
+            inactivation=None,
+            membership=None,
+            url="",
+        )
+        data = {
+            "award-person": self.person.pk,
+            "award-badge": self.badge.pk,
+            "award-awarded": "2022-09-19",
+        }
+
+        # Act
+        result = self.client.post(self.url, data)
+
+        # Assert
+        self.assertEqual(result.status_code, 302)
+        roles = CommunityRole.objects.filter(person=self.person).order_by("start")
+        self.assertEqual(len(roles), 2)
+        self.assertEqual(roles[0].config, self.config1)
+        self.assertEqual(roles[0].start, date(2022, 9, 1))
+        self.assertEqual(roles[0].end, None)
+        self.assertEqual(roles[1].config, self.config2)
+        self.assertEqual(roles[1].start, date.today())
+        self.assertEqual(roles[1].end, None)
diff --git a/amy/communityroles/tests/test_community_role_forms.py b/amy/communityroles/tests/test_community_role_forms.py
--- a/amy/communityroles/tests/test_community_role_forms.py
+++ b/amy/communityroles/tests/test_community_role_forms.py
@@ -316,7 +316,7 @@ def test_generic_relation_object_doesnt_exist(self):
             "membership": self.membership.pk,
             "url": "https://example.org",  # should be empty
             "generic_relation_content_type": ct.pk,
-            "generic_relation_pk": None,
+            "generic_relation_pk": 102390128371902,
         }
 
         # Act