From 48c8e9bd10bcdf778cb38c20651eff1ee1ef808a Mon Sep 17 00:00:00 2001 From: Thu Trang Pham Date: Sat, 31 Oct 2020 16:29:46 -0700 Subject: [PATCH] Messy but working save confirmation on change page --- .gitignore | 5 +- MANIFEST.in | 2 + README.md => README.rst | 0 admin_confirm/__init__.py | 0 admin_confirm/admin.py | 128 ++++++++++++++++++ admin_confirm/template_tags/admin_modify.py | 18 +++ .../templates/admin/change_confirmation.html | 36 +++++ .../templates/admin/submit_line.html | 7 + setup.py | 20 +++ tests/db.sqlite3 | Bin 155648 -> 0 bytes tests/testproject/asgi.py | 16 --- tests/testproject/settings.py | 12 +- 12 files changed, 221 insertions(+), 23 deletions(-) create mode 100644 MANIFEST.in rename README.md => README.rst (100%) create mode 100644 admin_confirm/__init__.py create mode 100644 admin_confirm/admin.py create mode 100644 admin_confirm/template_tags/admin_modify.py create mode 100644 admin_confirm/templates/admin/change_confirmation.html create mode 100644 admin_confirm/templates/admin/submit_line.html create mode 100644 setup.py delete mode 100644 tests/db.sqlite3 delete mode 100644 tests/testproject/asgi.py diff --git a/.gitignore b/.gitignore index bedd6a0..eb0f210 100644 --- a/.gitignore +++ b/.gitignore @@ -28,4 +28,7 @@ coverage.xml docs/_build/ # pycharm -.idea/ \ No newline at end of file +.idea/ + +# Database +db.sqlite3 \ No newline at end of file diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..da2b519 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +include *.rst +recursive-include admin-confirm/* \ No newline at end of file diff --git a/README.md b/README.rst similarity index 100% rename from README.md rename to README.rst diff --git a/admin_confirm/__init__.py b/admin_confirm/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/admin_confirm/admin.py b/admin_confirm/admin.py new file mode 100644 index 0000000..0581864 --- /dev/null +++ b/admin_confirm/admin.py @@ -0,0 +1,128 @@ + +from django.contrib.admin.exceptions import DisallowedModelAdminToField +from django.contrib.admin.utils import flatten_fieldsets, quote, unquote +from django.core.exceptions import PermissionDenied +from django.template.response import SimpleTemplateResponse, TemplateResponse +from django.contrib.admin.options import TO_FIELD_VAR, IS_POPUP_VAR +from django.utils.translation import gettext as _, ngettext + + +class AdminConfirmMixin(object): + """Generic AdminConfirm Mixin""" + + change_needs_confirmation = False + + # Custom templates (designed to be over-ridden in subclasses) + change_confirmation_template = None + + def render_change_confirmation(self, request, context): + opts = self.model._meta + app_label = opts.app_label + + request.current_app = self.admin_site.name + context.update( + media=self.media, + ) + + return TemplateResponse( + request, + self.change_confirmation_template + or [ + "admin/{}/{}/change_confirmation.html".format( + app_label, opts.model_name + ), + "admin/{}/change_confirmation.html".format(app_label), + "admin/change_confirmation.html", + ], + context, + ) + + def change_view(self, request, object_id=None, form_url="", extra_context=None): + self.message_user(request, f"{request.POST}") + if request.method == "POST" and request.POST.get("_change_needs_confirmation"): + self.message_user(request, "Needs confirmation was inside the request") + return self._change_confirmation_view( + request, object_id, form_url, extra_context + ) + + extra_context = { + **(extra_context or {}), + 'change_needs_confirmation': self.change_needs_confirmation + } + return super().change_view(request, object_id, form_url, extra_context) + + def _change_confirmation_view(self, request, object_id, form_url, extra_context): + # Do we need any of this code? + to_field = request.POST.get( + TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR) + ) + if to_field and not self.to_field_allowed(request, to_field): + raise DisallowedModelAdminToField( + "The field %s cannot be referenced." % to_field + ) + + model = self.model + opts = model._meta + + add = object_id is None + + obj = self.get_object(request, unquote(object_id), to_field) + + if obj is None: + return self._get_obj_does_not_exist_redirect(request, opts, object_id) + + if not self.has_view_or_change_permission(request, obj): + raise PermissionDenied + + fieldsets = self.get_fieldsets(request, obj) + ModelForm = self.get_form( + request, obj, change=not add, fields=flatten_fieldsets(fieldsets) + ) + + # Should we be validating the data here? Or just pass it to super? + + form = ModelForm(request.POST, request.FILES, obj) + form_validated = form.is_valid() + if form_validated: + new_object = self.save_form(request, form, change=not add) + else: + new_object = form.instance + + # End code copied from Django sourcecode + if add: + title = _("Add %s") + elif self.has_change_permission(request, obj): + title = _("Change %s") + + # Parse the original save action from request + save_action = None + for action in ["_save", "_saveasnew", "_addanother", "_continue"]: + if action in request.POST: + save_action = action + break + + form_data = {} + for key in request.POST: + if key.startswith("_") or key == 'csrfmiddlewaretoken': + continue + + form_data[key] = request.POST.get(key) + # { k: v for k, v in request.POST.\\ if not(k.startswith('_') or k == 'csrfmiddlewaretoken')} + + context = { + **self.admin_site.each_context(request), + "title": title % opts.verbose_name, + "subtitle": str(obj), + "object_name": str(obj), + "object_id": object_id, + "original": obj, + "new_object": new_object, + "app_label": opts.app_label, + "model_name": opts.model_name, + "opts": opts, + "preserved_filters": self.get_preserved_filters(request), + "form_data": form_data, + "submit_name": save_action, + **(extra_context or {}), + } + return self.render_change_confirmation(request, context) diff --git a/admin_confirm/template_tags/admin_modify.py b/admin_confirm/template_tags/admin_modify.py new file mode 100644 index 0000000..bcebf7e --- /dev/null +++ b/admin_confirm/template_tags/admin_modify.py @@ -0,0 +1,18 @@ +# import json +# +# from django import template +# from django.template.context import Context +# +# from django.contrib.admin.templatetags import admin_modify +# +# from django.contrib.admin.templatetags.base import InclusionAdminNode +# +# register = template.Library() +# +# def submit_row(context): +# ctx = admin_modify.submit_row(context) +# +# +# @register.tag(name='submit_row') +# def submit_row_tag(parser, token): +# return InclusionAdminNode(parser, token, func=submit_row, template_name='submit_line.html') \ No newline at end of file diff --git a/admin_confirm/templates/admin/change_confirmation.html b/admin_confirm/templates/admin/change_confirmation.html new file mode 100644 index 0000000..918cae6 --- /dev/null +++ b/admin_confirm/templates/admin/change_confirmation.html @@ -0,0 +1,36 @@ +{% extends "admin/base_site.html" %} +{% load i18n admin_urls static %} + +{% block extrahead %} + {{ block.super }} + {{ media }} + +{% endblock %} + +{% block bodyclass %}{{ block.super }} app-{{ opts.app_label }} model-{{ opts.model_name }} change-confirmation{% endblock %} + +{% block breadcrumbs %} + +{% endblock %} + +{% block content %} +

{% blocktrans with escaped_object=object %}Are you sure you want to change the {{ object_name }} "{{ escaped_object }}"?{% endblocktrans %}

+
{% csrf_token %} +
+ {% for key, value in form_data.items %} + + {% endfor %} +{# #} + {% if is_popup %}{% endif %} + {% if to_field %}{% endif %} + + {% trans "No, continue to edit" %} +
+
+{% endblock %} diff --git a/admin_confirm/templates/admin/submit_line.html b/admin_confirm/templates/admin/submit_line.html new file mode 100644 index 0000000..f7548f8 --- /dev/null +++ b/admin_confirm/templates/admin/submit_line.html @@ -0,0 +1,7 @@ +{% extends 'admin/submit_line.html' %} +{% load i18n admin_urls %} + +{% block submit-row %} + + {{ block.super }} +{% endblock %} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..2d89a69 --- /dev/null +++ b/setup.py @@ -0,0 +1,20 @@ +import os +from setuptools import setup + +here = os.path.abspath(os.path.dirname(__file__)) +README = open(os.path.join(here, 'README.rst')).read() + +setup( + name='django-admin-confirm', + version='0.1', + packages=['admin_confirm'], + description='Adds confirmation to Django Admin change', + long_description=README, + author='Thu Trang Pham', + author_email='thuutrangpham@gmail.com', + url='https://github.com/trangpham/django-admin-confirm/', + license='Apache 2.0', + install_requires=[ + 'Django>=1.7', + ] +) \ No newline at end of file diff --git a/tests/db.sqlite3 b/tests/db.sqlite3 deleted file mode 100644 index 97bfceb323f42e46b5b342f5bcc80df6e94d9c21..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 155648 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 diff --git a/tests/testproject/asgi.py b/tests/testproject/asgi.py deleted file mode 100644 index 6c853f1..0000000 --- a/tests/testproject/asgi.py +++ /dev/null @@ -1,16 +0,0 @@ -""" -ASGI config for testproject project. - -It exposes the ASGI callable as a module-level variable named ``application``. - -For more information on this file, see -https://docs.djangoproject.com/en/3.0/howto/deployment/asgi/ -""" - -import os - -from django.core.asgi import get_asgi_application - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproject.settings') - -application = get_asgi_application() diff --git a/tests/testproject/settings.py b/tests/testproject/settings.py index 2c3d3aa..c154f0b 100644 --- a/tests/testproject/settings.py +++ b/tests/testproject/settings.py @@ -13,7 +13,7 @@ https://docs.djangoproject.com/en/3.0/ref/settings/ import os # Build paths inside the project like this: os.path.join(BASE_DIR, ...) -BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production @@ -31,6 +31,8 @@ ALLOWED_HOSTS = ['127.0.0.1'] # Application definition INSTALLED_APPS = [ + 'admin_confirm', + 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', @@ -38,9 +40,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.staticfiles', - 'admin_confirm', - - 'tests.market', + 'market', ] MIDDLEWARE = [ @@ -53,7 +53,7 @@ MIDDLEWARE = [ 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] -ROOT_URLCONF = 'tests.testproject.urls' +ROOT_URLCONF = 'testproject.urls' TEMPLATES = [ { @@ -71,7 +71,7 @@ TEMPLATES = [ }, ] -WSGI_APPLICATION = 'tests.testproject.wsgi.application' +WSGI_APPLICATION = 'testproject.wsgi.application' # Database