diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..fa033f9 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,35 @@ +name: Tests + +on: [push] + +jobs: + build: + + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.6, 3.7, 3.8, 3.9] + django-version: [2.2, 3.0] + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Django ${{ matrix.django-version }} + run: | + pip install django==${{ matrix.django-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest + if [ -f requirements.txt ]; then pip install -r requirements.txt; fi + - name: Lint with flake8 + run: | + # stop the build if there are Python syntax errors or undefined names + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + - name: Test with pytest + run: | + make test diff --git a/.vscode/settings.json b/.vscode/settings.json index 32c8f3a..aa1e0b0 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,4 +1,7 @@ { - "python.formatting.provider": "black", - "editor.formatOnSave": true -} \ No newline at end of file + "python.formatting.provider": "black", + "editor.formatOnSave": true, + "python.linting.flake8Enabled": true, + "python.analysis.extraPaths": [], + "python.languageServer": "Pylance" // use MS's fast new Python language server, +} diff --git a/Makefile b/Makefile index c51a126..094204c 100644 --- a/Makefile +++ b/Makefile @@ -20,15 +20,9 @@ package: python3 setup.py sdist bdist_wheel upload-testpypi: - ifndef VERSION - $(error VERSION is not set) - endif python3 -m twine upload --repository testpypi dist/django_admin_confirm-$(VERSION)* i-have-tested-with-testpypi-and-am-ready-to-release: - ifndef VERSION - $(error VERSION is not set) - endif python3 -m twine upload --repository pypi dist/django_admin_confirm-$(VERSION)* install-testpypi: diff --git a/README.md b/README.md index deebb0f..53cc8fa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Django Admin Confirm -![coverage](https://raw.githubusercontent.com/TrangPham/django-admin-confirm/main/coverage.svg) +[![PyPI](https://img.shields.io/pypi/v/django-admin-confirm?color=blue)](https://pypi.org/project/django-admin-confirm/) ![Tests Status](https://github.com/TrangPham/django-admin-confirm/actions/workflows/.github/workflows/test.yml/badge.svg) [![Coverage Status](https://coveralls.io/repos/github/TrangPham/django-admin-confirm/badge.svg)](https://coveralls.io/github/TrangPham/django-admin-confirm) AdminConfirmMixin is a mixin for ModelAdmin to add confirmations to change, add and actions. diff --git a/admin_confirm/__init__.py b/admin_confirm/__init__.py index 21fa6b7..f595caa 100644 --- a/admin_confirm/__init__.py +++ b/admin_confirm/__init__.py @@ -1 +1,2 @@ -from .admin import AdminConfirmMixin +__all__ = ["admin"] +from .admin import AdminConfirmMixin # noqa diff --git a/admin_confirm/admin.py b/admin_confirm/admin.py index 579d070..6198712 100644 --- a/admin_confirm/admin.py +++ b/admin_confirm/admin.py @@ -1,3 +1,4 @@ +from typing import Dict from django.contrib.admin.exceptions import DisallowedModelAdminToField from django.contrib.admin.utils import flatten_fieldsets, unquote from django.core.exceptions import PermissionDenied @@ -5,8 +6,12 @@ from django.template.response import TemplateResponse from django.contrib.admin.options import TO_FIELD_VAR from django.utils.translation import gettext as _ from django.contrib.admin import helpers +from django.db.models import Model +from django.forms import ModelForm from admin_confirm.utils import snake_to_title_case +SAVE_ACTIONS = ["_save", "_saveasnew", "_addanother", "_continue"] + class AdminConfirmMixin: # Should we ask for confirmation for changes? @@ -92,6 +97,40 @@ class AdminConfirmMixin: } return super().changeform_view(request, object_id, form_url, extra_context) + def _get_changed_data( + self, form: ModelForm, model: Model, obj: object, add: bool + ) -> Dict: + """ + Given a form, detect the changes on the form from the default values (if add) or + from the database values of the object (model instance) + + form - Submitted form that is attempting to alter the obj + model - the model class of the obj + obj - instance of model which is being altered + add - are we attempting to add the obj or does it already exist in the database + + Returns a dictionary of the fields and their changed values if any + """ + changed_data = {} + if form.is_valid(): + if add: + for name, new_value in form.cleaned_data.items(): + # Don't consider default values as changed for adding + default_value = model._meta.get_field(name).get_default() + if new_value is not None and new_value != default_value: + # Show what the default value is + changed_data[name] = [str(default_value), new_value] + else: + # Parse the changed data - Note that using form.changed_data would not work because initial is not set + for name, new_value in form.cleaned_data.items(): + # Since the form considers initial as the value first shown in the form + # It could be incorrect when user hits save, and then hits "No, go back to edit" + obj.refresh_from_db() + initial_value = getattr(obj, name) + if initial_value != new_value: + changed_data[name] = [initial_value, new_value] + return changed_data + def _change_confirmation_view(self, request, object_id, form_url, extra_context): # This code is taken from super()._changeform_view to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR)) @@ -125,24 +164,7 @@ class AdminConfirmMixin: form = ModelForm(request.POST, request.FILES, obj) # End code from super()._changeform_view - changed_data = {} - if form.is_valid(): - if add: - for name, new_value in form.cleaned_data.items(): - # Don't consider default values as changed for adding - default_value = model._meta.get_field(name).get_default() - if new_value is not None and new_value != default_value: - # Show what the default value is - changed_data[name] = [str(default_value), new_value] - else: - # Parse the changed data - Note that using form.changed_data would not work because initial is not set - for name, new_value in form.cleaned_data.items(): - # Since the form considers initial as the value first shown in the form - # It could be incorrect when user hits save, and then hits "No, go back to edit" - obj.refresh_from_db() - initial_value = getattr(obj, name) - if initial_value != new_value: - changed_data[name] = [initial_value, new_value] + changed_data = self._get_changed_data(form, model, obj, add) changed_confirmation_fields = set( self.get_confirmation_fields(request, obj) @@ -155,18 +177,17 @@ class AdminConfirmMixin: form_data = {} # Parse the original save action from request save_action = None - for key in request.POST: - if key in ["_save", "_saveasnew", "_addanother", "_continue"]: + for key, value in request.POST.items(): + if key in SAVE_ACTIONS: save_action = key + continue if key.startswith("_") or key == "csrfmiddlewaretoken": continue - form_data[key] = request.POST.get(key) - if add: - title_action = _("adding") - else: - title_action = _("changing") + form_data[key] = value + + title_action = _("adding") if add else _("changing") context = { **self.admin_site.each_context(request), diff --git a/admin_confirm/tests/test_confirm_actions.py b/admin_confirm/tests/test_confirm_actions.py index e5c96ab..6b362f5 100644 --- a/admin_confirm/tests/test_confirm_actions.py +++ b/admin_confirm/tests/test_confirm_actions.py @@ -1,7 +1,6 @@ from django.test import TestCase, RequestFactory from django.contrib.admin.sites import AdminSite from django.contrib.auth.models import Permission, User -from django.contrib.admin.options import TO_FIELD_VAR from django.urls import reverse diff --git a/requirements.txt b/requirements.txt index b11c86a..90d3a1b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,8 +3,8 @@ factory-boy~=3.0.1 django-admin-confirm~=0.2.2 coverage~=5.4 pytest~=6.2.2 -tox~=3.21.4 pytest-django~=4.1.0 coverage-badge~=1.0.1 readme-renderer~=28.0 twine~=3.3.0 +coveralls~=3.0.0 diff --git a/setup.py b/setup.py index 0de82f3..e43e083 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ README = open(os.path.join(here, "README.md")).read() setup( name="django-admin-confirm", - version="0.2.2", + version="0.2.3", packages=["admin_confirm"], description="Adds confirmation to Django Admin changes, additions and actions", long_description_content_type="text/markdown", diff --git a/tests/test_project/settings/__init__.py b/tests/test_project/settings/__init__.py index 8f607e4..55d206d 100644 --- a/tests/test_project/settings/__init__.py +++ b/tests/test_project/settings/__init__.py @@ -1 +1 @@ -from .local import * +from .local import * # noqa diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 4bab5c4..0000000 --- a/tox.ini +++ /dev/null @@ -1,25 +0,0 @@ -# tox (https://tox.readthedocs.io/) is a tool for running tests -# in multiple virtualenvs. This configuration file will run the -# test suite on all supported python versions. To use it, "pip install tox" -# and then run "tox" from this directory. - -[pytest] -DJANGO_SETTINGS_MODULE=tests.test_project.settings -addopts = --doctest-modules -ra -l --tb=short --show-capture=log --color=yes -testpaths = admin_confirm - -[tox] -envlist = - {py38, py39, py3}-dj{31,30,22,19,17}-postgres - -[testenv] -whitelist_externals = pytest -deps = - djmaster: https://github.com/django/django/archive/master.tar.gz - dj31: Django>=3.1,<3.2 - dj30: Django>=3.0,<3.1 - dj22: Django>=2.2,<2.3 - dj19: Django>=1.9,<2.2 - dj17: Django>=1.7,<1.9 -commands = - pytest