Repository: e-valuation/EvaP

Issue:
Resend old login key if unused
When a user requests a new login key or receives one because of a course transaction, a new key should only be generated, if the previous one was not already used. An email should still be sent, but include the same key as before.

There should also be a test testing that behavior.


Discussion:
Assign please

Resolving patch:
diff --git a/evap/evaluation/auth.py b/evap/evaluation/auth.py
--- a/evap/evaluation/auth.py
+++ b/evap/evaluation/auth.py
@@ -68,14 +68,14 @@ def process_request(self, request):
             request.user = user
             auth.login(request, user)
             messages.success(request, _("Logged in as %s.") % user.full_name)
-            # Invalidate the login key (set to yesterday).
+            # Invalidate the login key, but keep it stored so we can later identify the user that is trying to login and send a new link
             user.login_key_valid_until = date.today() - timedelta(1)
             user.save()
         elif user:
             # A user exists, but the login key is not valid anymore. Send the user a new one.
-            user.generate_login_key()
+            user.ensure_valid_login_key()
             EmailTemplate.send_login_url_to_user(user)
-            messages.warning(request, _("The login URL was already used. We sent you a new one to your email address."))
+            messages.warning(request, _("The login URL is not valid anymore. We sent you a new one to your email address."))
         else:
             messages.warning(request, _("Invalid login URL. Please request a new one below."))
 
diff --git a/evap/evaluation/models.py b/evap/evaluation/models.py
--- a/evap/evaluation/models.py
+++ b/evap/evaluation/models.py
@@ -1004,7 +1004,10 @@ def email_needs_login_key(cls, email):
     def needs_login_key(self):
         return UserProfile.email_needs_login_key(self.email)
 
-    def generate_login_key(self):
+    def ensure_valid_login_key(self):
+        if self.login_key and self.login_key_valid_until > date.today():
+            return
+
         while True:
             key = random.randrange(0, UserProfile.MAX_LOGIN_KEY)
             if not UserProfile.objects.filter(login_key=key).exists():
@@ -1138,7 +1141,7 @@ def send_to_user(cls, user, template, subject_params, body_params, use_cc, reque
         send_separate_login_url = False
         body_params['login_url'] = ""
         if user.needs_login_key:
-            user.generate_login_key()
+            user.ensure_valid_login_key()
             if not cc_addresses:
                 body_params['login_url'] = user.login_url
             else:
diff --git a/evap/evaluation/views.py b/evap/evaluation/views.py
--- a/evap/evaluation/views.py
+++ b/evap/evaluation/views.py
@@ -36,7 +36,7 @@ def index(request):
         if new_key_form.is_valid():
             # user wants a new login key
             profile = new_key_form.get_user()
-            profile.generate_login_key()
+            profile.ensure_valid_login_key()
             profile.save()
 
             EmailTemplate.send_login_url_to_user(new_key_form.get_user())


Test patch:
diff --git a/evap/evaluation/tests/test_auth.py b/evap/evaluation/tests/test_auth.py
--- a/evap/evaluation/tests/test_auth.py
+++ b/evap/evaluation/tests/test_auth.py
@@ -8,13 +8,14 @@
 
 
 class LoginTests(WebTest):
+    csrf_checks = False
 
     @classmethod
     def setUpTestData(cls):
         cls.external_user = mommy.make(UserProfile, email="extern@extern.com")
-        cls.external_user.generate_login_key()
+        cls.external_user.ensure_valid_login_key()
         cls.inactive_external_user = mommy.make(UserProfile, email="inactive@extern.com", is_active=False)
-        cls.inactive_external_user.generate_login_key()
+        cls.inactive_external_user.ensure_valid_login_key()
 
     def test_login_url_works(self):
         self.assertRedirects(self.app.get(reverse("results:index")), "/?next=/results/")
@@ -28,7 +29,7 @@ def test_login_key_valid_only_once(self):
         page = self.app.get(reverse("django-auth-logout")).follow()
         self.assertContains(page, 'Not logged in')
         page = self.app.get(reverse("results:index") + "?loginkey=%s" % self.external_user.login_key).follow()
-        self.assertContains(page, 'The login URL was already used')
+        self.assertContains(page, 'The login URL is not valid anymore.')
         self.assertEqual(len(mail.outbox), 1)  # a new login key was sent
         self.external_user.refresh_from_db()
         page = self.app.get(reverse("results:index") + "?loginkey=%s" % self.external_user.login_key)
@@ -38,3 +39,13 @@ def test_inactive_external_users_can_not_login(self):
         page = self.app.get(reverse("results:index") + "?loginkey=%s" % self.inactive_external_user.login_key).follow()
         self.assertContains(page, "Inactive users are not allowed to login")
         self.assertNotContains(page, "Logged in")
+
+    def test_login_key_resend_if_still_valid(self):
+        old_key = self.external_user.login_key
+        page = self.app.post("/", params={"submit_type": "new_key", "email": self.external_user.email}).follow()
+        new_key = UserProfile.objects.get(id=self.external_user.id).login_key
+
+        self.assertEqual(old_key, new_key)
+        self.assertEqual(len(mail.outbox), 1)  # a login key was sent
+        self.assertContains(page, "We sent you an email with a one-time login URL. Please check your inbox.")
+
diff --git a/evap/evaluation/tests/test_models.py b/evap/evaluation/tests/test_models.py
--- a/evap/evaluation/tests/test_models.py
+++ b/evap/evaluation/tests/test_models.py
@@ -310,7 +310,7 @@ class TestLoginUrlEmail(TestCase):
     def setUpTestData(cls):
         cls.other_user = mommy.make(UserProfile, email="other@extern.com")
         cls.user = mommy.make(UserProfile, email="extern@extern.com")
-        cls.user.generate_login_key()
+        cls.user.ensure_valid_login_key()
 
         cls.course = mommy.make(Course)
         mommy.make(Contribution, course=cls.course, contributor=cls.user, responsible=True, can_edit=True, comment_visibility=Contribution.ALL_COMMENTS)
diff --git a/evap/evaluation/tests/test_tools.py b/evap/evaluation/tests/test_tools.py
--- a/evap/evaluation/tests/test_tools.py
+++ b/evap/evaluation/tests/test_tools.py
@@ -20,7 +20,7 @@ def test_signal_sets_language_if_none(self):
         translation.activate('de')
 
         user = mommy.make(UserProfile, language=None)
-        user.generate_login_key()
+        user.ensure_valid_login_key()
 
         set_or_get_language(None, user, None)
 
@@ -33,7 +33,7 @@ def test_signal_doesnt_set_language(self):
         """
         translation.activate('en')
         user = mommy.make(UserProfile, language='de')
-        user.generate_login_key()
+        user.ensure_valid_login_key()
 
         self.app.get(reverse("results:index") + "?loginkey=%s" % user.login_key)