Repository: e-valuation/EvaP

Issue:
Link to user edit pages in importer messages
When the importer shows messages about users (such as "This new user has the same email as this existing user"), the respective users' edit pages should be linked in the message to enable managers to easily open the user profile for inspection.


Discussion:

Resolving patch:
diff --git a/evap/staff/importers.py b/evap/staff/importers.py
--- a/evap/staff/importers.py
+++ b/evap/staff/importers.py
@@ -9,12 +9,13 @@
 from django.core.exceptions import ValidationError
 from django.db import transaction
 from django.utils.html import format_html, format_html_join
+from django.utils.safestring import mark_safe
 from django.utils.translation import gettext as _
 from django.utils.translation import gettext_lazy
 
 from evap.evaluation.models import Contribution, Course, CourseType, Degree, Evaluation, UserProfile
 from evap.evaluation.tools import clean_email, unordered_groupby
-from evap.staff.tools import ImportType, create_user_list_html_string_for_message
+from evap.staff.tools import ImportType, create_user_list_html_string_for_message, user_edit_link
 
 
 def sorted_messages(messages):
@@ -321,13 +322,17 @@ def _create_user_string(user):
     @staticmethod
     def _create_user_data_mismatch_warning(user, user_data, test_run):
         if test_run:
-            msg = format_html(_("The existing user would be overwritten with the following data:"))
+            msg = _("The existing user would be overwritten with the following data:")
         else:
-            msg = format_html(_("The existing user was overwritten with the following data:"))
-        return (
-            msg
-            + format_html("<br /> - {} ({})", ExcelImporter._create_user_string(user), _("existing"))
-            + format_html("<br /> - {} ({})", ExcelImporter._create_user_string(user_data), _("new"))
+            msg = _("The existing user was overwritten with the following data:")
+        return format_html(
+            "{}<br /> - {} ({}) [{}]<br /> - {} ({})",
+            msg,
+            ExcelImporter._create_user_string(user),
+            _("existing"),
+            user_edit_link(user.pk),
+            ExcelImporter._create_user_string(user_data),
+            _("new"),
         )
 
     @staticmethod
@@ -335,19 +340,26 @@ def _create_user_inactive_warning(user, test_run):
         user_string = ExcelImporter._create_user_string(user)
         if test_run:
             return format_html(
-                _("The following user is currently marked inactive and will be marked active upon importing: {}"),
+                _("The following user is currently marked inactive and will be marked active upon importing: {} [{}]"),
                 user_string,
+                user_edit_link(user.pk),
             )
 
         return format_html(
-            _("The following user was previously marked inactive and is now marked active upon importing: {}"),
+            _("The following user was previously marked inactive and is now marked active upon importing: {} [{}]"),
             user_string,
+            user_edit_link(user.pk),
         )
 
     def _create_user_name_collision_warning(self, user_data, users_with_same_names):
-        warningstring = format_html(_("An existing user has the same first and last name as a new user:"))
+        warningstring = mark_safe(_("An existing user has the same first and last name as a new user:"))
         for user in users_with_same_names:
-            warningstring += format_html("<br /> - {} ({})", self._create_user_string(user), _("existing"))
+            warningstring += format_html(
+                "<br /> - {} ({}) [{}]",
+                self._create_user_string(user),
+                _("existing"),
+                user_edit_link(user.pk),
+            )
         warningstring += format_html("<br /> - {} ({})", self._create_user_string(user_data), _("new"))
 
         self.warnings[ImporterWarning.DUPL].append(warningstring)
diff --git a/evap/staff/tools.py b/evap/staff/tools.py
--- a/evap/staff/tools.py
+++ b/evap/staff/tools.py
@@ -10,6 +10,7 @@
 from django.core.exceptions import SuspiciousOperation
 from django.db import transaction
 from django.db.models import Count
+from django.urls import reverse
 from django.utils.html import format_html, format_html_join
 from django.utils.translation import gettext_lazy as _
 
@@ -370,3 +371,11 @@ def remove_user_from_represented_and_ccing_users(user, ignored_users=None, test_
             cc_user.cc_users.remove(user)
             remove_messages.append(_("Removed {} from the CC users of {}.").format(user.full_name, cc_user.full_name))
     return remove_messages
+
+
+def user_edit_link(user_id):
+    return format_html(
+        '<a href="{}" target=_blank><span class="fas fa-user-pen"></span> {}</a>',
+        reverse("staff:user_edit", kwargs={"user_id": user_id}),
+        _("edit user"),
+    )


Test patch:
diff --git a/evap/staff/tests/test_importers.py b/evap/staff/tests/test_importers.py
--- a/evap/staff/tests/test_importers.py
+++ b/evap/staff/tests/test_importers.py
@@ -10,7 +10,7 @@
 import evap.staff.fixtures.excel_files_test_data as excel_data
 from evap.evaluation.models import Contribution, Course, CourseType, Degree, Evaluation, Semester, UserProfile
 from evap.staff.importers import EnrollmentImporter, ImporterError, ImporterWarning, PersonImporter, UserImporter
-from evap.staff.tools import ImportType
+from evap.staff.tools import ImportType, user_edit_link
 
 
 class TestUserImporter(TestCase):
@@ -71,7 +71,7 @@ def test_created_users(self):
         self.assertTrue(UserProfile.objects.filter(email="bastius.quid@external.example.com").exists())
 
     def test_duplicate_warning(self):
-        baker.make(UserProfile, first_name="Lucilia", last_name="Manilium", email="luma@institution.example.com")
+        user = baker.make(UserProfile, first_name="Lucilia", last_name="Manilium", email="luma@institution.example.com")
 
         __, __, warnings_test, __ = UserImporter.process(self.valid_excel_content, test_run=True)
         __, __, warnings_no_test, __ = UserImporter.process(self.valid_excel_content, test_run=False)
@@ -81,8 +81,8 @@ def test_duplicate_warning(self):
             warnings_test[ImporterWarning.DUPL],
             [
                 "An existing user has the same first and last name as a new user:<br />"
-                " -  Lucilia Manilium, luma@institution.example.com (existing)<br />"
-                " -  Lucilia Manilium, lucilia.manilium@institution.example.com (new)"
+                f" -  Lucilia Manilium, luma@institution.example.com (existing) [{user_edit_link(user.pk)}]<br />"
+                " -  Lucilia Manilium, lucilia.manilium@institution.example.com (new)",
             ],
         )
 
@@ -123,14 +123,14 @@ def test_invalid_file_error(self):
         self.assertEqual(UserProfile.objects.count(), original_user_count)
 
     def test_import_makes_inactive_user_active(self):
-        baker.make(UserProfile, email="lucilia.manilium@institution.example.com", is_active=False)
+        user = baker.make(UserProfile, email="lucilia.manilium@institution.example.com", is_active=False)
 
         __, __, warnings_test, __ = UserImporter.process(self.valid_excel_content, test_run=True)
         self.assertEqual(
             warnings_test[ImporterWarning.INACTIVE],
             [
                 "The following user is currently marked inactive and will be marked active upon importing: "
-                " None None, lucilia.manilium@institution.example.com"
+                f" None None, lucilia.manilium@institution.example.com [{user_edit_link(user.pk)}]",
             ],
         )
 
@@ -139,7 +139,7 @@ def test_import_makes_inactive_user_active(self):
             warnings_no_test[ImporterWarning.INACTIVE],
             [
                 "The following user was previously marked inactive and is now marked active upon importing: "
-                " None None, lucilia.manilium@institution.example.com"
+                f" None None, lucilia.manilium@institution.example.com [{user_edit_link(user.pk)}]"
             ],
         )
 
diff --git a/evap/staff/tests/test_tools.py b/evap/staff/tests/test_tools.py
--- a/evap/staff/tests/test_tools.py
+++ b/evap/staff/tests/test_tools.py
@@ -7,7 +7,12 @@
 from evap.evaluation.models import Contribution, Course, Evaluation, UserProfile
 from evap.evaluation.tests.tools import WebTest
 from evap.rewards.models import RewardPointGranting, RewardPointRedemption
-from evap.staff.tools import delete_navbar_cache_for_users, merge_users, remove_user_from_represented_and_ccing_users
+from evap.staff.tools import (
+    delete_navbar_cache_for_users,
+    merge_users,
+    remove_user_from_represented_and_ccing_users,
+    user_edit_link,
+)
 
 
 class NavbarCacheTest(WebTest):
@@ -257,3 +262,9 @@ def test_do_nothing_if_test_run(self):
         self.assertEqual([set(user1.delegates.all()), set(user1.cc_users.all())], [{delete_user}, {delete_user}])
         self.assertEqual([set(user2.delegates.all()), set(user2.cc_users.all())], [{delete_user}, {delete_user}])
         self.assertEqual(len(messages), 4)
+
+
+class UserEditLinkTest(TestCase):
+    def test_user_edit_link(self):
+        user = baker.make(UserProfile)
+        self.assertIn(f"/staff/user/{user.id}/edit", user_edit_link(user.id))
diff --git a/evap/staff/tests/test_views.py b/evap/staff/tests/test_views.py
--- a/evap/staff/tests/test_views.py
+++ b/evap/staff/tests/test_views.py
@@ -53,6 +53,7 @@
     helper_set_dynamic_choices_field_value,
     run_in_staff_mode,
 )
+from evap.staff.tools import user_edit_link
 from evap.staff.views import get_evaluations_with_prefetched_data
 from evap.student.models import TextAnswerWarning
 
@@ -536,7 +537,7 @@ def test_warning_handling(self):
         """
         Tests whether warnings given from the importer are displayed
         """
-        baker.make(UserProfile, email="lucilia.manilium@institution.example.com")
+        user = baker.make(UserProfile, email="lucilia.manilium@institution.example.com")
 
         page = self.app.get(self.url, user=self.manager)
 
@@ -547,7 +548,7 @@ def test_warning_handling(self):
         self.assertContains(
             reply,
             "The existing user would be overwritten with the following data:<br />"
-            " -  None None, lucilia.manilium@institution.example.com (existing)<br />"
+            f" -  None None, lucilia.manilium@institution.example.com (existing) [{user_edit_link(user.pk)}]<br />"
             " -  Lucilia Manilium, lucilia.manilium@institution.example.com (new)",
         )
 
@@ -1028,7 +1029,7 @@ def test_warning_handling(self):
         """
         Tests whether warnings given from the importer are displayed
         """
-        baker.make(UserProfile, email="lucilia.manilium@institution.example.com")
+        user = baker.make(UserProfile, email="lucilia.manilium@institution.example.com")
 
         page = self.app.get(self.url, user=self.manager)
 
@@ -1042,7 +1043,7 @@ def test_warning_handling(self):
         self.assertContains(
             reply,
             "The existing user would be overwritten with the following data:<br />"
-            " -  None None, lucilia.manilium@institution.example.com (existing)<br />"
+            f" -  None None, lucilia.manilium@institution.example.com (existing) [{user_edit_link(user.pk)}]<br />"
             " -  Lucilia Manilium, lucilia.manilium@institution.example.com (new)",
         )
 
@@ -2228,7 +2229,7 @@ def test_import_participants_warning_handling(self):
         """
         Tests whether warnings given from the importer are displayed
         """
-        baker.make(UserProfile, email="lucilia.manilium@institution.example.com")
+        user = baker.make(UserProfile, email="lucilia.manilium@institution.example.com")
 
         page = self.app.get(self.url, user=self.manager)
 
@@ -2239,7 +2240,7 @@ def test_import_participants_warning_handling(self):
         self.assertContains(
             reply,
             "The existing user would be overwritten with the following data:<br />"
-            " -  None None, lucilia.manilium@institution.example.com (existing)<br />"
+            f" -  None None, lucilia.manilium@institution.example.com (existing) [{user_edit_link(user.pk)}]<br />"
             " -  Lucilia Manilium, lucilia.manilium@institution.example.com (new)",
         )
 
@@ -2262,7 +2263,7 @@ def test_import_contributors_warning_handling(self):
         """
         Tests whether warnings given from the importer are displayed
         """
-        baker.make(UserProfile, email="lucilia.manilium@institution.example.com")
+        user = baker.make(UserProfile, email="lucilia.manilium@institution.example.com")
 
         page = self.app.get(self.url, user=self.manager)
 
@@ -2273,7 +2274,7 @@ def test_import_contributors_warning_handling(self):
         self.assertContains(
             reply,
             "The existing user would be overwritten with the following data:<br />"
-            " -  None None, lucilia.manilium@institution.example.com (existing)<br />"
+            f" -  None None, lucilia.manilium@institution.example.com (existing) [{user_edit_link(user.pk)}]<br />"
             " -  Lucilia Manilium, lucilia.manilium@institution.example.com (new)",
         )