diff --git a/evap/development/fixtures/test_data.json b/evap/development/fixtures/test_data.json index 635d5d9118..0b04b413c6 100644 --- a/evap/development/fixtures/test_data.json +++ b/evap/development/fixtures/test_data.json @@ -132980,7 +132980,7 @@ "title": "", "first_name_given": "", "first_name_chosen": "", - "last_name": "", + "last_name": "reviewer", "language": "", "is_proxy_user": false, "login_key": null, @@ -133008,7 +133008,7 @@ "title": "", "first_name_given": "", "first_name_chosen": "", - "last_name": "", + "last_name": "proxy", "language": "", "is_proxy_user": true, "login_key": null, @@ -133043,7 +133043,7 @@ "title": "", "first_name_given": "", "first_name_chosen": "", - "last_name": "", + "last_name": "proxy_delegate", "language": "", "is_proxy_user": false, "login_key": null, @@ -133071,7 +133071,79 @@ "title": "", "first_name_given": "", "first_name_chosen": "", - "last_name": "", + "last_name": "proxy_delegate_2", + "language": "", + "is_proxy_user": false, + "login_key": null, + "login_key_valid_until": null, + "is_active": true, + "notes": "", + "startpage": "DE", + "groups": [], + "user_permissions": [], + "delegates": [], + "cc_users": [] + } +}, +{ + "model": "evaluation.userprofile", + "fields": { + "password": "eZAyFmtqHydCIFtGdbevAxiVjiRpqMtmaVUCrmkcfXdoJDigmGWPVNHeoYYyRojokKUJjsgPSPvZkjiiIHSIQlBfOKtQFDbZlPEyKnrQRrHdPtEhUYHqJauIlyIkYpBM", + "last_login": null, + "is_superuser": false, + "email": "vincenzo.boston@student.institution.example.com", + "title": "", + "first_name_given": "Vincenzo Alfredo", + "first_name_chosen": "", + "last_name": "Boston", + "language": "", + "is_proxy_user": false, + "login_key": null, + "login_key_valid_until": null, + "is_active": true, + "notes": "", + "startpage": "DE", + "groups": [], + "user_permissions": [], + "delegates": [], + "cc_users": [] + } +}, +{ + "model": "evaluation.userprofile", + "fields": { + "password": "utAhMBbTpirVqtaoPpadEHdamaehnXWbEsliMMSnwDBYJcTnHluinAxkTeEupPoBzpuDBMYeXbpwmockMtQNYegbMuxkUBEBKqWGkOEFAWxzUFjdxevtIwYzvAgHCAwD", + "last_login": null, + "is_superuser": false, + "email": "bud.ledbetter@student.institution.example.com", + "title": "", + "first_name_given": "Bud", + "first_name_chosen": "", + "last_name": "LedBetter", + "language": "", + "is_proxy_user": false, + "login_key": null, + "login_key_valid_until": null, + "is_active": true, + "notes": "", + "startpage": "DE", + "groups": [], + "user_permissions": [], + "delegates": [], + "cc_users": [] + } +}, +{ + "model": "evaluation.userprofile", + "fields": { + "password": "naFmzOVrFhXrVVLsIGFYceDAarTGwDRFZKGJwBvKhNFCpupezBrwhorUHsyQSpUxLFKSQuOurcIyoBBYRjARXjzcJCbqYRiKRMOwvdTqwNjAbYDhUKbopBPDYhANXUkI", + "last_login": null, + "is_superuser": false, + "email": "melody.large@student.institution.example.com", + "title": "", + "first_name_given": "Melody", + "first_name_chosen": "", + "last_name": "Large", "language": "", "is_proxy_user": false, "login_key": null, diff --git a/evap/settings.py b/evap/settings.py index 9fa6190e5a..ed75d335d3 100644 --- a/evap/settings.py +++ b/evap/settings.py @@ -67,7 +67,7 @@ # email domains for the internal users of the hosting institution used to # figure out who is an internal user -INSTITUTION_EMAIL_DOMAINS = ["institution.example.com"] +INSTITUTION_EMAIL_DOMAINS = ["institution.example.com", "student.institution.example.com"] # List of tuples defining email domains that should be replaced on saving UserProfiles. # Emails ending on the first value will have this part replaced by the second value. diff --git a/evap/staff/templates/staff_user_merge_selection.html b/evap/staff/templates/staff_user_merge_selection.html index 8cccecb44c..8a04ad8407 100644 --- a/evap/staff/templates/staff_user_merge_selection.html +++ b/evap/staff/templates/staff_user_merge_selection.html @@ -8,20 +8,50 @@ {% block content %} {{ block.super }} -

{% trans 'Merge users' %}

- -
- {% csrf_token %} -
-
-

{% trans 'Select the users you want to merge.' %}

- {% include 'bootstrap_form.html' with form=form wide=True %} +
+
+
+
+

{% trans 'Merge users' %}

+ + {% csrf_token %} +

{% trans 'Select the users you want to merge.' %}

+ {% include 'bootstrap_form.html' with form=form wide=True %} + +
+
-
-
- +
+
+
+

{% trans 'Merge suggestions' %}

+ + + {% for main_user, merge_candidate in suggested_merges %} + + + + + + {% endfor %} + +
+ {{ main_user.full_name }}
+ {{ merge_candidate.full_name }} +
+ {{ main_user.email }}
+ {{ merge_candidate.email }} +
+ + + +
+
- +
+ {% endblock %} diff --git a/evap/staff/tests/test_views.py b/evap/staff/tests/test_views.py index 4e126ac97e..4ce5b7408f 100644 --- a/evap/staff/tests/test_views.py +++ b/evap/staff/tests/test_views.py @@ -339,7 +339,7 @@ def get_post_params(cls): class TestUserMergeSelectionView(WebTestStaffMode): - url = "/staff/user/merge" + url = reverse("staff:user_merge_selection") @classmethod def setUpTestData(cls): @@ -348,6 +348,10 @@ def setUpTestData(cls): cls.main_user = baker.make(UserProfile, _fill_optional=["email"]) cls.other_user = baker.make(UserProfile, _fill_optional=["email"]) + # The merge candidate is created first, so the account is older. + cls.suggested_merge_candidate = baker.make(UserProfile, email="user@student.institution.example.com") + cls.suggested_main_user = baker.make(UserProfile, email="user@institution.example.com") + def test_redirection_user_merge_view(self): page = self.app.get(self.url, user=self.manager) @@ -360,6 +364,19 @@ def test_redirection_user_merge_view(self): self.assertContains(page, self.main_user.email) self.assertContains(page, self.other_user.email) + def test_suggested_merge(self): + page = self.app.get(self.url, user=self.manager) + + expected_url = reverse( + "staff:user_merge", args=[self.suggested_main_user.id, self.suggested_merge_candidate.id] + ) + unexpected_url = reverse( + "staff:user_merge", args=[self.suggested_merge_candidate.id, self.suggested_main_user.id] + ) + + self.assertContains(page, f' dict[str, Any]: + context = super().get_context_data(**kwargs) + + class UserNameFromEmail(Func): + # django docs support our usage here: + # https://docs.djangoproject.com/en/5.0/ref/models/expressions/#func-expressions + # pylint: disable=abstract-method + template = "split_part(%(expressions)s, '@', 1)" + + query = UserProfile.objects.annotate(username_part_of_email=UserNameFromEmail("email")) + + users_with_merge_candidates = query.annotate( + merge_candidate_pk=query.filter(username_part_of_email=UserNameFromEmail(OuterRef("email"))) + .filter(pk__lt=OuterRef("pk")) + .values("pk")[:1] + ).exclude(merge_candidate_pk=None) + + merge_candidate_ids = [user.merge_candidate_pk for user in users_with_merge_candidates] + merge_candidates_by_id = {user.pk: user for user in UserProfile.objects.filter(pk__in=merge_candidate_ids)} + + suggested_merges = [ + (user, merge_candidates_by_id[user.merge_candidate_pk]) + for user in users_with_merge_candidates + if not user.is_external and not merge_candidates_by_id[user.merge_candidate_pk].is_external + ] + + context["suggested_merges"] = suggested_merges + return context + def form_valid(self, form: UserMergeSelectionForm) -> HttpResponse: return redirect( "staff:user_merge",