Skip to content

Commit 989daa6

Browse files
ViktorBojdaViktor Bojda
andauthored
feat: add optional login class override via sbadmin config (#86)
* feat: add param public to admin_view func to skip perm check and add typehints to site.py * feat: allow overriding login view class via sbadmin config * fix: make cacheable in admin_view positional arg * feat: add notifications to login base template * fix: add !important to left-0 class * triv: bump version --------- Co-authored-by: Viktor Bojda <[email protected]>
1 parent c2a88b5 commit 989daa6

File tree

5 files changed

+98
-24
lines changed

5 files changed

+98
-24
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[tool.poetry]
22
name = "django-smartbase-admin"
3-
version = "1.0.29"
3+
version = "1.0.30"
44
description = ""
55
authors = ["SmartBase <[email protected]>"]
66
readme = "README.md"

src/django_smartbase_admin/admin/site.py

Lines changed: 86 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
1+
from collections.abc import Callable
12
from functools import update_wrapper
3+
from typing import Any
24

35
from django.conf import settings
46
from 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
615
from django.views.generic import TemplateView
716

817
from 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

177243
sb_admin_site = SBAdminSite(name="sb_admin")

src/django_smartbase_admin/engine/configuration.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from django.contrib.auth import get_permission_codename
2+
from django.contrib.auth.views import LoginView
23
from django.db.models import Q
34

45
from django_smartbase_admin.admin.site import sb_admin_site
@@ -41,6 +42,7 @@ class SBAdminRoleConfiguration(metaclass=Singleton):
4142
global_filter_form = None
4243
filters_version = FilterVersions.FILTERS_VERSION_1
4344
default_color_scheme = ColorScheme.AUTO
45+
login_view_class = LoginView
4446

4547
def __init__(
4648
self,
@@ -50,6 +52,7 @@ def __init__(
5052
global_filter_form=None,
5153
filters_version=None,
5254
default_color_scheme=None,
55+
login_view_class=None,
5356
) -> None:
5457
super().__init__()
5558
self.default_view = default_view or self.default_view or []
@@ -60,6 +63,7 @@ def __init__(
6063
self.autocomplete_map = {}
6164
self.filters_version = filters_version or self.filters_version
6265
self.default_color_scheme = default_color_scheme or self.default_color_scheme
66+
self.login_view_class = login_view_class or self.login_view_class
6367

6468
def init_registered_views(self):
6569
registered_views = []

src/django_smartbase_admin/templates/sb_admin/authentification/login_base.html

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,12 @@
22
{% load i18n static %}
33

44
{% block main_wrapper %}
5+
{% include "sb_admin/includes/notifications.html" with classes="top-16 xl:!left-0" %}
56
<div class="flex items-center max-h-screen">
6-
<img loading="lazy" src="{% static "sb_admin/images/logo.svg" %}" alt="Logo" class="max-h-40 absolute md:left-40 md:top-40 left-24 top-24 dark:hidden">
7-
<img loading="lazy" src="{% static "sb_admin/images/logo_light.svg" %}" alt="Logo" class="max-h-40 absolute md:left-40 md:top-40 left-24 top-24 hidden dark:block">
7+
<img loading="lazy" src="{% static "sb_admin/images/logo.svg" %}" alt="Logo"
8+
class="max-h-40 absolute md:left-40 md:top-40 left-24 top-24 dark:hidden">
9+
<img loading="lazy" src="{% static "sb_admin/images/logo_light.svg" %}" alt="Logo"
10+
class="max-h-40 absolute md:left-40 md:top-40 left-24 top-24 hidden dark:block">
811
<div class="md:max-w-640 w-full flex justify-center items-center h-screen">
912
<div class="max-w-370 w-full mx-24">
1013
{% block content %}{% endblock %}

src/django_smartbase_admin/templates/sb_admin/includes/notifications.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
<div id="notification-messages" hx-swap-oob="beforeend" class="messagelist z-1000 fixed right-0 left-0 xl:left-260 mx-24">
1+
<div id="notification-messages" hx-swap-oob="beforeend"
2+
class="messagelist z-1000 fixed right-0 left-0 xl:left-260 mx-24{% if classes %} {{ classes }}{% endif %}">
23
{% for message in messages %}
34
{% if message.level == DEFAULT_MESSAGE_LEVELS.SUCCESS %}
45
{% include 'sb_admin/partials/messages/alert_success.html' with text=message %}

0 commit comments

Comments
 (0)