1+ from collections .abc import Callable
12from functools import update_wrapper
3+ from typing import Any
24
35from django .conf import settings
46from django .contrib import admin
5- from django .urls import path , reverse_lazy
7+ from django .contrib .auth import REDIRECT_FIELD_NAME
8+ from django .contrib .auth .decorators import login_not_required
9+ from django .http import HttpRequest , HttpResponse , HttpResponseRedirect
10+ from django .urls import path , reverse_lazy , URLPattern , URLResolver , reverse
11+ from django .utils .decorators import method_decorator
12+ from django .utils .translation import gettext_lazy as _
13+ from django .views .decorators .cache import never_cache
14+ from django .views .decorators .csrf import csrf_protect
615from django .views .generic import TemplateView
716
817from django_smartbase_admin .engine .admin_entrypoint_view import SBAdminEntrypointView
@@ -20,16 +29,18 @@ class SBAdminSite(admin.AdminSite):
2029 "sb_admin/authentification/password_change_done.html"
2130 )
2231
23- def initialize_admin_view (self , view_function , request , ** kwargs ):
32+ def initialize_admin_view (
33+ self , view_func : Callable [..., HttpResponse ], request : HttpRequest , ** kwargs
34+ ) -> None :
2435 request .current_app = "sb_admin"
2536 selected_view = None
2637 try :
27- selected_view = view_function .__self__
38+ selected_view = view_func .__self__
2839 from django_smartbase_admin .admin .admin_base import SBAdminBaseView
2940
3041 if not isinstance (selected_view , SBAdminBaseView ):
3142 selected_view = None
32- except :
43+ except Exception :
3344 pass
3445 request .sbadmin_selected_view = selected_view
3546 kwargs ["view" ] = selected_view .get_id () if selected_view else None
@@ -41,34 +52,46 @@ def initialize_admin_view(self, view_function, request, **kwargs):
4152 request , request_data = request_data , ** kwargs
4253 )
4354
44- def admin_view_response_wrapper (self , response , request , * args , ** kwargs ):
55+ def admin_view_response_wrapper (
56+ self , response : HttpResponse , request : HttpRequest , * args , ** kwargs
57+ ) -> HttpResponse :
4558 from django_smartbase_admin .admin .admin_base import SBAdminThirdParty
4659
4760 if isinstance (request .sbadmin_selected_view , SBAdminThirdParty ):
4861 response = SBAdminViewService .replace_legacy_admin_access_in_response (
4962 response
5063 )
51-
5264 return response
5365
54- def admin_view (self , view_function , cacheable = False ):
55- def inner (request , * args , ** kwargs ):
56- self .initialize_admin_view (view_function , request , ** kwargs )
57- return self .admin_view_response_wrapper (
58- view_function (request , * args , ** kwargs ), request , * args , ** kwargs
59- )
60-
61- return super (SBAdminSite , self ).admin_view (
62- update_wrapper (inner , view_function ), cacheable
63- )
64-
65- def each_context (self , request ):
66+ def admin_view (
67+ self ,
68+ view_func : Callable [..., HttpResponse ],
69+ cacheable : bool = False ,
70+ * ,
71+ public : bool = False
72+ ) -> Callable [[HttpRequest , ...], HttpResponse ]:
73+ def inner (request : HttpRequest , * args , ** kwargs ):
74+ self .initialize_admin_view (view_func , request , ** kwargs )
75+ response = view_func (request , * args , ** kwargs )
76+ return self .admin_view_response_wrapper (response , request , * args , ** kwargs )
77+
78+ if not public :
79+ return super ().admin_view (update_wrapper (inner , view_func ), cacheable )
80+ # standard Django admin behaviour, expect it skips staff/permission checks
81+ if not cacheable :
82+ inner = never_cache (inner )
83+ if not getattr (view_func , "csrf_exempt" , False ):
84+ inner = csrf_protect (inner )
85+ inner = login_not_required (inner )
86+ return update_wrapper (inner , view_func )
87+
88+ def each_context (self , request : HttpRequest ) -> dict [str , Any ]:
6689 try :
6790 return request .sbadmin_selected_view .get_global_context (request )
68- except :
91+ except Exception :
6992 return {}
7093
71- def get_urls (self ):
94+ def get_urls (self ) -> list [ URLPattern | URLResolver ] :
7295 from django .contrib .auth .views import (
7396 PasswordResetView ,
7497 PasswordResetDoneView ,
@@ -80,6 +103,8 @@ def get_urls(self):
80103 from django_smartbase_admin .views .user_config_view import ColorSchemeView
81104
82105 urls = [
106+ path ("login/" , self .admin_view (self .login , public = True ), name = "login" ),
107+ path ("logout/" , self .admin_view (self .logout ), name = "logout" ),
83108 path (
84109 "password_change/" ,
85110 self .admin_view (
@@ -173,5 +198,46 @@ def get_urls(self):
173198 urls .extend (super ().get_urls ())
174199 return urls
175200
201+ @method_decorator (never_cache )
202+ @login_not_required
203+ def login (self , request : HttpRequest , extra_context : dict [str , Any ] | None = None ):
204+ """
205+ Same as Django's built-in AdminSite.login view, except it allows the
206+ login view class to be overridden via configuration.
207+ """
208+ if request .method == "GET" and self .has_permission (request ):
209+ # Already logged-in, redirect to admin index
210+ index_path = reverse ("admin:index" , current_app = self .name )
211+ return HttpResponseRedirect (index_path )
212+
213+ # Since this module gets imported in the application's root package,
214+ # it cannot import models from other applications at the module level,
215+ # and django.contrib.admin.forms eventually imports User.
216+ from django .contrib .admin .forms import AdminAuthenticationForm
217+
218+ context = {
219+ ** self .each_context (request ),
220+ "title" : _ ("Log in" ),
221+ "subtitle" : None ,
222+ "app_path" : request .get_full_path (),
223+ "username" : request .user .get_username (),
224+ }
225+ if (
226+ REDIRECT_FIELD_NAME not in request .GET
227+ and REDIRECT_FIELD_NAME not in request .POST
228+ ):
229+ context [REDIRECT_FIELD_NAME ] = reverse ("admin:index" , current_app = self .name )
230+ context .update (extra_context or {})
231+
232+ defaults = {
233+ "extra_context" : context ,
234+ "authentication_form" : self .login_form or AdminAuthenticationForm ,
235+ "template_name" : self .login_template or "admin/login.html" ,
236+ }
237+ request .current_app = self .name
238+ return request .request_data .configuration .login_view_class .as_view (** defaults )(
239+ request
240+ )
241+
176242
177243sb_admin_site = SBAdminSite (name = "sb_admin" )
0 commit comments