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 %}
+ +{% 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 97bfceb..0000000 Binary files a/tests/db.sqlite3 and /dev/null differ 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