Compare commits

..

14 Commits

Author SHA1 Message Date
Ales (Shagi) Zabala Alava 312987410b chore: Bump version 0.8.13 2023-12-19 10:57:43 +01:00
Leo Hakim 6da05dd02e fix: set html email template 2023-12-19 10:55:42 +01:00
Ales (Shagi) Zabala Alava 69316f7abd chore: Bump version 0.8.12 2023-12-14 17:55:36 +01:00
Ales (Shagi) Zabala Alava 88ff5de282 chore: Bump version 0.8.11 2023-12-14 17:54:11 +01:00
Leo Hakim 49154ab3a5 feat: Add reset password (#5)
Co-authored-by: Leonardo Hakim <leohakim@gmail.com>
Reviewed-on: #5
Co-authored-by: Leo Hakim <leo@noreply.git.negromate.rocks>
Co-committed-by: Leo Hakim <leo@noreply.git.negromate.rocks>
2023-12-14 17:53:08 +01:00
Leo Hakim 9e77205fed
feat: Add preview widget in Image Field Form 2023-11-29 17:08:39 +01:00
Ales (Shagi) Zabala Alava fce719aca1 chore: Bump version 0.8.10 2023-11-15 10:22:08 +01:00
Ales (Shagi) Zabala Alava af773a0afd feat: new templatetag to check if user has role 2023-11-15 10:19:54 +01:00
Ales (Shagi) Zabala Alava 4d82c76fbd fix: fix gas urls, bump version 0.8.9 2023-10-26 16:13:00 +02:00
Ales (Shagi) Zabala Alava 6f2895253b Bump version 0.8.8 2023-10-26 15:43:05 +02:00
Ales (Shagi) Zabala Alava de9e0f488a chore: support Django 4.2 2023-10-26 15:40:59 +02:00
Ales (Shagi) Zabala Alava f881ada65c feat: use DeleteView.form_valid instead of DeleteView.delete 2023-10-26 15:39:20 +02:00
Ales (Shagi) Zabala Alava d62b8ab252 chore: lintin 2023-10-26 15:38:44 +02:00
Ales (Shagi) Zabala Alava b0eaa66e37 feat: add no-cache headers to login redirects 2023-10-26 13:27:26 +02:00
22 changed files with 334 additions and 89 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
/django_gas.egg-info /django_gas.egg-info
/.coverage /.coverage
/htmlcov /htmlcov
*.pyc

View File

@ -1,6 +1,38 @@
Changelog Changelog
========= =========
0.8.13
------
* Fix: set html email template
0.8.12
------
* Add reset password (thanks Leo!)
0.8.11
------
* Add preview widget in Image Field Form (thanks Leo!)
0.8.10
-----
* New templatetag to check if user has role
0.8.9
-----
* Bugfix
0.8.8
-----
* Support Django 4.2
* Use DeleteView.form_valid instead of DeleteView.delete
* Add no-cache headers to login redirects
0.8.7 0.8.7
----- -----

View File

@ -2,3 +2,4 @@ Collaborators
============= =============
* Shakarina * Shakarina
* Leo Hakim

View File

@ -8,5 +8,9 @@ urlpatterns = [
path('login/', views.GASLoginView.as_view(), name='login'), path('login/', views.GASLoginView.as_view(), name='login'),
path('logout/', logout_then_login, {'login_url': 'gas:login'}, name='logout'), path('logout/', logout_then_login, {'login_url': 'gas:login'}, name='logout'),
path('change-password/', views.GASPasswordChangeView.as_view(), name='change_password'), path('change-password/', views.GASPasswordChangeView.as_view(), name='change_password'),
path('reset-password-confirm/<uidb64>/<token>/', views.GASPasswordResetConfirmView.as_view(), name='password_reset_confirm'),
path('reset-password-confirm/done/', views.GASPasswordResetCompleteView.as_view(), name='password_reset_complete'),
path('reset-password/done/', views.GASPasswordResetDoneView.as_view(), name='password_reset_done'),
path('reset-password/', views.GASPasswordResetView.as_view(), name='reset_password'),
path('', views.Index.as_view(), name='index'), path('', views.Index.as_view(), name='index'),
] ]

View File

@ -1,45 +1,72 @@
from django.contrib.auth.views import LoginView, PasswordChangeView from django.contrib.auth.views import (
LoginView,
PasswordChangeView,
PasswordResetConfirmView,
PasswordResetDoneView,
PasswordResetView,
)
from django.shortcuts import resolve_url from django.shortcuts import resolve_url
from django.urls import reverse_lazy from django.urls import reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic import TemplateView from django.views.generic import TemplateView
from gas import gas_settings
from gas.views import GASMixin from gas.views import GASMixin
from gas import gas_settings
class GASPasswordChangeView(GASMixin, PasswordChangeView):
template_name = "gas/base_form.html"
success_url = reverse_lazy("gas:index")
continue_url = reverse_lazy("gas:change_password")
title = _("Change your password")
success_message = _("Password changed.")
class GASLoginView(LoginView): class Index(GASMixin, TemplateView):
main_menu = "index"
template_name = "gas/index.html"
roles = ("staff",)
class GASContextMixin:
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
css = gas_settings.MEDIA["css"]
javascript = gas_settings.MEDIA["js"]
if gas_settings.EXTRA_MEDIA:
css = css + gas_settings.EXTRA_MEDIA.get("css", [])
javascript = javascript + gas_settings.EXTRA_MEDIA.get("js", [])
ctx.update(
{
"logo_static_url": gas_settings.LOGO,
"css": css,
"js": javascript,
}
)
return ctx
class GASLoginView(GASContextMixin, LoginView):
template_name = "gas/login.html" template_name = "gas/login.html"
def get_success_url(self): def get_success_url(self):
url = self.get_redirect_url() url = self.get_redirect_url()
return url or resolve_url('gas:index') return url or resolve_url("gas:index")
def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs)
css = gas_settings.MEDIA['css']
js = gas_settings.MEDIA['js']
if gas_settings.EXTRA_MEDIA:
css = css + gas_settings.EXTRA_MEDIA.get('css', [])
js = js + gas_settings.EXTRA_MEDIA.get('js', [])
ctx.update({
'logo_static_url': gas_settings.LOGO,
'css': css,
'js': js,
})
return ctx
class GASPasswordChangeView(GASMixin, PasswordChangeView): class GASPasswordResetView(GASContextMixin, PasswordResetView):
template_name = 'gas/base_form.html' template_name = "gas/reset.html"
success_url = reverse_lazy('gas:index') html_email_template_name = "registration/password_reset_email.html"
continue_url = reverse_lazy('gas:change_password') success_url = reverse_lazy("gas:password_reset_done")
title = _('Change your password')
success_message = _('Password changed.')
class Index(GASMixin, TemplateView): class GASPasswordResetDoneView(GASContextMixin, PasswordResetDoneView):
main_menu = 'index' template_name = "gas/reset_done.html"
template_name = "gas/index.html"
roles = ('staff',)
class GASPasswordResetConfirmView(GASContextMixin, PasswordResetConfirmView):
template_name = "gas/reset_confirm.html"
success_url = reverse_lazy("gas:password_reset_complete")
class GASPasswordResetCompleteView(GASContextMixin, TemplateView):
template_name = "gas/reset_complete.html"

View File

@ -7,6 +7,9 @@ urlpatterns = [
path('', views.UserList.as_view(), name="user_list"), path('', views.UserList.as_view(), name="user_list"),
path('create/', views.CreateUser.as_view(), name="user_create"), path('create/', views.CreateUser.as_view(), name="user_create"),
path('<int:pk>/edit/', views.UpdateUser.as_view(), name="user_update"), path('<int:pk>/edit/', views.UpdateUser.as_view(), name="user_update"),
path('<int:pk>/change-password/', views.ChangePasswordUser.as_view(), name="user_change_password"), path(
'<int:pk>/change-password/',
views.ChangePasswordUser.as_view(),
name="user_change_password"),
path('<int:pk>/delete/', views.DeleteUser.as_view(), name="user_delete"), path('<int:pk>/delete/', views.DeleteUser.as_view(), name="user_delete"),
] ]

View File

@ -20,3 +20,5 @@ MEDIA = getattr(settings, 'GAS_MEDIA', {
}) })
EXTRA_MEDIA = getattr(settings, 'GAS_EXTRA_MEDIA', None) EXTRA_MEDIA = getattr(settings, 'GAS_EXTRA_MEDIA', None)
IMAGE_PREVIEW_WIDTH = getattr(settings, 'IMAGE_PREVIEW_WIDTH', 240)

View File

@ -10,7 +10,7 @@ class Entry:
self.icon = icon self.icon = icon
self.url = url self.url = url
self.roles = roles self.roles = roles
self.children = dict() self.children = {}
self.parent = parent self.parent = parent
self.order = order if order is not None else float("inf") self.order = order if order is not None else float("inf")
@ -20,7 +20,7 @@ class Entry:
return super().__lt__(other) return super().__lt__(other)
class GASSite(object): class GASSite:
base_role = 'admins' base_role = 'admins'
def __init__(self): def __init__(self):
@ -42,12 +42,12 @@ class GASSite(object):
exposed when gas is not active. exposed when gas is not active.
""" """
if prefix in self._registry['urls']: if prefix in self._registry['urls']:
raise ImproperlyConfigured("Prefix {0} already in use".format(prefix)) raise ImproperlyConfigured(f"Prefix {prefix} already in use")
self._registry['urls'][prefix] = urls self._registry['urls'][prefix] = urls
def register_menu(self, name, label, url=None, icon=None, roles=None, parent=None, order=None): def register_menu(self, name, label, url=None, icon=None, roles=None, parent=None, order=None):
if name in self._registry['menu']: if name in self._registry['menu']:
raise ImproperlyConfigured("Menu entry '{0}' already registered.".format(name)) raise ImproperlyConfigured(f"Menu entry '{name}' already registered.")
if roles is None: if roles is None:
roles = set() roles = set()
@ -61,8 +61,8 @@ class GASSite(object):
if parent: if parent:
try: try:
parent_entry = self._registry['menu'][parent] parent_entry = self._registry['menu'][parent]
except KeyError: except KeyError as exc:
raise ImproperlyConfigured("Parent {} not registered.".format(parent)) raise ImproperlyConfigured("Parent {parent} not registered.") from exc
parent_entry.children[name] = entry parent_entry.children[name] = entry
self._registry['menu'][name] = entry self._registry['menu'][name] = entry
@ -86,7 +86,7 @@ class GASSite(object):
for prefix, urls in self._registry['urls'].items(): for prefix, urls in self._registry['urls'].items():
if prefix: if prefix:
urlpatterns.append( urlpatterns.append(
re_path(r'^{0}/'.format(prefix), include(urls)), re_path(f'^{prefix}/', include(urls)),
) )
else: else:
urlpatterns.append( urlpatterns.append(

View File

@ -262,3 +262,7 @@ label {
width: 20em; width: 20em;
padding: 1em; padding: 1em;
} }
.preview {
display: block;
}

View File

@ -0,0 +1,14 @@
{% load i18n %}{% autoescape off %}
{% blocktranslate %}You're receiving this email because you requested a password reset for your user account at {{ site_name }}.{% endblocktranslate %}
{% translate "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{ protocol }}://{{ domain }}{% url 'gas:password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% translate 'Your username, in case youve forgotten:' %} {{ user.get_username }}
{% translate "Thanks for using our site!" %}
{% blocktranslate %}The {{ site_name }} team{% endblocktranslate %}
{% endautoescape %}

View File

@ -0,0 +1,20 @@
{% load i18n static form_tags %}<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{{ gas_title }} {% trans "Login" %}</title>
{% for cssfile in css %}
<link href="{% static cssfile %}" rel="stylesheet" type="text/css" />
{% endfor %}
</head>
<body id="login">
<p>{% translate 'Forgotten your password? Enter your email address below, and well email instructions for setting a new one.' %}</p>
<form action="." method="POST">{% csrf_token %}
{% form_errors form %}
{% form_field form.email %}
<input type="hidden" name="next" value="{{ next }}">
<button type="submit">{% trans "Reset my password" %}</button>
</form>
</body>
</html>

View File

@ -0,0 +1,14 @@
{% load i18n static form_tags %}<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{{ gas_title }} {% trans "Login" %}</title>
{% for cssfile in css %}
<link href="{% static cssfile %}" rel="stylesheet" type="text/css" />
{% endfor %}
</head>
<body id="login">
<p>{% translate 'Your password was changed.' %}</p>
</body>
</html>

View File

@ -0,0 +1,26 @@
{% load i18n static form_tags %}<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{{ gas_title }} {% trans "Login" %}</title>
{% for cssfile in css %}
<link href="{% static cssfile %}" rel="stylesheet" type="text/css" />
{% endfor %}
</head>
<body id="login">
{% if validlink %}
<p>{% translate "Please enter your new password twice so we can verify you typed it in correctly." %}</p>
<form action="." method="POST">{% csrf_token %}
<input class="hidden" autocomplete="username" value="{{ form.user.get_username }}">
{% form_errors form %}
{% form_field form.new_password1 %}
{% form_field form.new_password2 %}
<input type="hidden" name="next" value="{{ next }}">
<button type="submit">{% trans "Change my password" %}</button>
</form>
{% else %}
<p>{% translate "The password reset link was invalid, possibly because it has already been used. Please request a new password reset." %}</p>
{% endif %}
</body>
</html>

View File

@ -0,0 +1,17 @@
{% load i18n static form_tags %}<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>{{ gas_title }} {% trans "Login" %}</title>
{% for cssfile in css %}
<link href="{% static cssfile %}" rel="stylesheet" type="text/css" />
{% endfor %}
</head>
<body id="login">
<p>{% translate 'Weve emailed you instructions for setting your password, if an account exists with the email you entered. You should receive them shortly.' %}</p>
<p>{% translate 'If you dont receive an email, please make sure youve entered the address you registered with, and check your spam folder.' %}</p>
</body>
</html>

View File

@ -0,0 +1,5 @@
{% if widget.is_initial %}{{ widget.initial_text }}: <a href="{{ widget.value.url }}"><img src="{{ widget.value.url }}" width="{{ widget.attrs.IMAGE_PREVIEW_WIDTH }}" class="preview" /></a>{% if not widget.required %}
<input type="checkbox" name="{{ widget.checkbox_name }}" id="{{ widget.checkbox_id }}"{% if widget.attrs.disabled %} disabled{% endif %}>
<label for="{{ widget.checkbox_id }}">{{ widget.clear_checkbox_label }}</label>{% endif %}<br>
{{ widget.input_text }}:{% endif %}
<input type="{{ widget.type }}" name="{{ widget.name }}"{% include "django/forms/widgets/attrs.html" %}>

View File

@ -1,7 +1,8 @@
from django import forms from django import forms, template
from django import template
from django.template.loader import render_to_string from django.template.loader import render_to_string
from ..gas_settings import IMAGE_PREVIEW_WIDTH
register = template.Library() register = template.Library()
@ -19,6 +20,10 @@ def base_form_field(field, css=None, container_class='', add_another_url=None, f
elif isinstance(field.field, forms.DateField): elif isinstance(field.field, forms.DateField):
field.field.widget.input_type = 'date' field.field.widget.input_type = 'date'
if isinstance(field.field, forms.ImageField):
field.field.widget.template_name = 'gas/tags/widgets/image_preview.html'
field.field.widget.attrs['IMAGE_PREVIEW_WIDTH'] = IMAGE_PREVIEW_WIDTH
if isinstance(field.field, forms.SplitDateTimeField): if isinstance(field.field, forms.SplitDateTimeField):
field.field.widget.widgets[1].attrs['placeholder'] = '00:00:00' field.field.widget.widgets[1].attrs['placeholder'] = '00:00:00'
else: else:

View File

@ -5,6 +5,7 @@ from django import template
from django.utils.html import mark_safe from django.utils.html import mark_safe
from ..sites import site from ..sites import site
from ..models import UserRole
register = template.Library() register = template.Library()
@ -59,3 +60,10 @@ def pagination(request, page):
@register.filter @register.filter
def to_json(data): def to_json(data):
return mark_safe(json.dumps(data)) return mark_safe(json.dumps(data))
@register.filter
def has_role(user, role):
if user.is_superuser:
return True
return UserRole.objects.filter(role__in=(role, "admins"), user=user).exists()

View File

@ -0,0 +1,60 @@
from django.test import TestCase, Client
from django.urls import reverse
from model_bakery import baker
class GASLoginTestCase(TestCase):
def test_load(self):
client = Client()
response = client.get(reverse("gas:login"))
self.assertEqual(response.status_code, 200)
class IndexTestCase(TestCase):
def test_load(self):
admin_user = baker.make(
"auth.User",
username="admin",
is_superuser=True,
)
client = Client()
response = client.get(reverse("gas:index"))
self.assertEqual(response.status_code, 302)
client.force_login(admin_user)
response = client.get(reverse("gas:index"))
self.assertEqual(response.status_code, 200)
class GASPasswordResetViewTestCase(TestCase):
def test_load(self):
client = Client()
response = client.get(reverse("gas:reset_password"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "gas/reset.html")
class GASPasswordResetDoneViewTestCase(TestCase):
def test_load(self):
client = Client()
response = client.get(reverse("gas:password_reset_done"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "gas/reset_done.html")
class GASPasswordResetConfirmViewTestCase(TestCase):
def test_load(self):
client = Client()
response = client.get(reverse("gas:password_reset_confirm", kwargs={"uidb64": "uidb64", "token": "token"}))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "gas/reset_confirm.html")
class GASPasswordResetCompleteViewTestCase(TestCase):
def test_load(self):
client = Client()
response = client.get(reverse("gas:password_reset_complete"))
self.assertEqual(response.status_code, 200)
self.assertTemplateUsed(response, "gas/reset_complete.html")

View File

@ -6,13 +6,13 @@ from django.utils.functional import Promise
class JSONEncoder(json.JSONEncoder): class JSONEncoder(json.JSONEncoder):
def default(self, obj): def default(self, o):
if isinstance(obj, datetime.datetime): if isinstance(o, datetime.datetime):
return obj.strftime('%Y-%m-%d %H:%M') return o.strftime('%Y-%m-%d %H:%M')
if isinstance(obj, datetime.date): if isinstance(o, datetime.date):
return obj.strftime('%Y-%m-%d') return o.strftime('%Y-%m-%d')
if isinstance(obj, QuerySet): if isinstance(o, QuerySet):
return list(obj) return list(o)
if isinstance(obj, Promise): if isinstance(o, Promise):
return str(obj) return str(o)
return json.JSONEncoder.default(self, obj) return json.JSONEncoder.default(self, o)

View File

@ -1,11 +1,10 @@
import json
from django.contrib import messages from django.contrib import messages
from django.contrib.admin.utils import NestedObjects from django.contrib.admin.utils import NestedObjects
from django.core.exceptions import ImproperlyConfigured from django.core.exceptions import ImproperlyConfigured
from django.db import router from django.db import router
from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseRedirect, HttpResponseBadRequest, JsonResponse
from django.urls import reverse from django.urls import reverse
from django.utils.cache import add_never_cache_headers
from django.utils.html import escape, escapejs from django.utils.html import escape, escapejs
from django.utils.text import capfirst from django.utils.text import capfirst
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -44,30 +43,26 @@ class AjaxCommandsMixin:
""" """
def post(self, request, *args, **kwargs): def post(self, request, *args, **kwargs):
if 'command' in self.request.POST: if 'command' in self.request.POST:
command_processor = getattr(self, 'do_{0}'.format(self.request.POST['command']), None) command = self.request.POST['command']
command_processor = getattr(self, f'do_{command}', None)
if command_processor is not None: if command_processor is not None:
return command_processor() return command_processor()
else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else:
handler = getattr(super(), 'post', self.http_method_not_allowed) handler = getattr(super(), 'post', self.http_method_not_allowed)
return handler(request, *args, **kwargs) return handler(request, *args, **kwargs)
def get(self, request, *args, **kwargs): def get(self, request, *args, **kwargs):
if 'command' in self.request.GET: if 'command' in self.request.GET:
command_processor = getattr(self, 'send_{0}'.format(self.request.GET['command']), None) command = self.request.POST['command']
command_processor = getattr(self, f'send_{command}', None)
if command_processor is not None: if command_processor is not None:
return command_processor() return command_processor()
else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else:
handler = getattr(super(), 'get', self.http_method_not_allowed) handler = getattr(super(), 'get', self.http_method_not_allowed)
return handler(request, *args, **kwargs) return handler(request, *args, **kwargs)
def render_json(self, data, encoder=utils.JSONEncoder): def render_json(self, data, encoder=utils.JSONEncoder):
return HttpResponse( return JsonResponse(data, json_dumps_params={"indent": 2}, encoder=encoder)
json.dumps(data, indent=2, cls=encoder),
content_type='application/json')
class GASMixin: class GASMixin:
@ -97,7 +92,10 @@ class GASMixin:
def dispatch(self, *args, **kwargs): def dispatch(self, *args, **kwargs):
if self.check_user_forbidden(): if self.check_user_forbidden():
return HttpResponseRedirect(reverse('gas:login') + '?next={}'.format(self.request.path)) path = self.request.path
response = HttpResponseRedirect(reverse('gas:login') + f'?next={path}')
add_never_cache_headers(response)
return response
return super().dispatch(*args, **kwargs) return super().dispatch(*args, **kwargs)
def form_valid(self, form): def form_valid(self, form):
@ -119,7 +117,6 @@ class GASMixin:
# Forcing possible reverse_lazy evaluation # Forcing possible reverse_lazy evaluation
url = str(self.cancel_url) url = str(self.cancel_url)
return url return url
else:
return self.get_success_url() return self.get_success_url()
def get_continue_url(self): def get_continue_url(self):
@ -127,7 +124,6 @@ class GASMixin:
# Forcing possible reverse_lazy evaluation # Forcing possible reverse_lazy evaluation
url = str(self.continue_url) url = str(self.continue_url)
return url return url
else:
raise ImproperlyConfigured("No URL to redirect to. Provide a continue_url.") raise ImproperlyConfigured("No URL to redirect to. Provide a continue_url.")
def get_header_title(self): def get_header_title(self):
@ -152,10 +148,10 @@ class GASMixin:
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
ctx = super().get_context_data(**kwargs) ctx = super().get_context_data(**kwargs)
css = gas_settings.MEDIA['css'] css = gas_settings.MEDIA['css']
js = gas_settings.MEDIA['js'] javascript = gas_settings.MEDIA['js']
if gas_settings.EXTRA_MEDIA: if gas_settings.EXTRA_MEDIA:
css = css + gas_settings.EXTRA_MEDIA.get('css', []) css = css + gas_settings.EXTRA_MEDIA.get('css', [])
js = js + gas_settings.EXTRA_MEDIA.get('js', []) javascript = javascript + gas_settings.EXTRA_MEDIA.get('js', [])
ctx.update({ ctx.update({
'base_template': self.base_template, 'base_template': self.base_template,
'home_url': self.get_home_url(), 'home_url': self.get_home_url(),
@ -167,7 +163,7 @@ class GASMixin:
'gas_title': gas_settings.TITLE, 'gas_title': gas_settings.TITLE,
'logo_static_url': gas_settings.LOGO, 'logo_static_url': gas_settings.LOGO,
'css': css, 'css': css,
'js': js, 'js': javascript,
}) })
return ctx return ctx
@ -186,7 +182,6 @@ class GASListView(GASMixin, ListView):
self.filter_form = self.get_filter_form() self.filter_form = self.get_filter_form()
if self.filter_form is not None and self.filter_form.is_valid(): if self.filter_form is not None and self.filter_form.is_valid():
return self.filter_form.filter(qs) return self.filter_form.filter(qs)
else:
return qs return qs
def get_queryset(self): def get_queryset(self):
@ -216,12 +211,15 @@ class GASCreateView(GASMixin, CreateView):
def form_valid(self, form): def form_valid(self, form):
response = super().form_valid(form) response = super().form_valid(form)
if '_popup' in self.request.POST: if '_popup' in self.request.POST:
# escape() calls force_text.
obj_pk = escape(self.object.pk)
obj = escapejs(self.object)
return HttpResponse( return HttpResponse(
'<!DOCTYPE html><html><head><title></title></head><body>' '<!DOCTYPE html><html><head><title></title></head><body>'
'<script type="text/javascript">opener.dismissAddAnotherPopup(window, "%s", "%s");</script></body></html>' % '<script type="text/javascript">'
# escape() calls force_text. f' opener.dismissAddAnotherPopup(window, "{obj_pk}", "{obj}");'
(escape(self.object.pk), escapejs(self.object))) '</script></body></html>'
else: )
return response return response
@ -255,10 +253,14 @@ class GASDeleteView(GASMixin, DeleteView):
def format_callback(obj): def format_callback(obj):
opts = obj._meta opts = obj._meta
return '%s: %s' % (capfirst(opts.verbose_name), obj) name = capfirst(opts.verbose_name)
return f'{name}: {obj}'
collector.collect([self.object]) collector.collect([self.object])
model_count = {model._meta.verbose_name_plural: len(objs) for model, objs in collector.model_objs.items()} model_count = {
model._meta.verbose_name_plural: len(objs)
for model, objs in collector.model_objs.items()
}
return collector.nested(format_callback), model_count return collector.nested(format_callback), model_count
def get_context_data(self, **kwargs): def get_context_data(self, **kwargs):
@ -278,7 +280,7 @@ class GASDeleteView(GASMixin, DeleteView):
}) })
return ctx return ctx
def delete(self, request, *args, **kwargs): def form_valid(self, form):
response = super().delete(request, *args, **kwargs) response = super().form_valid(form)
messages.add_message(request, messages.SUCCESS, self.get_deleted_text()) messages.add_message(self.request, messages.SUCCESS, self.get_deleted_text())
return response return response

View File

@ -1 +1 @@
Django==3.2.13 Django>=3.2.13,<5.0

View File

@ -1,6 +1,6 @@
[metadata] [metadata]
name = django-gas name = django-gas
version = 0.8.7 version = 0.8.13
description = An alternative to django admin description = An alternative to django admin
long_description = file: readme.md, changelog.md, collaborators.md long_description = file: readme.md, changelog.md, collaborators.md
long_description_content_type = text/markdown long_description_content_type = text/markdown