Repository: carpentries/amy
Issue: `action_required_policy` doesn't capture `?next` parameter from log-in page

When user logs in to a specific page, and `action_required_policy` captures their request, it doesn't redirect to the page specified by `next` query parameter.

Changed files:
amy/workshops/views.py, amy/workshops/action_required.py, amy/workshops/tests/test_action_required.py

diff --git a/amy/workshops/action_required.py b/amy/workshops/action_required.py
index e3506fd9..80b352cd 100644
--- a/amy/workshops/action_required.py
+++ b/amy/workshops/action_required.py
@@ -1,5 +1,7 @@
 from django.shortcuts import redirect
 from django.urls import reverse
+from django.utils.http import urlencode
+
 
 class PrivacyPolicy:
     def __init__(self, get_response):
@@ -13,15 +15,27 @@ class PrivacyPolicy:
             url
         ]
 
+        if 'next' in request.GET:
+            # prepare `?next` URL if it's already present (e.g. user refreshes
+            # the `action_required_privacy` page)
+            next_param = request.GET['next']
+        else:
+            # grab requested page path to use in `?next` query string
+            next_param = request.path
+
+        # only add `?next` if it's outside the scope of allowed URLs
+        if next_param not in allowed_urls:
+            url += '?{}'.format(urlencode({'next': next_param}))
+
         # redirect only users who didn't agree on the privacy policy
         # also don't redirect if the requested page is the page we want to
         # redirect to
         if (
-                request.path not in allowed_urls and
-                not request.user.is_anonymous and
-                hasattr(request.user, 'data_privacy_agreement') and
-                not request.user.data_privacy_agreement
-            ):
-                return redirect(url)
+            request.path not in allowed_urls and
+            not request.user.is_anonymous and
+            hasattr(request.user, 'data_privacy_agreement') and
+            not request.user.data_privacy_agreement
+        ):
+            return redirect(url)
         else:
             return self.get_response(request)
diff --git a/amy/workshops/tests/test_action_required.py b/amy/workshops/tests/test_action_required.py
index e23c4d48..b4f88b86 100644
--- a/amy/workshops/tests/test_action_required.py
+++ b/amy/workshops/tests/test_action_required.py
@@ -1,4 +1,5 @@
 from django.urls import reverse
+from django.utils.http import urlencode
 
 from workshops.tests.base import TestBase
 from workshops.forms import ActionRequiredPrivacyForm
@@ -35,9 +36,9 @@ class TestActionRequiredPrivacy(TestBase):
         self.neville.data_privacy_agreement = True
         self.neville.save()
 
-        # form redirects
+        # form throws 404
         rv = self.client.get(url)
-        self.assertEqual(rv.status_code, 302)
+        self.assertEqual(rv.status_code, 404)
 
     def test_agreement_submit(self):
         "Make sure the form passes only when `data_agreement_policy` is set."
@@ -117,7 +118,7 @@ class TestActionRequiredPrivacyMiddleware(TestBase):
             rv = self.client.get(url)
             # redirects to the form
             self.assertEqual(rv.status_code, 302)
-            self.assertEqual(rv['Location'], form_url)
+            self.assertTrue(rv['Location'].startswith(form_url))
 
     def test_no_more_redirects_after_agreement(self):
         """Ensure user is no longer forcefully redirected to accept the
@@ -132,7 +133,7 @@ class TestActionRequiredPrivacyMiddleware(TestBase):
         # we can't get to the url because we're redirected to the form
         rv = self.client.get(url)
         self.assertEqual(rv.status_code, 302)
-        self.assertEqual(rv['Location'], form_url)
+        self.assertTrue(rv['Location'].startswith(form_url))
 
         # agree on the privacy policy
         self.neville.data_privacy_agreement = True
@@ -156,3 +157,20 @@ class TestActionRequiredPrivacyMiddleware(TestBase):
             self.assertIn(rv.status_code, [200, 302])
             if 'Location' in rv:
                 self.assertNotEqual(rv['Location'], form_url)
+
+    def test_next_param(self):
+        """Ensure a non-dispatch URL is reachable through `?next` query
+        string."""
+
+        url = reverse('autoupdate_profile')
+        form_url = reverse('action_required_privacy')
+        form_url += '?{}'.format(urlencode({'next': url}))
+
+        # ensure we're logged in
+        self.client.force_login(self.neville)
+        self.assertEqual(self.neville.data_privacy_agreement, False)
+
+        # submit form
+        rv = self.client.post(form_url, data=dict(data_privacy_agreement=True))
+        self.assertEqual(rv.status_code, 302)
+        self.assertEqual(rv['Location'], url)
diff --git a/amy/workshops/views.py b/amy/workshops/views.py
index 60b482b0..61bf9287 100644
--- a/amy/workshops/views.py
+++ b/amy/workshops/views.py
@@ -1797,7 +1797,7 @@ def action_required_privacy(request):
 
     # disable the view for users who already agreed
     if person.data_privacy_agreement:
-        return redirect(reverse('dispatch'))
+        raise Http404('This view is disabled.')
 
     form = ActionRequiredPrivacyForm(instance=person)
 
@@ -1808,7 +1808,10 @@ def action_required_privacy(request):
             person = form.save()
             messages.success(request, 'Agreement successfully saved.')
 
-            return redirect(reverse('dispatch'))
+            if 'next' in request.GET:
+                return redirect(request.GET['next'])
+            else:
+                return redirect(reverse('dispatch'))
         else:
             messages.error(request, 'Fix errors below.')