Repository: django-oscar/django-oscar

Issue:
Dashboard login doesnt control next page
### Issue Summary

Navigating to `/oscar/dashboard/` works fine - it sends the user to `/oscar/dashboard/login/?next=/oscar/dashboard/` , which directs them to the correct spot.

However if the user somehow navigates directly to `/oscar/dashboard/login`, upon successful login the user will be redirected to default `LOGIN_REDIRECT_URL`, which is typically *not* the dashboard.  Im not sure how, but one of our partners was doing this, complaining it did work, and we traced it back to that trailing 'login' being in their bookmark.

I suspect this is fixable by adding a default next in the `LoginView`, but it might require subclassing it to get a bit more control.  [link to view creation code](https://github.com/django-oscar/django-oscar/blob/a75d6ee/src/oscar/apps/dashboard/apps.py#L55)

More confusing, and maybe not so easy to fix, if a logged out partner happens to navigate to a restricted dashboard page, such as `/oscar/dashboard/partners/` , they are redirected to `/oscar/dashboard/login/?next=/oscar/dashboard/partners/` , and if they log in as a partner with limited dashboard access, they are taken to `/oscar/dashboard/partners/` which is a 403 , *and* that 403 page doesnt look like the dashboard.

The partner was successfully logged in, but the UI makes it seem something bad has happened.  This happened at an on-site demo with a partner, confusing everyone, because they had re-used a url their webbrowser offered them, from when a site staff had been accessing the full dashboard.

Given this wasnt normal usage, it may not be worth fiddling with the auth form or dashboard view authentication to improve this.  The current approach is completely understandable, and probably quite a lot of effort to make it more intuitive for non-technical users.

What might be useful is to provide a custom `handler403` (and others) which detects if the exception occurred on the shop vs dashboard, and render the error differently so that dashboard auth errors show the user is logged in, but doesnt have access to that part of the dashboard, or even just show the usual exception / error in a dashboard skin.
https://docs.djangoproject.com/en/3.0/ref/urls/#django.conf.urls.handler403
It looks like implementers would need to manually apply that in their URLConf, but maybe there is some other wizbangery that can make it magically active, ideally only on the dashboard.



Discussion:
I see there are error handlers in https://github.com/django-oscar/django-oscar/blob/0cc99f3bc337d45dfb0341a8ec0345f6ed81f083/src/oscar/views/__init__.py

I couldnt find any docs for those, but they are in the sandbox example which the docs recommend studying.

It might also be useful to have a check in `checks.py` to inform implementers that it should be added to their urlconf.  I notice there is no `checks.py` in Oscar.  Has their been a decision that `checks.py` is not the right path.

There is probably enough context given to those handlers to allow them to detect "dashboard" and present a different UI.


Resolving patch:
diff --git a/src/oscar/apps/dashboard/apps.py b/src/oscar/apps/dashboard/apps.py
--- a/src/oscar/apps/dashboard/apps.py
+++ b/src/oscar/apps/dashboard/apps.py
@@ -18,6 +18,7 @@ class DashboardConfig(OscarDashboardConfig):
 
     def ready(self):
         self.index_view = get_class('dashboard.views', 'IndexView')
+        self.login_view = get_class('dashboard.views', 'LoginView')
 
         self.catalogue_app = apps.get_app_config('catalogue_dashboard')
         self.reports_app = apps.get_app_config('reports_dashboard')
@@ -34,7 +35,6 @@ def ready(self):
 
     def get_urls(self):
         from django.contrib.auth import views as auth_views
-        from django.contrib.auth.forms import AuthenticationForm
 
         urls = [
             path('', self.index_view.as_view(), name='index'),
@@ -50,12 +50,7 @@ def get_urls(self):
             path('vouchers/', include(self.vouchers_app.urls[0])),
             path('comms/', include(self.comms_app.urls[0])),
             path('shipping/', include(self.shipping_app.urls[0])),
-
-            path(
-                'login/',
-                auth_views.LoginView.as_view(template_name='oscar/dashboard/login.html',
-                                             authentication_form=AuthenticationForm),
-                name='login'),
+            path('login/', self.login_view.as_view(), name='login'),
             path('logout/', auth_views.LogoutView.as_view(next_page='/'), name='logout'),
         ]
         return self.post_process_urls(urls)
diff --git a/src/oscar/apps/dashboard/views.py b/src/oscar/apps/dashboard/views.py
--- a/src/oscar/apps/dashboard/views.py
+++ b/src/oscar/apps/dashboard/views.py
@@ -4,8 +4,11 @@
 from decimal import Decimal as D
 
 from django.contrib import messages
+from django.contrib.auth import views as auth_views
+from django.contrib.auth.forms import AuthenticationForm
 from django.db.models import Avg, Count, Sum
 from django.template.response import TemplateResponse
+from django.urls import reverse_lazy
 from django.utils.timezone import now
 from django.views.generic import TemplateView
 
@@ -308,3 +311,13 @@ def delete(self, request, *args, **kwargs):
             )
         else:
             return response
+
+
+class LoginView(auth_views.LoginView):
+    template_name = 'oscar/dashboard/login.html'
+    authentication_form = AuthenticationForm
+    login_redirect_url = reverse_lazy('dashboard:index')
+
+    def get_success_url(self):
+        url = self.get_redirect_url()
+        return url or self.login_redirect_url


Test patch:
diff --git a/tests/functional/dashboard/test_dashboard.py b/tests/functional/dashboard/test_dashboard.py
--- a/tests/functional/dashboard/test_dashboard.py
+++ b/tests/functional/dashboard/test_dashboard.py
@@ -84,6 +84,14 @@ def test_has_stats_vars_in_context(self):
         for key in GENERIC_STATS_KEYS + STAFF_STATS_KEYS:
             self.assertInContext(response, key)
 
+    def test_login_redirects_to_dashboard_index(self):
+        page = self.get(reverse('dashboard:login'))
+        form = page.forms['dashboard_login_form']
+        form['username'] = self.email
+        form['password'] = self.password
+        response = form.submit('login_submit')
+        self.assertRedirectsTo(response, 'dashboard:index')
+
 
 class TestDashboardIndexForPartnerUser(WebTestCase):
     permissions = ['partner.dashboard_access']