From ee8a700b1b97e405638af5f9f9369651e8601583 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 22 Apr 2020 21:05:54 +0200 Subject: [PATCH 01/41] Clean up project structure --- .gitignore | 90 +++++- .travis.yml | 2 +- MANIFEST | 24 -- {src/auditlog => auditlog}/__init__.py | 2 + {src/auditlog => auditlog}/admin.py | 0 {src/auditlog => auditlog}/apps.py | 0 {src/auditlog => auditlog}/compat.py | 0 {src/auditlog => auditlog}/diff.py | 0 {src/auditlog => auditlog}/filters.py | 0 .../management/__init__.py | 0 .../management/commands/__init__.py | 0 .../management/commands/auditlogflush.py | 0 {src/auditlog => auditlog}/middleware.py | 0 .../migrations/0001_initial.py | 0 .../0002_auto_support_long_primary_keys.py | 0 .../migrations/0003_logentry_remote_addr.py | 0 .../0004_logentry_detailed_object_repr.py | 0 ...5_logentry_additional_data_verbose_name.py | 0 .../migrations/0006_object_pk_index.py | 0 .../migrations/0007_object_pk_type.py | 0 .../migrations/__init__.py | 0 {src/auditlog => auditlog}/mixins.py | 0 {src/auditlog => auditlog}/models.py | 0 {src/auditlog => auditlog}/receivers.py | 0 {src/auditlog => auditlog}/registry.py | 0 .../__init__.py | 0 .../auditlog_tests => auditlog_tests}/apps.py | 0 .../manage.py | 0 .../models.py | 0 .../test_settings.py | 1 - .../tests.py | 0 .../auditlog_tests => auditlog_tests}/urls.py | 0 docs/Makefile | 185 +----------- docs/make.bat | 277 +++--------------- docs/source/conf.py | 267 +++-------------- requirements-test.txt | 7 - requirements.txt | 16 + src/runtests.py => runtests.py | 0 setup.py | 17 +- tox.ini | 6 +- 40 files changed, 198 insertions(+), 696 deletions(-) delete mode 100644 MANIFEST rename {src/auditlog => auditlog}/__init__.py (80%) rename {src/auditlog => auditlog}/admin.py (100%) rename {src/auditlog => auditlog}/apps.py (100%) rename {src/auditlog => auditlog}/compat.py (100%) rename {src/auditlog => auditlog}/diff.py (100%) rename {src/auditlog => auditlog}/filters.py (100%) rename {src/auditlog => auditlog}/management/__init__.py (100%) rename {src/auditlog => auditlog}/management/commands/__init__.py (100%) rename {src/auditlog => auditlog}/management/commands/auditlogflush.py (100%) rename {src/auditlog => auditlog}/middleware.py (100%) rename {src/auditlog => auditlog}/migrations/0001_initial.py (100%) rename {src/auditlog => auditlog}/migrations/0002_auto_support_long_primary_keys.py (100%) rename {src/auditlog => auditlog}/migrations/0003_logentry_remote_addr.py (100%) rename {src/auditlog => auditlog}/migrations/0004_logentry_detailed_object_repr.py (100%) rename {src/auditlog => auditlog}/migrations/0005_logentry_additional_data_verbose_name.py (100%) rename {src/auditlog => auditlog}/migrations/0006_object_pk_index.py (100%) rename {src/auditlog => auditlog}/migrations/0007_object_pk_type.py (100%) rename {src/auditlog => auditlog}/migrations/__init__.py (100%) rename {src/auditlog => auditlog}/mixins.py (100%) rename {src/auditlog => auditlog}/models.py (100%) rename {src/auditlog => auditlog}/receivers.py (100%) rename {src/auditlog => auditlog}/registry.py (100%) rename {src/auditlog_tests => auditlog_tests}/__init__.py (100%) rename {src/auditlog_tests => auditlog_tests}/apps.py (100%) rename {src/auditlog_tests => auditlog_tests}/manage.py (100%) rename {src/auditlog_tests => auditlog_tests}/models.py (100%) rename {src/auditlog_tests => auditlog_tests}/test_settings.py (99%) rename {src/auditlog_tests => auditlog_tests}/tests.py (100%) rename {src/auditlog_tests => auditlog_tests}/urls.py (100%) delete mode 100644 requirements-test.txt rename src/runtests.py => runtests.py (100%) diff --git a/.gitignore b/.gitignore index e6d60c8..155da2b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,12 +1,82 @@ -*.db -*.egg-info -*.log -*.pot -*.pyc -.idea -.project -.pydevproject -.coverage -venv/ +### Python template +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ .tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Sphinx documentation +docs/_build/ + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +### JetBrains +.idea/ diff --git a/.travis.yml b/.travis.yml index 386315a..3b9e431 100644 --- a/.travis.yml +++ b/.travis.yml @@ -48,7 +48,7 @@ matrix: fast_finish: true -install: pip install -r requirements-test.txt +install: pip install -r requirements.txt script: tox diff --git a/MANIFEST b/MANIFEST deleted file mode 100644 index f2ed177..0000000 --- a/MANIFEST +++ /dev/null @@ -1,24 +0,0 @@ -# file GENERATED by distutils, do NOT edit -setup.py -src/auditlog/__init__.py -src/auditlog/admin.py -src/auditlog/apps.py -src/auditlog/compat.py -src/auditlog/diff.py -src/auditlog/filters.py -src/auditlog/middleware.py -src/auditlog/mixins.py -src/auditlog/models.py -src/auditlog/receivers.py -src/auditlog/registry.py -src/auditlog/management/__init__.py -src/auditlog/management/commands/__init__.py -src/auditlog/management/commands/auditlogflush.py -src/auditlog/migrations/0001_initial.py -src/auditlog/migrations/0002_auto_support_long_primary_keys.py -src/auditlog/migrations/0003_logentry_remote_addr.py -src/auditlog/migrations/0004_logentry_detailed_object_repr.py -src/auditlog/migrations/0005_logentry_additional_data_verbose_name.py -src/auditlog/migrations/0006_object_pk_index.py -src/auditlog/migrations/0007_object_pk_type.py -src/auditlog/migrations/__init__.py diff --git a/src/auditlog/__init__.py b/auditlog/__init__.py similarity index 80% rename from src/auditlog/__init__.py rename to auditlog/__init__.py index 64e9e4a..3dd296c 100644 --- a/src/auditlog/__init__.py +++ b/auditlog/__init__.py @@ -1,3 +1,5 @@ from __future__ import unicode_literals +__version__ = '1.0a1' + default_app_config = 'auditlog.apps.AuditlogConfig' diff --git a/src/auditlog/admin.py b/auditlog/admin.py similarity index 100% rename from src/auditlog/admin.py rename to auditlog/admin.py diff --git a/src/auditlog/apps.py b/auditlog/apps.py similarity index 100% rename from src/auditlog/apps.py rename to auditlog/apps.py diff --git a/src/auditlog/compat.py b/auditlog/compat.py similarity index 100% rename from src/auditlog/compat.py rename to auditlog/compat.py diff --git a/src/auditlog/diff.py b/auditlog/diff.py similarity index 100% rename from src/auditlog/diff.py rename to auditlog/diff.py diff --git a/src/auditlog/filters.py b/auditlog/filters.py similarity index 100% rename from src/auditlog/filters.py rename to auditlog/filters.py diff --git a/src/auditlog/management/__init__.py b/auditlog/management/__init__.py similarity index 100% rename from src/auditlog/management/__init__.py rename to auditlog/management/__init__.py diff --git a/src/auditlog/management/commands/__init__.py b/auditlog/management/commands/__init__.py similarity index 100% rename from src/auditlog/management/commands/__init__.py rename to auditlog/management/commands/__init__.py diff --git a/src/auditlog/management/commands/auditlogflush.py b/auditlog/management/commands/auditlogflush.py similarity index 100% rename from src/auditlog/management/commands/auditlogflush.py rename to auditlog/management/commands/auditlogflush.py diff --git a/src/auditlog/middleware.py b/auditlog/middleware.py similarity index 100% rename from src/auditlog/middleware.py rename to auditlog/middleware.py diff --git a/src/auditlog/migrations/0001_initial.py b/auditlog/migrations/0001_initial.py similarity index 100% rename from src/auditlog/migrations/0001_initial.py rename to auditlog/migrations/0001_initial.py diff --git a/src/auditlog/migrations/0002_auto_support_long_primary_keys.py b/auditlog/migrations/0002_auto_support_long_primary_keys.py similarity index 100% rename from src/auditlog/migrations/0002_auto_support_long_primary_keys.py rename to auditlog/migrations/0002_auto_support_long_primary_keys.py diff --git a/src/auditlog/migrations/0003_logentry_remote_addr.py b/auditlog/migrations/0003_logentry_remote_addr.py similarity index 100% rename from src/auditlog/migrations/0003_logentry_remote_addr.py rename to auditlog/migrations/0003_logentry_remote_addr.py diff --git a/src/auditlog/migrations/0004_logentry_detailed_object_repr.py b/auditlog/migrations/0004_logentry_detailed_object_repr.py similarity index 100% rename from src/auditlog/migrations/0004_logentry_detailed_object_repr.py rename to auditlog/migrations/0004_logentry_detailed_object_repr.py diff --git a/src/auditlog/migrations/0005_logentry_additional_data_verbose_name.py b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py similarity index 100% rename from src/auditlog/migrations/0005_logentry_additional_data_verbose_name.py rename to auditlog/migrations/0005_logentry_additional_data_verbose_name.py diff --git a/src/auditlog/migrations/0006_object_pk_index.py b/auditlog/migrations/0006_object_pk_index.py similarity index 100% rename from src/auditlog/migrations/0006_object_pk_index.py rename to auditlog/migrations/0006_object_pk_index.py diff --git a/src/auditlog/migrations/0007_object_pk_type.py b/auditlog/migrations/0007_object_pk_type.py similarity index 100% rename from src/auditlog/migrations/0007_object_pk_type.py rename to auditlog/migrations/0007_object_pk_type.py diff --git a/src/auditlog/migrations/__init__.py b/auditlog/migrations/__init__.py similarity index 100% rename from src/auditlog/migrations/__init__.py rename to auditlog/migrations/__init__.py diff --git a/src/auditlog/mixins.py b/auditlog/mixins.py similarity index 100% rename from src/auditlog/mixins.py rename to auditlog/mixins.py diff --git a/src/auditlog/models.py b/auditlog/models.py similarity index 100% rename from src/auditlog/models.py rename to auditlog/models.py diff --git a/src/auditlog/receivers.py b/auditlog/receivers.py similarity index 100% rename from src/auditlog/receivers.py rename to auditlog/receivers.py diff --git a/src/auditlog/registry.py b/auditlog/registry.py similarity index 100% rename from src/auditlog/registry.py rename to auditlog/registry.py diff --git a/src/auditlog_tests/__init__.py b/auditlog_tests/__init__.py similarity index 100% rename from src/auditlog_tests/__init__.py rename to auditlog_tests/__init__.py diff --git a/src/auditlog_tests/apps.py b/auditlog_tests/apps.py similarity index 100% rename from src/auditlog_tests/apps.py rename to auditlog_tests/apps.py diff --git a/src/auditlog_tests/manage.py b/auditlog_tests/manage.py similarity index 100% rename from src/auditlog_tests/manage.py rename to auditlog_tests/manage.py diff --git a/src/auditlog_tests/models.py b/auditlog_tests/models.py similarity index 100% rename from src/auditlog_tests/models.py rename to auditlog_tests/models.py diff --git a/src/auditlog_tests/test_settings.py b/auditlog_tests/test_settings.py similarity index 99% rename from src/auditlog_tests/test_settings.py rename to auditlog_tests/test_settings.py index 2ad4b35..7bb0dc6 100644 --- a/src/auditlog_tests/test_settings.py +++ b/auditlog_tests/test_settings.py @@ -1,7 +1,6 @@ """ Settings file for the Auditlog test suite. """ -import os import django SECRET_KEY = 'test' diff --git a/src/auditlog_tests/tests.py b/auditlog_tests/tests.py similarity index 100% rename from src/auditlog_tests/tests.py rename to auditlog_tests/tests.py diff --git a/src/auditlog_tests/urls.py b/auditlog_tests/urls.py similarity index 100% rename from src/auditlog_tests/urls.py rename to auditlog_tests/urls.py diff --git a/docs/Makefile b/docs/Makefile index 7717a53..92dd33a 100644 --- a/docs/Makefile +++ b/docs/Makefile @@ -1,177 +1,20 @@ -# Makefile for Sphinx documentation +# Minimal makefile for Sphinx documentation # -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# User-friendly check for sphinx-build -ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) -$(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) -endif - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source -# the i18n builder cannot share the environment and doctrees with the others -I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = source +BUILDDIR = _build +# Put it first so that "make" without argument is like "make help". help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " texinfo to make Texinfo files" - @echo " info to make Texinfo files and run them through makeinfo" - @echo " gettext to make PO message catalogs" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " xml to make Docutils-native XML files" - @echo " pseudoxml to make pseudoxml-XML files for display purposes" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) -clean: - rm -rf $(BUILDDIR)/* +.PHONY: help Makefile -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-auditlog.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-auditlog.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/django-auditlog" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-auditlog" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -latexpdfja: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through platex and dvipdfmx..." - $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -texinfo: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo - @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." - @echo "Run \`make' in that directory to run these through makeinfo" \ - "(use \`make info' here to do that automatically)." - -info: - $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo - @echo "Running Texinfo files through makeinfo..." - make -C $(BUILDDIR)/texinfo info - @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." - -gettext: - $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale - @echo - @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." - -xml: - $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml - @echo - @echo "Build finished. The XML files are in $(BUILDDIR)/xml." - -pseudoxml: - $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml - @echo - @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/make.bat b/docs/make.bat index 155cb67..447b8bd 100644 --- a/docs/make.bat +++ b/docs/make.bat @@ -1,242 +1,35 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -if "%SPHINXBUILD%" == "" ( - set SPHINXBUILD=sphinx-build -) -set BUILDDIR=build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source -set I18NSPHINXOPTS=%SPHINXOPTS% source -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% - set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. singlehtml to make a single large HTML file - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. devhelp to make HTML files and a Devhelp project - echo. epub to make an epub - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. text to make text files - echo. man to make manual pages - echo. texinfo to make Texinfo files - echo. gettext to make PO message catalogs - echo. changes to make an overview over all changed/added/deprecated items - echo. xml to make Docutils-native XML files - echo. pseudoxml to make pseudoxml-XML files for display purposes - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - - -%SPHINXBUILD% 2> nul -if errorlevel 9009 ( - echo. - echo.The 'sphinx-build' command was not found. Make sure you have Sphinx - echo.installed, then set the SPHINXBUILD environment variable to point - echo.to the full path of the 'sphinx-build' executable. Alternatively you - echo.may add the Sphinx directory to PATH. - echo. - echo.If you don't have Sphinx installed, grab it from - echo.http://sphinx-doc.org/ - exit /b 1 -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "singlehtml" ( - %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\django-auditlog.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\django-auditlog.ghc - goto end -) - -if "%1" == "devhelp" ( - %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. - goto end -) - -if "%1" == "epub" ( - %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The epub file is in %BUILDDIR%/epub. - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - if errorlevel 1 exit /b 1 - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdf" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "latexpdfja" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - cd %BUILDDIR%/latex - make all-pdf-ja - cd %BUILDDIR%/.. - echo. - echo.Build finished; the PDF files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "text" ( - %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The text files are in %BUILDDIR%/text. - goto end -) - -if "%1" == "man" ( - %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The manual pages are in %BUILDDIR%/man. - goto end -) - -if "%1" == "texinfo" ( - %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. - goto end -) - -if "%1" == "gettext" ( - %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The message catalogs are in %BUILDDIR%/locale. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - if errorlevel 1 exit /b 1 - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - if errorlevel 1 exit /b 1 - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - if errorlevel 1 exit /b 1 - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -if "%1" == "xml" ( - %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The XML files are in %BUILDDIR%/xml. - goto end -) - -if "%1" == "pseudoxml" ( - %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml - if errorlevel 1 exit /b 1 - echo. - echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. - goto end -) - -:end +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=source +set BUILDDIR=_build + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/source/conf.py b/docs/source/conf.py index 508716c..8855625 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -1,35 +1,40 @@ -# -*- coding: utf-8 -*- +# Configuration file for the Sphinx documentation builder. # -# django-auditlog documentation build configuration file, created by -# sphinx-quickstart on Wed Nov 6 20:39:48 2013. -# -# This file is execfile()d with the current directory set to its -# containing dir. -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Preliminary ------------------------------------------------------------- -import sys import os -import sphinx_rtd_theme +import sys +from datetime import date + +# -- Path setup -------------------------------------------------------------- # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -sys.path.insert(0, os.path.abspath('../../src')) -# Django settings -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'testproject.settings') -from django.conf import settings -settings.configure() +# Add sources folder +sys.path.insert(0, os.path.abspath('../../')) -# -- General configuration ------------------------------------------------ +# Setup Django for autodoc +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'auditlog_tests.test_settings') +import django +django.setup() -# If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# -- Project information ----------------------------------------------------- + +project = 'django-auditlog' +author = 'Jan-Jelle Kester and contributors' +copyright = f'2013-{date.today().year}, {author}' + +# The full version, including alpha/beta/rc tags +import auditlog +release = auditlog.__version__ + +# -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom @@ -39,229 +44,25 @@ extensions = [ 'sphinx.ext.viewcode', ] +# Master document that contains the root table of contents +master_doc = 'index' + # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -#source_encoding = 'utf-8-sig' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = u'django-auditlog' -copyright = u'2017, Jan-Jelle Kester and contributors' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. -version = '0.4' -# The full version, including alpha/beta/rc tags. -release = '0.4.7' - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -#language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -#today = '' -# Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' - # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = [] +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] -# The reST default role (used for this markup: `text`) to use for all -# documents. -#default_role = None - -# If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -#add_module_names = True - -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -#show_authors = False - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] - -# If true, keep warnings as "system message" paragraphs in the built documents. -#keep_warnings = False - - -# -- Options for HTML output ---------------------------------------------- +# -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. +# html_theme = 'sphinx_rtd_theme' -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -#html_theme_options = {} - -# Add any paths that contain custom themes here, relative to this directory. -html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] - -# The name for this set of Sphinx documents. If None, it defaults to -# " v documentation". -#html_title = None - -# A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -#html_logo = None - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -#html_favicon = None - # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] - -# Add any extra paths that contain custom files (such as robots.txt or -# .htaccess) here, relative to this directory. These files are copied -# directly to the root of the documentation. -#html_extra_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' - -# If true, SmartyPants will be used to convert quotes and dashes to -# typographically correct entities. -#html_use_smartypants = True - -# Custom sidebar templates, maps document names to template names. -#html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -#html_additional_pages = {} - -# If false, no module index is generated. -#html_domain_indices = True - -# If false, no index is generated. -#html_use_index = True - -# If true, the index is split into individual pages for each letter. -#html_split_index = False - -# If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True - -# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True - -# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True - -# If true, an OpenSearch description file will be output, and all pages will -# contain a tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -#html_use_opensearch = '' - -# This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None - -# Output file base name for HTML help builder. -htmlhelp_basename = 'django-auditlogdoc' - - -# -- Options for LaTeX output --------------------------------------------- - -latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - ('index', 'django-auditlog.tex', u'django-auditlog Documentation', - u'Jan-Jelle Kester', 'manual'), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -#latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -#latex_use_parts = False - -# If true, show page references after internal links. -#latex_show_pagerefs = False - -# If true, show URL addresses after external links. -#latex_show_urls = False - -# Documents to append as an appendix to all manuals. -#latex_appendices = [] - -# If false, no module index is generated. -#latex_domain_indices = True - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'django-auditlog', u'django-auditlog Documentation', - [u'Jan-Jelle Kester'], 1) -] - -# If true, show URL addresses after external links. -#man_show_urls = False - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ('index', 'django-auditlog', u'django-auditlog Documentation', - u'Jan-Jelle Kester', 'django-auditlog', '', - 'Miscellaneous'), -] - -# Documents to append as an appendix to all manuals. -#texinfo_appendices = [] - -# If false, no module index is generated. -#texinfo_domain_indices = True - -# How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' - -# If true, do not generate a @detailmenu in the "Top" node's menu. -#texinfo_no_detailmenu = False diff --git a/requirements-test.txt b/requirements-test.txt deleted file mode 100644 index d240772..0000000 --- a/requirements-test.txt +++ /dev/null @@ -1,7 +0,0 @@ --r requirements.txt - -coverage==4.3.4 -tox>=1.7.0 -codecov>=2.0.0 -django-multiselectfield==0.1.8 -psycopg2-binary diff --git a/requirements.txt b/requirements.txt index 300aef5..4ad0782 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,18 @@ +# Library requirements django-jsonfield>=1.0.0 python-dateutil==2.6.0 + +# Build requirements +setuptools +wheel + +# Docs requirements +sphinx +sphinx_rtd_theme + +# Test requirements +coverage==4.3.4 +tox>=1.7.0 +codecov>=2.0.0 +django-multiselectfield==0.1.8 +psycopg2-binary diff --git a/src/runtests.py b/runtests.py similarity index 100% rename from src/runtests.py rename to runtests.py diff --git a/setup.py b/setup.py index 2ad3091..a4992f5 100644 --- a/setup.py +++ b/setup.py @@ -1,14 +1,23 @@ -from distutils.core import setup +import os + +from setuptools import setup + +import auditlog + +# Readme as long description +with open(os.path.join(os.path.dirname(__file__), "README.md"), "r") as readme_file: + long_description = readme_file.read() setup( name='django-auditlog', - version='0.4.7', + version=auditlog.__version__, packages=['auditlog', 'auditlog.migrations', 'auditlog.management', 'auditlog.management.commands'], - package_dir={'': 'src'}, url='https://github.com/jjkester/django-auditlog', license='MIT', author='Jan-Jelle Kester', description='Audit log app for Django', + long_description=long_description, + long_description_content_type='text/markdown', install_requires=[ 'django-jsonfield>=1.0.0', 'python-dateutil==2.6.0' @@ -23,5 +32,5 @@ setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'License :: OSI Approved :: MIT License', - ], + ], ) diff --git a/tox.ini b/tox.ini index fcd0388..6f2cdd7 100644 --- a/tox.ini +++ b/tox.ini @@ -7,14 +7,14 @@ envlist = [testenv] setenv = - PYTHONPATH = {toxinidir}:{toxinidir}/src/auditlog -commands = coverage run --source src/auditlog src/runtests.py + PYTHONPATH = {toxinidir}:{toxinidir}/auditlog +commands = coverage run --source auditlog runtests.py deps = django-111: Django>=1.11,<2.0 django-20: Django>=2.0,<2.1 django-21: Django>=2.1,<2.2 django-22: Django>=2.2,<2.3 - -r{toxinidir}/requirements-test.txt + -r{toxinidir}/requirements.txt basepython = py37: python3.7 py36: python3.6 From 4e7c640ba029ffa0b8103293ba1529fc27ff1863 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 22 Apr 2020 21:12:19 +0200 Subject: [PATCH 02/41] Bump copyright year --- LICENSE | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE b/LICENSE index abf8ad7..97def06 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015 Jan-Jelle Kester +Copyright (c) 2013-2020 Jan-Jelle Kester Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in From 3acab4322b57b22d5d9874abeb8f66a774432be0 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 22 Apr 2020 22:28:29 +0200 Subject: [PATCH 03/41] Drop Python 2, support Django 3.0, update dependencies --- .travis.yml | 37 +++++----------- auditlog/__init__.py | 2 - auditlog/apps.py | 2 - auditlog/diff.py | 2 - auditlog/management/commands/auditlogflush.py | 3 +- auditlog/middleware.py | 19 +++----- auditlog/migrations/0001_initial.py | 3 -- .../0002_auto_support_long_primary_keys.py | 3 -- .../migrations/0003_logentry_remote_addr.py | 3 -- .../0004_logentry_detailed_object_repr.py | 3 -- ...5_logentry_additional_data_verbose_name.py | 3 -- auditlog/migrations/0006_object_pk_index.py | 3 -- auditlog/migrations/0007_object_pk_type.py | 3 -- auditlog/models.py | 44 +++++++++---------- auditlog/receivers.py | 2 - auditlog/registry.py | 3 -- auditlog_tests/models.py | 3 -- auditlog_tests/test_settings.py | 16 +------ auditlog_tests/tests.py | 17 ------- docs/source/installation.rst | 6 +-- requirements.txt | 11 +++-- tox.ini | 13 ++---- 22 files changed, 53 insertions(+), 148 deletions(-) diff --git a/.travis.yml b/.travis.yml index 3b9e431..2707a70 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,40 +11,23 @@ addons: matrix: include: - - python: 2.7 - env: TOXENV=py27-django-111 - - - python: 3.4 - env: TOXENV=py34-django-111 - - python: 3.4 - env: TOXENV=py34-django-20 - - - python: 3.5 - env: TOXENV=py35-django-111 - - python: 3.5 - env: TOXENV=py35-django-20 - - python: 3.5 - env: TOXENV=py35-django-21 - python: 3.5 env: TOXENV=py35-django-22 - - python: 3.6 - env: TOXENV=py36-django-111 - - python: 3.6 - env: TOXENV=py36-django-20 - - python: 3.6 - env: TOXENV=py36-django-21 - python: 3.6 env: TOXENV=py36-django-22 + - python: 3.6 + env: TOXENV=py36-django-30 - - python: 3.7 - env: TOXENV=py37-django-111 - - python: 3.7 - env: TOXENV=py37-django-20 - - python: 3.7 - env: TOXENV=py37-django-21 - python: 3.7 env: TOXENV=py37-django-22 + - python: 3.7 + env: TOXENV=py37-django-30 + + - python: 3.8 + env: TOXENV=py38-django-22 + - python: 3.8 + env: TOXENV=py38-django-30 fast_finish: true @@ -61,5 +44,5 @@ deploy: on: repo: jjkester/django-auditlog branch: stable - condition: $TOXENV = py36-django-20 + condition: $TOXENV = py38-django-30 edge: true diff --git a/auditlog/__init__.py b/auditlog/__init__.py index 3dd296c..b1dd7f3 100644 --- a/auditlog/__init__.py +++ b/auditlog/__init__.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - __version__ = '1.0a1' default_app_config = 'auditlog.apps.AuditlogConfig' diff --git a/auditlog/apps.py b/auditlog/apps.py index 9704ed9..d7629f0 100644 --- a/auditlog/apps.py +++ b/auditlog/apps.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.apps import AppConfig diff --git a/auditlog/diff.py b/auditlog/diff.py index 9f996f6..e5c6221 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db.models import Model, NOT_PROVIDED, DateTimeField diff --git a/auditlog/management/commands/auditlogflush.py b/auditlog/management/commands/auditlogflush.py index 575fd39..155ba96 100644 --- a/auditlog/management/commands/auditlogflush.py +++ b/auditlog/management/commands/auditlogflush.py @@ -1,5 +1,4 @@ from django.core.management.base import BaseCommand -from six import moves from auditlog.models import LogEntry @@ -11,7 +10,7 @@ class Command(BaseCommand): answer = None while answer not in ['', 'y', 'n']: - answer = moves.input("Are you sure? [y/N]: ").lower().strip() + answer = input("Are you sure? [y/N]: ").lower().strip() if answer == 'y': count = LogEntry.objects.all().count() diff --git a/auditlog/middleware.py b/auditlog/middleware.py index 176de05..2a105ab 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,21 +1,14 @@ -from __future__ import unicode_literals - import threading import time +from functools import partial +from django.apps import apps from django.conf import settings from django.db.models.signals import pre_save -from django.utils.functional import curry -from django.apps import apps -from auditlog.models import LogEntry +from django.utils.deprecation import MiddlewareMixin + from auditlog.compat import is_authenticated - -# Use MiddlewareMixin when present (Django >= 1.10) -try: - from django.utils.deprecation import MiddlewareMixin -except ImportError: - MiddlewareMixin = object - +from auditlog.models import LogEntry threadlocal = threading.local() @@ -43,7 +36,7 @@ class AuditlogMiddleware(MiddlewareMixin): # Connect signal for automatic logging if hasattr(request, 'user') and is_authenticated(request.user): - set_actor = curry(self.set_actor, user=request.user, signal_duid=threadlocal.auditlog['signal_duid']) + set_actor = partial(self.set_actor, user=request.user, signal_duid=threadlocal.auditlog['signal_duid']) pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'], weak=False) def process_response(self, request, response): diff --git a/auditlog/migrations/0001_initial.py b/auditlog/migrations/0001_initial.py index 5a96248..7abec05 100644 --- a/auditlog/migrations/0001_initial.py +++ b/auditlog/migrations/0001_initial.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import django.db.models.deletion from django.conf import settings diff --git a/auditlog/migrations/0002_auto_support_long_primary_keys.py b/auditlog/migrations/0002_auto_support_long_primary_keys.py index 5ea87f6..e34b3c9 100644 --- a/auditlog/migrations/0002_auto_support_long_primary_keys.py +++ b/auditlog/migrations/0002_auto_support_long_primary_keys.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/auditlog/migrations/0003_logentry_remote_addr.py b/auditlog/migrations/0003_logentry_remote_addr.py index 948f63c..adf2c89 100644 --- a/auditlog/migrations/0003_logentry_remote_addr.py +++ b/auditlog/migrations/0003_logentry_remote_addr.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations diff --git a/auditlog/migrations/0004_logentry_detailed_object_repr.py b/auditlog/migrations/0004_logentry_detailed_object_repr.py index 2b9af2c..d6c8ee7 100644 --- a/auditlog/migrations/0004_logentry_detailed_object_repr.py +++ b/auditlog/migrations/0004_logentry_detailed_object_repr.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import models, migrations import jsonfield.fields diff --git a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py index 7837a7c..6554289 100644 --- a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py +++ b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models import jsonfield.fields diff --git a/auditlog/migrations/0006_object_pk_index.py b/auditlog/migrations/0006_object_pk_index.py index 273e6bd..ac431c0 100644 --- a/auditlog/migrations/0006_object_pk_index.py +++ b/auditlog/migrations/0006_object_pk_index.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/auditlog/migrations/0007_object_pk_type.py b/auditlog/migrations/0007_object_pk_type.py index 3a724e8..275db7e 100644 --- a/auditlog/migrations/0007_object_pk_type.py +++ b/auditlog/migrations/0007_object_pk_type.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - from django.db import migrations, models diff --git a/auditlog/models.py b/auditlog/models.py index 0832f2c..4d69387 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -1,8 +1,8 @@ -from __future__ import unicode_literals - -import json import ast +import json +from dateutil import parser +from dateutil.tz import gettz from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType @@ -10,13 +10,9 @@ from django.core.exceptions import FieldDoesNotExist from django.db import models, DEFAULT_DB_ALIAS from django.db.models import QuerySet, Q from django.utils import formats, timezone -from django.utils.encoding import python_2_unicode_compatible, smart_text -from django.utils.six import iteritems, integer_types +from django.utils.encoding import smart_str from django.utils.translation import ugettext_lazy as _ - from jsonfield.fields import JSONField -from dateutil import parser -from dateutil.tz import gettz class LogEntryManager(models.Manager): @@ -41,9 +37,9 @@ class LogEntryManager(models.Manager): if changes is not None: kwargs.setdefault('content_type', ContentType.objects.get_for_model(instance)) kwargs.setdefault('object_pk', pk) - kwargs.setdefault('object_repr', smart_text(instance)) + kwargs.setdefault('object_repr', smart_str(instance)) - if isinstance(pk, integer_types): + if isinstance(pk, int): kwargs.setdefault('object_id', pk) get_additional_data = getattr(instance, 'get_additional_data', None) @@ -53,7 +49,9 @@ class LogEntryManager(models.Manager): # Delete log entries with the same pk as a newly created model. This should only be necessary when an pk is # used twice. if kwargs.get('action', None) is LogEntry.Action.CREATE: - if kwargs.get('object_id', None) is not None and self.filter(content_type=kwargs.get('content_type'), object_id=kwargs.get('object_id')).exists(): + if kwargs.get('object_id', None) is not None and self.filter(content_type=kwargs.get('content_type'), + object_id=kwargs.get( + 'object_id')).exists(): self.filter(content_type=kwargs.get('content_type'), object_id=kwargs.get('object_id')).delete() else: self.filter(content_type=kwargs.get('content_type'), object_pk=kwargs.get('object_pk', '')).delete() @@ -78,10 +76,10 @@ class LogEntryManager(models.Manager): content_type = ContentType.objects.get_for_model(instance.__class__) pk = self._get_pk_value(instance) - if isinstance(pk, integer_types): + if isinstance(pk, int): return self.filter(content_type=content_type, object_id=pk) else: - return self.filter(content_type=content_type, object_pk=smart_text(pk)) + return self.filter(content_type=content_type, object_pk=smart_str(pk)) def get_for_objects(self, queryset): """ @@ -98,10 +96,10 @@ class LogEntryManager(models.Manager): content_type = ContentType.objects.get_for_model(queryset.model) primary_keys = list(queryset.values_list(queryset.model._meta.pk.name, flat=True)) - if isinstance(primary_keys[0], integer_types): + if isinstance(primary_keys[0], int): return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys)).distinct() elif isinstance(queryset.model._meta.pk, models.UUIDField): - primary_keys = [smart_text(pk) for pk in primary_keys] + primary_keys = [smart_str(pk) for pk in primary_keys] return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct() else: return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct() @@ -140,7 +138,6 @@ class LogEntryManager(models.Manager): return pk -@python_2_unicode_compatible class LogEntry(models.Model): """ Represents an entry in the audit log. The content type is saved along with the textual and numeric (if available) @@ -171,13 +168,15 @@ class LogEntry(models.Model): (DELETE, _("delete")), ) - content_type = models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE, related_name='+', verbose_name=_("content type")) + content_type = models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE, related_name='+', + verbose_name=_("content type")) object_pk = models.CharField(db_index=True, max_length=255, verbose_name=_("object pk")) object_id = models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name=_("object id")) object_repr = models.TextField(verbose_name=_("object representation")) action = models.PositiveSmallIntegerField(choices=Action.choices, verbose_name=_("action")) changes = models.TextField(blank=True, verbose_name=_("change message")) - actor = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, related_name='+', verbose_name=_("actor")) + actor = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, + related_name='+', verbose_name=_("actor")) remote_addr = models.GenericIPAddressField(blank=True, null=True, verbose_name=_("remote address")) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) additional_data = JSONField(blank=True, null=True, verbose_name=_("additional data")) @@ -213,7 +212,7 @@ class LogEntry(models.Model): return {} @property - def changes_str(self, colon=': ', arrow=smart_text(' \u2192 '), separator='; '): + def changes_str(self, colon=': ', arrow=' \u2192 ', separator='; '): """ Return the changes recorded in this log entry as a string. The formatting of the string can be customized by setting alternate values for colon, arrow and separator. If the formatting is still not satisfying, please use @@ -226,8 +225,8 @@ class LogEntry(models.Model): """ substrings = [] - for field, values in iteritems(self.changes_dict): - substring = smart_text('{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}').format( + for field, values in self.changes_dict.items(): + substring = '{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}'.format( field_name=field, colon=colon, old=values[0], @@ -249,7 +248,7 @@ class LogEntry(models.Model): model_fields = auditlog.get_model_fields(model._meta.model) changes_display_dict = {} # grab the changes_dict and iterate through - for field_name, values in iteritems(self.changes_dict): + for field_name, values in self.changes_dict.items(): # try to get the field attribute on the model try: field = model._meta.get_field(field_name) @@ -355,6 +354,7 @@ class AuditlogHistoryField(GenericRelation): # South compatibility for AuditlogHistoryField try: from south.modelsinspector import add_introspection_rules + add_introspection_rules([], ["^auditlog\.models\.AuditlogHistoryField"]) raise DeprecationWarning("South support will be dropped in django-auditlog 0.4.0 or later.") except ImportError: diff --git a/auditlog/receivers.py b/auditlog/receivers.py index 9ee4cf3..25a6228 100644 --- a/auditlog/receivers.py +++ b/auditlog/receivers.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import json from auditlog.diff import model_instance_diff diff --git a/auditlog/registry.py b/auditlog/registry.py index 124fd3c..e5e34ff 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -1,8 +1,5 @@ -from __future__ import unicode_literals - from django.db.models.signals import pre_save, post_save, post_delete from django.db.models import Model -from django.utils.six import iteritems class AuditlogModelRegistry(object): diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index 50b5833..4eda784 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -5,8 +5,6 @@ from django.db import models from auditlog.models import AuditlogHistoryField from auditlog.registry import auditlog -from multiselectfield import MultiSelectField - @auditlog.register() class SimpleModel(models.Model): @@ -171,7 +169,6 @@ class ChoicesFieldModel(models.Model): ) status = models.CharField(max_length=1, choices=STATUS_CHOICES) - multiselect = MultiSelectField(max_length=3, choices=STATUS_CHOICES, max_choices=3) multiplechoice = models.CharField(max_length=255, choices=STATUS_CHOICES) history = AuditlogHistoryField() diff --git a/auditlog_tests/test_settings.py b/auditlog_tests/test_settings.py index 7bb0dc6..fdd2f2a 100644 --- a/auditlog_tests/test_settings.py +++ b/auditlog_tests/test_settings.py @@ -1,7 +1,6 @@ """ Settings file for the Auditlog test suite. """ -import django SECRET_KEY = 'test' @@ -13,10 +12,9 @@ INSTALLED_APPS = [ 'django.contrib.admin', 'auditlog', 'auditlog_tests', - 'multiselectfield', ] -middlewares = ( +MIDDLEWARE = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', @@ -24,19 +22,9 @@ middlewares = ( 'auditlog.middleware.AuditlogMiddleware', ) -if django.VERSION < (1, 10): - MIDDLEWARE_CLASSES = middlewares -else: - MIDDLEWARE = middlewares - -if django.VERSION <= (1, 9): - POSTGRES_DRIVER = 'django.db.backends.postgresql_psycopg2' -else: - POSTGRES_DRIVER = 'django.db.backends.postgresql' - DATABASES = { 'default': { - 'ENGINE': POSTGRES_DRIVER, + 'ENGINE': 'django.db.backends.postgresql', 'NAME': 'auditlog_tests_db', 'USER': 'postgres', 'PASSWORD': '', diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index d13f72f..7e7b198 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -506,7 +506,6 @@ class ChoicesFieldModelTest(TestCase): def setUp(self): self.obj = ChoicesFieldModel.objects.create( status=ChoicesFieldModel.RED, - multiselect=[ChoicesFieldModel.RED, ChoicesFieldModel.GREEN], multiplechoice=[ChoicesFieldModel.RED, ChoicesFieldModel.YELLOW, ChoicesFieldModel.GREEN], ) @@ -518,22 +517,6 @@ class ChoicesFieldModelTest(TestCase): self.obj.save() self.assertTrue(self.obj.history.latest().changes_display_dict["status"][1] == "Green", msg="The human readable text 'Green' is displayed.") - def test_changes_display_dict_multiselect(self): - self.assertTrue(self.obj.history.latest().changes_display_dict["multiselect"][1] == "Red, Green", - msg="The human readable text for the two choices, 'Red, Green' is displayed.") - self.obj.multiselect = ChoicesFieldModel.GREEN - self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["multiselect"][1] == "Green", - msg="The human readable text 'Green' is displayed.") - self.obj.multiselect = None - self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["multiselect"][1] == "None", - msg="The human readable text 'None' is displayed.") - self.obj.multiselect = ChoicesFieldModel.GREEN - self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["multiselect"][1] == "Green", - msg="The human readable text 'Green' is displayed.") - def test_changes_display_dict_multiplechoice(self): self.assertTrue(self.obj.history.latest().changes_display_dict["multiplechoice"][1] == "Red, Yellow, Green", msg="The human readable text 'Red, Yellow, Green' is displayed.") diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 0db8706..9f7c2a8 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -11,10 +11,10 @@ The repository can be found at https://github.com/jjkester/django-auditlog/. **Requirements** -- Python 2.7, 3.4 or higher -- Django 1.8 or higher +- Python 3.5 or higher +- Django 2.2 or higher -Auditlog is currently tested with Python 2.7 and 3.4 and Django 1.8, 1.9 and 1.10. The latest test report can be found +Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2 and 3.0. The latest test report can be found at https://travis-ci.org/jjkester/django-auditlog. Adding Auditlog to your Django application diff --git a/requirements.txt b/requirements.txt index 4ad0782..7616965 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,6 @@ # Library requirements -django-jsonfield>=1.0.0 -python-dateutil==2.6.0 +django-jsonfield +python-dateutil # Build requirements setuptools @@ -11,8 +11,7 @@ sphinx sphinx_rtd_theme # Test requirements -coverage==4.3.4 -tox>=1.7.0 -codecov>=2.0.0 -django-multiselectfield==0.1.8 +coverage +tox +codecov psycopg2-binary diff --git a/tox.ini b/tox.ini index 6f2cdd7..16f700f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,23 +1,18 @@ [tox] envlist = - {py27,py34,py35,py36,py37}-django-111 - {py34,py35,py36,py37}-django-20 - {py35,py36,py37}-django-21 - {py35,py36,py37}-django-22 + {py35,py36,py37,py38}-django-22 + {py36,py37,py38}-django-30 [testenv] setenv = PYTHONPATH = {toxinidir}:{toxinidir}/auditlog commands = coverage run --source auditlog runtests.py deps = - django-111: Django>=1.11,<2.0 - django-20: Django>=2.0,<2.1 - django-21: Django>=2.1,<2.2 django-22: Django>=2.2,<2.3 + django-30: Django>=3.0,<3.1 -r{toxinidir}/requirements.txt basepython = + py38: python3.8 py37: python3.7 py36: python3.6 py35: python3.5 - py34: python3.4 - py27: python2.7 From 2010b49d0687ae67a249da81d4cfff2656d54465 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Wed, 22 Apr 2020 23:00:12 +0200 Subject: [PATCH 04/41] Fix Django 3.0 field choices diff --- auditlog/models.py | 6 +++--- auditlog_tests/test_settings.py | 11 ++++++----- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/auditlog/models.py b/auditlog/models.py index 4d69387..f04e562 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -8,7 +8,7 @@ from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist from django.db import models, DEFAULT_DB_ALIAS -from django.db.models import QuerySet, Q +from django.db.models import QuerySet, Q, Field from django.utils import formats, timezone from django.utils.encoding import smart_str from django.utils.translation import ugettext_lazy as _ @@ -258,9 +258,9 @@ class LogEntry(models.Model): values_display = [] # handle choices fields and Postgres ArrayField to get human readable version choices_dict = None - if hasattr(field, 'choices') and len(field.choices) > 0: + if getattr(field, 'choices') and len(field.choices) > 0: choices_dict = dict(field.choices) - if hasattr(field, 'base_field') and getattr(field.base_field, 'choices', False): + if hasattr(field, 'base_field') and isinstance(field.base_field, Field) and getattr(field.base_field, 'choices') and len(field.base_field.choices) > 0: choices_dict = dict(field.base_field.choices) if choices_dict: diff --git a/auditlog_tests/test_settings.py b/auditlog_tests/test_settings.py index fdd2f2a..e8ac594 100644 --- a/auditlog_tests/test_settings.py +++ b/auditlog_tests/test_settings.py @@ -1,6 +1,7 @@ """ Settings file for the Auditlog test suite. """ +import os SECRET_KEY = 'test' @@ -25,11 +26,11 @@ MIDDLEWARE = ( DATABASES = { 'default': { 'ENGINE': 'django.db.backends.postgresql', - 'NAME': 'auditlog_tests_db', - 'USER': 'postgres', - 'PASSWORD': '', - 'HOST': '127.0.0.1', - 'PORT': '5432', + 'NAME': os.getenv('TEST_DB_NAME', 'auditlog_tests_db'), + 'USER': os.getenv('TEST_DB_USER', 'postgres'), + 'PASSWORD': os.getenv('TEST_DB_PASS', ''), + 'HOST': os.getenv('TEST_DB_HOST', '127.0.0.1'), + 'PORT': os.getenv('TEST_DB_PORT', '5432'), } } From c619b8c60649fb05b02f339e23ffff7bcbe74811 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Mon, 31 Aug 2020 13:57:10 +0200 Subject: [PATCH 05/41] Support Django 3.1 --- auditlog_tests/test_settings.py | 6 ++++++ docs/source/installation.rst | 2 +- requirements.txt | 1 + tox.ini | 1 + 4 files changed, 9 insertions(+), 1 deletion(-) diff --git a/auditlog_tests/test_settings.py b/auditlog_tests/test_settings.py index e8ac594..eb05b48 100644 --- a/auditlog_tests/test_settings.py +++ b/auditlog_tests/test_settings.py @@ -3,6 +3,8 @@ Settings file for the Auditlog test suite. """ import os +DEBUG = True + SECRET_KEY = 'test' INSTALLED_APPS = [ @@ -11,6 +13,7 @@ INSTALLED_APPS = [ 'django.contrib.messages', 'django.contrib.sessions', 'django.contrib.admin', + 'django.contrib.staticfiles', 'auditlog', 'auditlog_tests', ] @@ -41,6 +44,7 @@ TEMPLATES = [ 'DIRS': [], 'OPTIONS': { 'context_processors': [ + 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', ] @@ -48,6 +52,8 @@ TEMPLATES = [ }, ] +STATIC_URL = '/static/' + ROOT_URLCONF = 'auditlog_tests.urls' USE_TZ = True diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 9f7c2a8..4833db7 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -14,7 +14,7 @@ The repository can be found at https://github.com/jjkester/django-auditlog/. - Python 3.5 or higher - Django 2.2 or higher -Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2 and 3.0. The latest test report can be found +Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2, 3.0 and 3.1. The latest test report can be found at https://travis-ci.org/jjkester/django-auditlog. Adding Auditlog to your Django application diff --git a/requirements.txt b/requirements.txt index 7616965..7f57516 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ # Library requirements +Django django-jsonfield python-dateutil diff --git a/tox.ini b/tox.ini index 16f700f..39fbea2 100644 --- a/tox.ini +++ b/tox.ini @@ -2,6 +2,7 @@ envlist = {py35,py36,py37,py38}-django-22 {py36,py37,py38}-django-30 + {py36,py37,py38}-django-31 [testenv] setenv = From 228c5949fb5fd22842232fb882e45704bc333211 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Mon, 31 Aug 2020 14:14:12 +0200 Subject: [PATCH 06/41] Use admin for test site --- auditlog_tests/admin.py | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 auditlog_tests/admin.py diff --git a/auditlog_tests/admin.py b/auditlog_tests/admin.py new file mode 100644 index 0000000..858596e --- /dev/null +++ b/auditlog_tests/admin.py @@ -0,0 +1,6 @@ +from django.contrib import admin + +from auditlog.registry import auditlog + +for model in auditlog.get_models(): + admin.site.register(model) From 469fe362de56c4b63927529b4eca2bb040e75b6f Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Mon, 31 Aug 2020 14:14:32 +0200 Subject: [PATCH 07/41] Code improvements --- auditlog/compat.py | 20 -------------- auditlog/diff.py | 7 ++--- auditlog/middleware.py | 3 +-- auditlog/mixins.py | 21 ++++++++------- auditlog/registry.py | 60 ++++++++++++++++++++++++----------------- auditlog_tests/tests.py | 39 --------------------------- 6 files changed, 51 insertions(+), 99 deletions(-) delete mode 100644 auditlog/compat.py diff --git a/auditlog/compat.py b/auditlog/compat.py deleted file mode 100644 index 086b346..0000000 --- a/auditlog/compat.py +++ /dev/null @@ -1,20 +0,0 @@ -import django - -def is_authenticated(user): - """Return whether or not a User is authenticated. - - Function provides compatibility following deprecation of method call to - `is_authenticated()` in Django 2.0. - - This is *only* required to support Django < v1.10 (i.e. v1.9 and earlier), - as `is_authenticated` was introduced as a property in v1.10.s - """ - if not hasattr(user, 'is_authenticated'): - return False - if callable(user.is_authenticated): - # Will be callable if django.version < 2.0, but is only necessary in - # v1.9 and earlier due to change introduced in v1.10 making - # `is_authenticated` a property instead of a callable. - return user.is_authenticated() - else: - return user.is_authenticated diff --git a/auditlog/diff.py b/auditlog/diff.py index e5c6221..137d12b 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -55,6 +55,7 @@ def get_fields_in_model(instance): def get_field_value(obj, field): """ Gets the value of a given model instance field. + :param obj: The model instance. :type obj: Model :param field: The field you want to find the value of. @@ -64,7 +65,7 @@ def get_field_value(obj, field): """ if isinstance(field, DateTimeField): # DateTimeFields are timezone-aware, so we need to convert the field - # to its naive form before we can accuratly compare them for changes. + # to its naive form before we can accurately compare them for changes. try: value = field.to_python(getattr(obj, field.name, None)) if value is not None and settings.USE_TZ and not timezone.is_naive(value): @@ -95,9 +96,9 @@ def model_instance_diff(old, new): """ from auditlog.registry import auditlog - if not(old is None or isinstance(old, Model)): + if not (old is None or isinstance(old, Model)): raise TypeError("The supplied old instance is not a valid model instance.") - if not(new is None or isinstance(new, Model)): + if not (new is None or isinstance(new, Model)): raise TypeError("The supplied new instance is not a valid model instance.") diff = {} diff --git a/auditlog/middleware.py b/auditlog/middleware.py index 2a105ab..4ef07f6 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -7,7 +7,6 @@ from django.conf import settings from django.db.models.signals import pre_save from django.utils.deprecation import MiddlewareMixin -from auditlog.compat import is_authenticated from auditlog.models import LogEntry threadlocal = threading.local() @@ -35,7 +34,7 @@ class AuditlogMiddleware(MiddlewareMixin): threadlocal.auditlog['remote_addr'] = request.META.get('HTTP_X_FORWARDED_FOR').split(',')[0] # Connect signal for automatic logging - if hasattr(request, 'user') and is_authenticated(request.user): + if hasattr(request, 'user') and getattr(request.user, 'is_authenticated', False): set_actor = partial(self.set_actor, user=request.user, signal_duid=threadlocal.auditlog['signal_duid']) pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'], weak=False) diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 5a0b829..7d4635d 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -1,17 +1,13 @@ import json +from django import urls as urlresolvers from django.conf import settings -try: - from django.core import urlresolvers -except ImportError: - from django import urls as urlresolvers -try: - from django.urls.exceptions import NoReverseMatch -except ImportError: - from django.core.urlresolvers import NoReverseMatch +from django.urls.exceptions import NoReverseMatch from django.utils.html import format_html from django.utils.safestring import mark_safe +from auditlog.models import LogEntry + MAX = 75 @@ -19,6 +15,7 @@ class LogEntryAdminMixin(object): def created(self, obj): return obj.timestamp.strftime('%Y-%m-%d %H:%M:%S') + created.short_description = 'Created' def user_url(self, obj): @@ -32,6 +29,7 @@ class LogEntryAdminMixin(object): return format_html(u'{}', link, obj.actor) return 'system' + user_url.short_description = 'User' def resource_url(self, obj): @@ -44,10 +42,11 @@ class LogEntryAdminMixin(object): return obj.object_repr else: return format_html(u'{}', link, obj.object_repr) + resource_url.short_description = 'Resource' def msg_short(self, obj): - if obj.action == 2: + if obj.action == LogEntry.Action.DELETE: return '' # delete changes = json.loads(obj.changes) s = '' if len(changes) == 1 else 's' @@ -56,10 +55,11 @@ class LogEntryAdminMixin(object): i = fields.rfind(' ', 0, MAX) fields = fields[:i] + ' ..' return '%d change%s: %s' % (len(changes), s, fields) + msg_short.short_description = 'Changes' def msg(self, obj): - if obj.action == 2: + if obj.action == LogEntry.Action.DELETE: return '' # delete changes = json.loads(obj.changes) msg = '' @@ -69,4 +69,5 @@ class LogEntryAdminMixin(object): msg += '
#FieldFromTo
' return mark_safe(msg) + msg.short_description = 'Changes' diff --git a/auditlog/registry.py b/auditlog/registry.py index e5e34ff..538f5dd 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -1,12 +1,19 @@ -from django.db.models.signals import pre_save, post_save, post_delete +from typing import Dict, Callable, Optional, List, Tuple + from django.db.models import Model +from django.db.models.base import ModelBase +from django.db.models.signals import pre_save, post_save, post_delete, ModelSignal + +DispatchUID = Tuple[int, str, int] class AuditlogModelRegistry(object): """ A registry that keeps track of the models that use Auditlog to track changes. """ - def __init__(self, create=True, update=True, delete=True, custom=None): + + def __init__(self, create: bool = True, update: bool = True, delete: bool = True, + custom: Optional[Dict[ModelSignal, Callable]] = None): from auditlog.receivers import log_create, log_update, log_delete self._registry = {} @@ -22,17 +29,25 @@ class AuditlogModelRegistry(object): if custom is not None: self._signals.update(custom) - def register(self, model=None, include_fields=[], exclude_fields=[], mapping_fields={}): + def register(self, model: ModelBase = None, include_fields: Optional[List[str]] = None, + exclude_fields: Optional[List[str]] = None, mapping_fields: Optional[Dict[str, str]] = None): """ Register a model with auditlog. Auditlog will then track mutations on this model's instances. :param model: The model to register. - :type model: Model :param include_fields: The fields to include. Implicitly excludes all other fields. - :type include_fields: list :param exclude_fields: The fields to exclude. Overrides the fields to include. - :type exclude_fields: list + :param mapping_fields: Mapping from field names to strings in diff. + """ + + if include_fields is None: + include_fields = [] + if exclude_fields is None: + exclude_fields = [] + if mapping_fields is None: + mapping_fields = {} + def registrar(cls): """Register models for a given class.""" if not issubclass(cls, Model): @@ -58,23 +73,21 @@ class AuditlogModelRegistry(object): # Otherwise, just register the model. registrar(model) - def contains(self, model): + def contains(self, model: ModelBase) -> bool: """ Check if a model is registered with auditlog. :param model: The model to check. - :type model: Model :return: Whether the model has been registered. :rtype: bool """ return model in self._registry - def unregister(self, model): + def unregister(self, model: ModelBase) -> None: """ Unregister a model with auditlog. This will not affect the database. :param model: The model to unregister. - :type model: Model """ try: del self._registry[model] @@ -83,6 +96,16 @@ class AuditlogModelRegistry(object): else: self._disconnect_signals(model) + def get_models(self) -> List[ModelBase]: + return list(self._registry.keys()) + + def get_model_fields(self, model: ModelBase): + return { + 'include_fields': list(self._registry[model]['include_fields']), + 'exclude_fields': list(self._registry[model]['exclude_fields']), + 'mapping_fields': dict(self._registry[model]['mapping_fields']), + } + def _connect_signals(self, model): """ Connect signals for the model. @@ -98,24 +121,11 @@ class AuditlogModelRegistry(object): for signal, receiver in self._signals.items(): signal.disconnect(sender=model, dispatch_uid=self._dispatch_uid(signal, model)) - def _dispatch_uid(self, signal, model): + def _dispatch_uid(self, signal, model) -> DispatchUID: """ Generate a dispatch_uid. """ - return (self.__class__, model, signal) - - def get_model_fields(self, model): - return { - 'include_fields': self._registry[model]['include_fields'], - 'exclude_fields': self._registry[model]['exclude_fields'], - 'mapping_fields': self._registry[model]['mapping_fields'], - } - - -class AuditLogModelRegistry(AuditlogModelRegistry): - def __init__(self, *args, **kwargs): - super(AuditLogModelRegistry, self).__init__(*args, **kwargs) - raise DeprecationWarning("Use AuditlogModelRegistry instead of AuditLogModelRegistry, AuditLogModelRegistry will be removed in django-auditlog 0.4.0 or later.") + return self.__hash__(), model.__qualname__, signal.__hash__() auditlog = AuditlogModelRegistry() diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 7e7b198..9993f76 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -17,7 +17,6 @@ from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, UUIDPrimaryKe ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \ ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \ CharfieldTextfieldModel, PostgresArrayFieldModel, NoDeleteHistoryModel -from auditlog import compat class SimpleModelTest(TestCase): @@ -586,44 +585,6 @@ class PostgresArrayFieldModelTest(TestCase): msg="The human readable text 'Green' is displayed.") -class CompatibilityTest(TestCase): - """Test case for compatibility functions.""" - - def test_is_authenticated(self): - """Test that the 'is_authenticated' compatibility function is working. - - Bit of explanation: the `is_authenticated` property on request.user is - *always* set to 'False' for AnonymousUser, and it is *always* set to - 'True' for *any* other (i.e. identified/authenticated) user. - - So, the logic of this test is to ensure that compat.is_authenticated() - returns the correct value based on whether or not the User is an - anonymous user (simulating what goes on in the real request.user). - - """ - - # Test compat.is_authenticated for anonymous users - self.user = auth.get_user(self.client) - if django.VERSION < (1, 10): - assert self.user.is_anonymous() - else: - assert self.user.is_anonymous - assert not compat.is_authenticated(self.user) - - # Setup some other user, which is *not* anonymous, and check - # compat.is_authenticated - self.user = User.objects.create( - username="test.user", - email="test.user@mail.com", - password="auditlog" - ) - if django.VERSION < (1, 10): - assert not self.user.is_anonymous() - else: - assert not self.user.is_anonymous - assert compat.is_authenticated(self.user) - - class AdminPanelTest(TestCase): @classmethod def setUpTestData(cls): From f14f6b34eed9ba635ba687b04ce52bd421a3b74b Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Mon, 31 Aug 2020 14:16:21 +0200 Subject: [PATCH 08/41] Remove stale code --- auditlog/diff.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/auditlog/diff.py b/auditlog/diff.py index 137d12b..e627149 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -25,10 +25,6 @@ def track_field(field): if getattr(field, 'remote_field', None) is not None and field.remote_field.model == LogEntry: return False - # 1.8 check - elif getattr(field, 'rel', None) is not None and field.rel.to == LogEntry: - return False - return True @@ -44,12 +40,7 @@ def get_fields_in_model(instance): """ assert isinstance(instance, Model) - # Check if the Django 1.8 _meta API is available - use_api = hasattr(instance._meta, 'get_fields') and callable(instance._meta.get_fields) - - if use_api: - return [f for f in instance._meta.get_fields() if track_field(f)] - return instance._meta.fields + return [f for f in instance._meta.get_fields() if track_field(f)] def get_field_value(obj, field): From f4edfc0592eb0c1180086027ba5934e6fa55d1ac Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Mon, 31 Aug 2020 14:22:50 +0200 Subject: [PATCH 09/41] Management command improvements --- auditlog/management/commands/auditlogflush.py | 22 ++++++++++++------- docs/source/usage.rst | 4 ++-- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/auditlog/management/commands/auditlogflush.py b/auditlog/management/commands/auditlogflush.py index 155ba96..00e7647 100644 --- a/auditlog/management/commands/auditlogflush.py +++ b/auditlog/management/commands/auditlogflush.py @@ -4,16 +4,22 @@ from auditlog.models import LogEntry class Command(BaseCommand): - help = 'Deletes all log entries from the database.' + help = "Deletes all log entries from the database." + + def add_arguments(self, parser): + parser.add_argument('-y, --yes', action='store_true', default=None, + help="Continue without asking confirmation.", dest='yes') def handle(self, *args, **options): - answer = None + answer = options['yes'] - while answer not in ['', 'y', 'n']: - answer = input("Are you sure? [y/N]: ").lower().strip() - - if answer == 'y': - count = LogEntry.objects.all().count() - LogEntry.objects.all().delete() + while answer is None: + print("This action will clear all log entries from the database.") + response = input("Are you sure you want to continue? [y/N]: ").lower().strip() + answer = True if response == 'y' else False if response == 'n' else None + if answer: + count, _ = LogEntry.objects.all().delete() print("Deleted %d objects." % count) + else: + print("Aborted.") diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 22bb6f6..8aa15f0 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -191,8 +191,8 @@ Management commands Auditlog provides the ``auditlogflush`` management command to clear all log entries from the database. -The command asks for confirmation, it is not possible to execute the command without giving any form of (simulated) user -input. +By default, the command asks for confirmation. It is possible to run the command with the `-y` or `--yes` flag to skip +confirmation and immediately delete all entries. .. warning:: From 346105dcf9b17a85ccb29583cb0081cbcc71a409 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Mon, 31 Aug 2020 14:47:26 +0200 Subject: [PATCH 10/41] Use more generic `.pk` to get primary key instead of `.id` Fixes #140 --- auditlog/mixins.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 7d4635d..87225dd 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -23,7 +23,7 @@ class LogEntryAdminMixin(object): app_label, model = settings.AUTH_USER_MODEL.split('.') viewname = 'admin:%s_%s_change' % (app_label, model.lower()) try: - link = urlresolvers.reverse(viewname, args=[obj.actor.id]) + link = urlresolvers.reverse(viewname, args=[obj.actor.pk]) except NoReverseMatch: return u'%s' % (obj.actor) return format_html(u'{}', link, obj.actor) From 33fa2490714954b189e24963e4586f86ce4f99c2 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Mon, 31 Aug 2020 14:50:16 +0200 Subject: [PATCH 11/41] Allow higher versions of python-dateutil Fixes #162, closes #184 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index a4992f5..436cac0 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ setup( long_description_content_type='text/markdown', install_requires=[ 'django-jsonfield>=1.0.0', - 'python-dateutil==2.6.0' + 'python-dateutil>=2.6.0' ], zip_safe=False, classifiers=[ From 7bb17fd5d2cd564a1504ddc619ca04eb08e8d65d Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Mon, 31 Aug 2020 15:36:24 +0200 Subject: [PATCH 12/41] Remove 'Django' requirement to satisfy Travis --- requirements.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index 7f57516..7616965 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,4 @@ # Library requirements -Django django-jsonfield python-dateutil From 565239180e2cc386156e80c98566df2c77f55463 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Mon, 7 Sep 2020 09:10:37 +0200 Subject: [PATCH 13/41] Review improvements --- auditlog/management/commands/auditlogflush.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/auditlog/management/commands/auditlogflush.py b/auditlog/management/commands/auditlogflush.py index 00e7647..ecf4bd7 100644 --- a/auditlog/management/commands/auditlogflush.py +++ b/auditlog/management/commands/auditlogflush.py @@ -13,13 +13,13 @@ class Command(BaseCommand): def handle(self, *args, **options): answer = options['yes'] - while answer is None: - print("This action will clear all log entries from the database.") + if answer is None: + self.stdout.write("This action will clear all log entries from the database.") response = input("Are you sure you want to continue? [y/N]: ").lower().strip() - answer = True if response == 'y' else False if response == 'n' else None + answer = response == 'y' if answer: count, _ = LogEntry.objects.all().delete() - print("Deleted %d objects." % count) + self.stdout.write("Deleted %d objects." % count) else: - print("Aborted.") + self.stdout.write("Aborted.") From 31418d54f2792927afa14b141601114df681ae98 Mon Sep 17 00:00:00 2001 From: Jan-Jelle Kester Date: Fri, 23 Oct 2020 12:16:27 +0200 Subject: [PATCH 14/41] Changes for Jazzband (#269) * Update repository references to Jazzband Issue #268 * Add Jazzband badge to README Issue #268 * Add Jazzband contribution guideline Issue #268 --- .travis.yml | 2 +- CONTRIBUTING.md | 3 +++ README.md | 3 ++- docs/source/index.rst | 4 ++-- docs/source/installation.rst | 4 ++-- setup.py | 4 +--- 6 files changed, 11 insertions(+), 9 deletions(-) create mode 100644 CONTRIBUTING.md diff --git a/.travis.yml b/.travis.yml index 2707a70..53170ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,7 +42,7 @@ deploy: provider: pypi # PyPI credentials supplied with environment variables from repository settings on: - repo: jjkester/django-auditlog + repo: jazzband/django-auditlog branch: stable condition: $TOXENV = py38-django-30 edge: true diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..ad78220 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,3 @@ +[![Jazzband](https://jazzband.co/static/img/jazzband.svg)](https://jazzband.co/) + +This is a [Jazzband](https://jazzband.co/) project. By contributing you agree to abide by the [Contributor Code of Conduct](https://jazzband.co/about/conduct) and follow the [guidelines](https://jazzband.co/about/guidelines). diff --git a/README.md b/README.md index 84f26da..d499699 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ django-auditlog =============== -[![Build Status](https://travis-ci.org/jjkester/django-auditlog.svg?branch=master)](https://travis-ci.org/jjkester/django-auditlog) +[![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) +[![Build Status](https://travis-ci.org/jazzband/django-auditlog.svg?branch=master)](https://travis-ci.org/jazzband/django-auditlog) [![Docs](https://readthedocs.org/projects/django-auditlog/badge/?version=latest)](http://django-auditlog.readthedocs.org/en/latest/?badge=latest) **Please remember that this app is still in development.** diff --git a/docs/source/index.rst b/docs/source/index.rst index 9afcb5e..a9c0016 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -36,5 +36,5 @@ Contribute to Auditlog If you discovered a bug or want to improve the code, please submit an issue and/or pull request via GitHub. Before submitting a new issue, please make sure there is no issue submitted that involves the same problem. -| GitHub repository: https://github.com/jjkester/django-auditlog -| Issues: https://github.com/jjkester/django-auditlog/issues +| GitHub repository: https://github.com/jazzband/django-auditlog +| Issues: https://github.com/jazzband/django-auditlog/issues diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 4833db7..149e2fa 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -7,7 +7,7 @@ way to do this is by using the Python Package Index (PyPI). Simply run the follo ``pip install django-auditlog`` Instead of installing Auditlog via PyPI, you can also clone the Git repository or download the source code via GitHub. -The repository can be found at https://github.com/jjkester/django-auditlog/. +The repository can be found at https://github.com/jazzband/django-auditlog/. **Requirements** @@ -15,7 +15,7 @@ The repository can be found at https://github.com/jjkester/django-auditlog/. - Django 2.2 or higher Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2, 3.0 and 3.1. The latest test report can be found -at https://travis-ci.org/jjkester/django-auditlog. +at https://travis-ci.org/jazzband/django-auditlog. Adding Auditlog to your Django application ------------------------------------------ diff --git a/setup.py b/setup.py index 436cac0..03c3c78 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ setup( name='django-auditlog', version=auditlog.__version__, packages=['auditlog', 'auditlog.migrations', 'auditlog.management', 'auditlog.management.commands'], - url='https://github.com/jjkester/django-auditlog', + url='https://github.com/jazzband/django-auditlog', license='MIT', author='Jan-Jelle Kester', description='Audit log app for Django', @@ -24,8 +24,6 @@ setup( ], zip_safe=False, classifiers=[ - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', From 50da34125cd078a1b10ad8d956a8c6e1697ad2e2 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Wed, 25 Nov 2020 22:15:14 +0100 Subject: [PATCH 15/41] Simplified travis config and added Jazzband release config. (#281) * Simplified travis config and added Jazzband release config. * Migrate to travis-ci.com. * Split requirements to prevent env spoilage. * Add docs requirements. * Huh * type * Add psycopg2 doc requirements. --- .travis.yml | 70 ++++++++++++++++-------------------- auditlog/__init__.py | 8 ++++- auditlog_tests/tests.py | 1 + docs/requirements.txt | 4 +++ docs/source/conf.py | 9 ++--- docs/source/installation.rst | 2 +- requirements.txt | 17 --------- setup.py | 5 ++- tox.ini | 22 ++++++++++-- 9 files changed, 69 insertions(+), 69 deletions(-) create mode 100644 docs/requirements.txt delete mode 100644 requirements.txt diff --git a/.travis.yml b/.travis.yml index 53170ee..d5d29d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,48 +1,38 @@ -# Config file for automatic testing at travis-ci.org - dist: xenial sudo: required language: python services: - - postgresql - +- postgresql addons: - postgresql: "10" - -matrix: + postgresql: '10' +cache: + pip: true +python: + - 3.5 + - 3.6 + - 3.7 + - 3.8 +jobs: include: - - python: 3.5 - env: TOXENV=py35-django-22 - - - python: 3.6 - env: TOXENV=py36-django-22 - - python: 3.6 - env: TOXENV=py36-django-30 - - - python: 3.7 - env: TOXENV=py37-django-22 - - python: 3.7 - env: TOXENV=py37-django-30 - - - python: 3.8 - env: TOXENV=py38-django-22 - - python: 3.8 - env: TOXENV=py38-django-30 - + - stage: deploy + if: tag IS present + python: 3.8 + script: skip + deploy: + provider: pypi + user: jazzband + server: https://jazzband.co/projects/django-auditlog/upload + distributions: sdist bdist_wheel + password: + secure: AD22a73v//OXpP8WgFscTQHQxpNE5Rup9+NhoxNRNYXszc+g9Z9yNO5cokPHD0HUL7pma/V31xK1tavFO7jHfvidcnx8mNwezoW7lQctNErxhtuAUN59654IaTPmTOU6vvdslmtNT1W/M/LDsp5hWyzaCbTNbl5Ag4+spUpSCq8= + skip_existing: true + on: + tags: true + repo: jazzband/django-auditlog fast_finish: true - -install: pip install -r requirements.txt - -script: tox - +install: + - travis_retry pip install -U pip + - travis_retry pip install tox-travis codecov +script: tox -v after_success: - - codecov -e TOX_ENV - -deploy: - provider: pypi - # PyPI credentials supplied with environment variables from repository settings - on: - repo: jazzband/django-auditlog - branch: stable - condition: $TOXENV = py38-django-30 - edge: true +- codecov diff --git a/auditlog/__init__.py b/auditlog/__init__.py index b1dd7f3..e369079 100644 --- a/auditlog/__init__.py +++ b/auditlog/__init__.py @@ -1,3 +1,9 @@ -__version__ = '1.0a1' +from pkg_resources import get_distribution, DistributionNotFound + +try: + __version__ = get_distribution("django-auditlog").version +except DistributionNotFound: + # package is not installed + pass default_app_config = 'auditlog.apps.AuditlogConfig' diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 9993f76..edd0670 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -259,6 +259,7 @@ class AdditionalDataModelTest(TestCase): log_entry = obj_with_additional_data.history.get() self.assertIsNotNone(log_entry.additional_data) extra_data = log_entry.additional_data + print(type(extra_data), extra_data) self.assertTrue(extra_data['related_model_text'] == related_model.text, msg="Related model's text is logged") self.assertTrue(extra_data['related_model_id'] == related_model.id, diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..f516c7c --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,4 @@ +# Docs requirements +sphinx +sphinx_rtd_theme +psycopg2-binary diff --git a/docs/source/conf.py b/docs/source/conf.py index 8855625..9f3c37f 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -9,6 +9,7 @@ import os import sys from datetime import date +from pkg_resources import get_distribution # -- Path setup -------------------------------------------------------------- @@ -30,9 +31,9 @@ project = 'django-auditlog' author = 'Jan-Jelle Kester and contributors' copyright = f'2013-{date.today().year}, {author}' -# The full version, including alpha/beta/rc tags -import auditlog -release = auditlog.__version__ +release = get_distribution('django-auditlog').version +# for example take major/minor +version = '.'.join(release.split('.')[:2]) # -- General configuration --------------------------------------------------- @@ -65,4 +66,4 @@ html_theme = 'sphinx_rtd_theme' # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +# html_static_path = ['_static'] diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 149e2fa..fd63125 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -15,7 +15,7 @@ The repository can be found at https://github.com/jazzband/django-auditlog/. - Django 2.2 or higher Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2, 3.0 and 3.1. The latest test report can be found -at https://travis-ci.org/jazzband/django-auditlog. +at https://travis-ci.com/jazzband/django-auditlog. Adding Auditlog to your Django application ------------------------------------------ diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 7616965..0000000 --- a/requirements.txt +++ /dev/null @@ -1,17 +0,0 @@ -# Library requirements -django-jsonfield -python-dateutil - -# Build requirements -setuptools -wheel - -# Docs requirements -sphinx -sphinx_rtd_theme - -# Test requirements -coverage -tox -codecov -psycopg2-binary diff --git a/setup.py b/setup.py index 03c3c78..956dd80 100644 --- a/setup.py +++ b/setup.py @@ -2,15 +2,14 @@ import os from setuptools import setup -import auditlog - # Readme as long description with open(os.path.join(os.path.dirname(__file__), "README.md"), "r") as readme_file: long_description = readme_file.read() setup( name='django-auditlog', - version=auditlog.__version__, + use_scm_version={"version_scheme": "post-release"}, + setup_requires=["setuptools_scm"], packages=['auditlog', 'auditlog.migrations', 'auditlog.management', 'auditlog.management.commands'], url='https://github.com/jazzband/django-auditlog', license='MIT', diff --git a/tox.ini b/tox.ini index 39fbea2..7afa09d 100644 --- a/tox.ini +++ b/tox.ini @@ -3,17 +3,33 @@ envlist = {py35,py36,py37,py38}-django-22 {py36,py37,py38}-django-30 {py36,py37,py38}-django-31 + py38-docs [testenv] -setenv = - PYTHONPATH = {toxinidir}:{toxinidir}/auditlog commands = coverage run --source auditlog runtests.py deps = django-22: Django>=2.2,<2.3 django-30: Django>=3.0,<3.1 - -r{toxinidir}/requirements.txt + # Test requirements + coverage + codecov + psycopg2-binary + basepython = py38: python3.8 py37: python3.7 py36: python3.6 py35: python3.5 + +[testenv:py38-docs] +changedir = docs/source +deps = -rdocs/requirements.txt +commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html + +[travis] +python = + 2.7: py27 + 3.5: py35 + 3.6: py36 + 3.7: py37 + 3.8: py38 From 910089597eebce9b23b7fca9085fa08e4cffd55d Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Thu, 26 Nov 2020 10:45:20 +0100 Subject: [PATCH 16/41] Initial GitHub Actions workflow. (#283) * Initial GitHub Actions workflow. * Use correct Postgres port. * Fix duplicate. * Use POSTGRES_HOST? * Fixing postgres config? * Pass test env vars with Tox. * Work around issue with Django 3.1. * Write coverage file. * Add release workflow. * Remove Travis config file. * Update .github/workflows/test.yml Co-authored-by: Hugo van Kemenade * Update auditlog_tests/tests.py Co-authored-by: Hugo van Kemenade * Update .github/workflows/test.yml Co-authored-by: Hugo van Kemenade * Update README.md Co-authored-by: Hugo van Kemenade * Add Django 3.1 to tox config. Co-authored-by: Hugo van Kemenade --- .github/workflows/release.yml | 53 +++++++++++++++++++++++++++ .github/workflows/test.yml | 69 +++++++++++++++++++++++++++++++++++ .travis.yml | 38 ------------------- README.md | 4 +- auditlog_tests/tests.py | 11 ++++-- docs/source/installation.rst | 2 +- tox.ini | 17 ++++++--- 7 files changed, 145 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/release.yml create mode 100644 .github/workflows/test.yml delete mode 100644 .travis.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..55a6b6b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +name: Release + +on: + push: + tags: + - '*' + +jobs: + build: + if: github.repository == 'jazzband/django-auditlog' + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: 3.8 + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: release-${{ hashFiles('**/setup.py') }} + restore-keys: | + release- + + - name: Install dependencies + run: | + python -m pip install -U pip + python -m pip install -U setuptools twine wheel + + - name: Build package + run: | + python setup.py --version + python setup.py sdist --format=gztar bdist_wheel + twine check dist/* + + - name: Upload packages to Jazzband + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') + uses: pypa/gh-action-pypi-publish@master + with: + user: jazzband + password: ${{ secrets.JAZZBAND_RELEASE_KEY }} + repository_url: https://jazzband.co/projects/django-auditlog/upload diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..255c02e --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,69 @@ +name: Test + +on: [push, pull_request] + +jobs: + build: + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 5 + matrix: + python-version: ['3.5', '3.6', '3.7', '3.8'] + + services: + postgres: + image: postgres:10 + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: postgres + POSTGRES_DB: postgres + ports: + - 5432/tcp + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + + steps: + - uses: actions/checkout@v2 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + + - name: Get pip cache dir + id: pip-cache + run: | + echo "::set-output name=dir::$(pip cache dir)" + + - name: Cache + uses: actions/cache@v2 + with: + path: ${{ steps.pip-cache.outputs.dir }} + key: + -${{ matrix.python-version }}-v1-${{ hashFiles('**/setup.py') }} + restore-keys: | + -${{ matrix.python-version }}-v1- + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install --upgrade tox tox-gh-actions + + - name: Tox tests + run: | + tox -v + env: + TEST_DB_HOST: localhost + TEST_DB_USER: postgres + TEST_DB_PASS: postgres + TEST_DB_NAME: postgres + TEST_DB_PORT: ${{ job.services.postgres.ports[5432] }} + + - name: Upload coverage + uses: codecov/codecov-action@v1 + with: + name: Python ${{ matrix.python-version }} diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index d5d29d8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,38 +0,0 @@ -dist: xenial -sudo: required -language: python -services: -- postgresql -addons: - postgresql: '10' -cache: - pip: true -python: - - 3.5 - - 3.6 - - 3.7 - - 3.8 -jobs: - include: - - stage: deploy - if: tag IS present - python: 3.8 - script: skip - deploy: - provider: pypi - user: jazzband - server: https://jazzband.co/projects/django-auditlog/upload - distributions: sdist bdist_wheel - password: - secure: AD22a73v//OXpP8WgFscTQHQxpNE5Rup9+NhoxNRNYXszc+g9Z9yNO5cokPHD0HUL7pma/V31xK1tavFO7jHfvidcnx8mNwezoW7lQctNErxhtuAUN59654IaTPmTOU6vvdslmtNT1W/M/LDsp5hWyzaCbTNbl5Ag4+spUpSCq8= - skip_existing: true - on: - tags: true - repo: jazzband/django-auditlog - fast_finish: true -install: - - travis_retry pip install -U pip - - travis_retry pip install tox-travis codecov -script: tox -v -after_success: -- codecov diff --git a/README.md b/README.md index d499699..306dbce 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ django-auditlog =============== [![Jazzband](https://jazzband.co/static/img/badge.svg)](https://jazzband.co/) -[![Build Status](https://travis-ci.org/jazzband/django-auditlog.svg?branch=master)](https://travis-ci.org/jazzband/django-auditlog) +[![Build Status](https://github.com/jazzband/django-auditlog/workflows/Test/badge.svg)](https://github.com/jazzband/django-auditlog/actions) [![Docs](https://readthedocs.org/projects/django-auditlog/badge/?version=latest)](http://django-auditlog.readthedocs.org/en/latest/?badge=latest) +[![codecov](https://codecov.io/gh/jazzband/django-auditlog/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/django-auditlog) **Please remember that this app is still in development.** **Test this app before deploying it in production environments.** @@ -39,4 +40,3 @@ Releases 5. Pull request `stable` -> `master`. Now everything is back in sync. Opening a pull request from `master` directly to `stable` is discouraged as `master` may be updated while the PR is open, thus changing the contents of the release. - diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index edd0670..4dd68d7 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1,5 +1,6 @@ import datetime import django +import json from django.conf import settings from django.contrib import auth from django.contrib.auth.models import User, AnonymousUser @@ -257,9 +258,13 @@ class AdditionalDataModelTest(TestCase): self.assertTrue(obj_with_additional_data.history.count() == 1, msg="There is 1 log entry") log_entry = obj_with_additional_data.history.get() - self.assertIsNotNone(log_entry.additional_data) - extra_data = log_entry.additional_data - print(type(extra_data), extra_data) + # FIXME: Work-around for the fact that additional_data isn't working + # on Django 3.1 correctly (see https://github.com/jazzband/django-auditlog/issues/266) + if django.VERSION >= (3, 1): + extra_data = json.loads(log_entry.additional_data) + else: + extra_data = log_entry.additional_data + self.assertIsNotNone(extra_data) self.assertTrue(extra_data['related_model_text'] == related_model.text, msg="Related model's text is logged") self.assertTrue(extra_data['related_model_id'] == related_model.id, diff --git a/docs/source/installation.rst b/docs/source/installation.rst index fd63125..a879370 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -15,7 +15,7 @@ The repository can be found at https://github.com/jazzband/django-auditlog/. - Django 2.2 or higher Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2, 3.0 and 3.1. The latest test report can be found -at https://travis-ci.com/jazzband/django-auditlog. +at https://github.com/jazzband/django-auditlog/actions. Adding Auditlog to your Django application ------------------------------------------ diff --git a/tox.ini b/tox.ini index 7afa09d..bd3a88c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,19 +1,27 @@ [tox] envlist = {py35,py36,py37,py38}-django-22 - {py36,py37,py38}-django-30 - {py36,py37,py38}-django-31 + {py36,py37,py38}-django-{30,31} py38-docs [testenv] -commands = coverage run --source auditlog runtests.py +commands = + coverage run --source auditlog runtests.py + coverage xml deps = django-22: Django>=2.2,<2.3 django-30: Django>=3.0,<3.1 + django-31: Django>=3.1,<3.2 # Test requirements coverage codecov psycopg2-binary +passenv= + TEST_DB_HOST + TEST_DB_USER + TEST_DB_PASS + TEST_DB_NAME + TEST_DB_PORT basepython = py38: python3.8 @@ -26,9 +34,8 @@ changedir = docs/source deps = -rdocs/requirements.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html -[travis] +[gh-actions] python = - 2.7: py27 3.5: py35 3.6: py36 3.7: py37 From 793cb459606de09363fb94c299177152c405e04e Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 6 Dec 2020 21:02:39 +0100 Subject: [PATCH 17/41] Add Python 3.8 to setup.py classifiers. --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 956dd80..b464f13 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ setup( 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'License :: OSI Approved :: MIT License', ], ) From e9cfdb2e480a621381e673436c24cdfffb752084 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 6 Dec 2020 21:12:00 +0100 Subject: [PATCH 18/41] Add Python 3.9 support. --- .github/workflows/test.yml | 2 +- README.md | 1 + setup.py | 1 + tox.ini | 6 ++++-- 4 files changed, 7 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 255c02e..40c56e4 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.5', '3.6', '3.7', '3.8'] + python-version: ['3.5', '3.6', '3.7', '3.8', '3.9'] services: postgres: diff --git a/README.md b/README.md index 306dbce..b656281 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ django-auditlog [![Build Status](https://github.com/jazzband/django-auditlog/workflows/Test/badge.svg)](https://github.com/jazzband/django-auditlog/actions) [![Docs](https://readthedocs.org/projects/django-auditlog/badge/?version=latest)](http://django-auditlog.readthedocs.org/en/latest/?badge=latest) [![codecov](https://codecov.io/gh/jazzband/django-auditlog/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/django-auditlog) +[![Supported Python versions](https://img.shields.io/pypi/pyversions/django-auditlog.svg)](https://pypi.python.org/pypi/django-auditlog) **Please remember that this app is still in development.** **Test this app before deploying it in production environments.** diff --git a/setup.py b/setup.py index b464f13..7fc063b 100644 --- a/setup.py +++ b/setup.py @@ -29,6 +29,7 @@ setup( 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', + 'Programming Language :: Python :: 3.9', 'License :: OSI Approved :: MIT License', ], ) diff --git a/tox.ini b/tox.ini index bd3a88c..2a5eb8c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] envlist = - {py35,py36,py37,py38}-django-22 - {py36,py37,py38}-django-{30,31} + {py35,py36,py37,py38,py39}-django-22 + {py36,py37,py38,py39}-django-{30,31} py38-docs [testenv] @@ -24,6 +24,7 @@ passenv= TEST_DB_PORT basepython = + py39: python3.9 py38: python3.8 py37: python3.7 py36: python3.6 @@ -40,3 +41,4 @@ python = 3.6: py36 3.7: py37 3.8: py38 + 3.9: py39 From f08b521b8b77b6515dbbf02e01c885985f6c01f0 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 6 Dec 2020 21:13:05 +0100 Subject: [PATCH 19/41] Add Supported Django versions badge to README. --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index b656281..7cd2bf8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ django-auditlog [![Docs](https://readthedocs.org/projects/django-auditlog/badge/?version=latest)](http://django-auditlog.readthedocs.org/en/latest/?badge=latest) [![codecov](https://codecov.io/gh/jazzband/django-auditlog/branch/master/graph/badge.svg)](https://codecov.io/gh/jazzband/django-auditlog) [![Supported Python versions](https://img.shields.io/pypi/pyversions/django-auditlog.svg)](https://pypi.python.org/pypi/django-auditlog) +[![Supported Django versions](https://img.shields.io/pypi/djversions/django-auditlog.svg)](https://pypi.python.org/pypi/django-auditlog) **Please remember that this app is still in development.** **Test this app before deploying it in production environments.** From b700e40f65953ea0c87666d38d53e968581611e1 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 6 Dec 2020 21:57:08 +0100 Subject: [PATCH 20/41] Remove old django related codes. --- auditlog_tests/urls.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/auditlog_tests/urls.py b/auditlog_tests/urls.py index c9346f2..df5b838 100644 --- a/auditlog_tests/urls.py +++ b/auditlog_tests/urls.py @@ -1,13 +1,7 @@ -import django -from django.conf.urls import include, url +from django.urls import path from django.contrib import admin -if django.VERSION < (1, 9): - admin_urls = include(admin.site.urls) -else: - admin_urls = admin.site.urls - urlpatterns = [ - url(r'^admin/', admin_urls), + path("admin/", admin.site.urls), ] From f5bb5cb1a2b08c596fa9f2e233fc0050030202e3 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 6 Dec 2020 21:29:24 +0100 Subject: [PATCH 21/41] Add black and format files with black. --- auditlog/__init__.py | 2 +- auditlog/admin.py | 18 +- auditlog/apps.py | 2 +- auditlog/diff.py | 30 +- auditlog/filters.py | 8 +- auditlog/management/commands/auditlogflush.py | 21 +- auditlog/middleware.py | 55 +- auditlog/migrations/0001_initial.py | 73 ++- .../0002_auto_support_long_primary_keys.py | 10 +- .../migrations/0003_logentry_remote_addr.py | 10 +- .../0004_logentry_detailed_object_repr.py | 6 +- ...5_logentry_additional_data_verbose_name.py | 10 +- auditlog/migrations/0006_object_pk_index.py | 10 +- auditlog/migrations/0007_object_pk_type.py | 10 +- auditlog/mixins.py | 53 +- auditlog/models.py | 151 ++++-- auditlog/registry.py | 38 +- auditlog_tests/__init__.py | 2 +- auditlog_tests/apps.py | 2 +- auditlog_tests/models.py | 47 +- auditlog_tests/test_settings.py | 62 +-- auditlog_tests/tests.py | 479 +++++++++++++----- pyproject.toml | 2 + runtests.py | 2 +- setup.py | 40 +- tox.ini | 8 + 26 files changed, 778 insertions(+), 373 deletions(-) create mode 100644 pyproject.toml diff --git a/auditlog/__init__.py b/auditlog/__init__.py index e369079..b3d77c3 100644 --- a/auditlog/__init__.py +++ b/auditlog/__init__.py @@ -6,4 +6,4 @@ except DistributionNotFound: # package is not installed pass -default_app_config = 'auditlog.apps.AuditlogConfig' +default_app_config = "auditlog.apps.AuditlogConfig" diff --git a/auditlog/admin.py b/auditlog/admin.py index a4c60fc..2f32ccb 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -5,13 +5,19 @@ from .filters import ResourceTypeFilter class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): - list_display = ['created', 'resource_url', 'action', 'msg_short', 'user_url'] - search_fields = ['timestamp', 'object_repr', 'changes', 'actor__first_name', 'actor__last_name'] - list_filter = ['action', ResourceTypeFilter] - readonly_fields = ['created', 'resource_url', 'action', 'user_url', 'msg'] + list_display = ["created", "resource_url", "action", "msg_short", "user_url"] + search_fields = [ + "timestamp", + "object_repr", + "changes", + "actor__first_name", + "actor__last_name", + ] + list_filter = ["action", ResourceTypeFilter] + readonly_fields = ["created", "resource_url", "action", "user_url", "msg"] fieldsets = [ - (None, {'fields': ['created', 'user_url', 'resource_url']}), - ('Changes', {'fields': ['action', 'msg']}), + (None, {"fields": ["created", "user_url", "resource_url"]}), + ("Changes", {"fields": ["action", "msg"]}), ] diff --git a/auditlog/apps.py b/auditlog/apps.py index d7629f0..e592f8d 100644 --- a/auditlog/apps.py +++ b/auditlog/apps.py @@ -2,5 +2,5 @@ from django.apps import AppConfig class AuditlogConfig(AppConfig): - name = 'auditlog' + name = "auditlog" verbose_name = "Audit log" diff --git a/auditlog/diff.py b/auditlog/diff.py index e627149..3774e60 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -17,12 +17,16 @@ def track_field(field): :rtype: bool """ from auditlog.models import LogEntry + # Do not track many to many relations if field.many_to_many: return False # Do not track relations to LogEntry - if getattr(field, 'remote_field', None) is not None and field.remote_field.model == LogEntry: + if ( + getattr(field, "remote_field", None) is not None + and field.remote_field.model == LogEntry + ): return False return True @@ -108,16 +112,26 @@ def model_instance_diff(old, new): model_fields = None # Check if fields must be filtered - if model_fields and (model_fields['include_fields'] or model_fields['exclude_fields']) and fields: + if ( + model_fields + and (model_fields["include_fields"] or model_fields["exclude_fields"]) + and fields + ): filtered_fields = [] - if model_fields['include_fields']: - filtered_fields = [field for field in fields - if field.name in model_fields['include_fields']] + if model_fields["include_fields"]: + filtered_fields = [ + field + for field in fields + if field.name in model_fields["include_fields"] + ] else: filtered_fields = fields - if model_fields['exclude_fields']: - filtered_fields = [field for field in filtered_fields - if field.name not in model_fields['exclude_fields']] + if model_fields["exclude_fields"]: + filtered_fields = [ + field + for field in filtered_fields + if field.name not in model_fields["exclude_fields"] + ] fields = filtered_fields for field in fields: diff --git a/auditlog/filters.py b/auditlog/filters.py index c5b651a..21591ac 100644 --- a/auditlog/filters.py +++ b/auditlog/filters.py @@ -2,13 +2,13 @@ from django.contrib.admin import SimpleListFilter class ResourceTypeFilter(SimpleListFilter): - title = 'Resource Type' - parameter_name = 'resource_type' + title = "Resource Type" + parameter_name = "resource_type" def lookups(self, request, model_admin): qs = model_admin.get_queryset(request) - types = qs.values_list('content_type_id', 'content_type__model') - return list(types.order_by('content_type__model').distinct()) + types = qs.values_list("content_type_id", "content_type__model") + return list(types.order_by("content_type__model").distinct()) def queryset(self, request, queryset): if self.value() is None: diff --git a/auditlog/management/commands/auditlogflush.py b/auditlog/management/commands/auditlogflush.py index ecf4bd7..a2becd4 100644 --- a/auditlog/management/commands/auditlogflush.py +++ b/auditlog/management/commands/auditlogflush.py @@ -7,16 +7,25 @@ class Command(BaseCommand): help = "Deletes all log entries from the database." def add_arguments(self, parser): - parser.add_argument('-y, --yes', action='store_true', default=None, - help="Continue without asking confirmation.", dest='yes') + parser.add_argument( + "-y, --yes", + action="store_true", + default=None, + help="Continue without asking confirmation.", + dest="yes", + ) def handle(self, *args, **options): - answer = options['yes'] + answer = options["yes"] if answer is None: - self.stdout.write("This action will clear all log entries from the database.") - response = input("Are you sure you want to continue? [y/N]: ").lower().strip() - answer = response == 'y' + self.stdout.write( + "This action will clear all log entries from the database." + ) + response = ( + input("Are you sure you want to continue? [y/N]: ").lower().strip() + ) + answer = response == "y" if answer: count, _ = LogEntry.objects.all().delete() diff --git a/auditlog/middleware.py b/auditlog/middleware.py index 4ef07f6..ec1f563 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -25,25 +25,40 @@ class AuditlogMiddleware(MiddlewareMixin): """ # Initialize thread local storage threadlocal.auditlog = { - 'signal_duid': (self.__class__, time.time()), - 'remote_addr': request.META.get('REMOTE_ADDR'), + "signal_duid": (self.__class__, time.time()), + "remote_addr": request.META.get("REMOTE_ADDR"), } # In case of proxy, set 'original' address - if request.META.get('HTTP_X_FORWARDED_FOR'): - threadlocal.auditlog['remote_addr'] = request.META.get('HTTP_X_FORWARDED_FOR').split(',')[0] + if request.META.get("HTTP_X_FORWARDED_FOR"): + threadlocal.auditlog["remote_addr"] = request.META.get( + "HTTP_X_FORWARDED_FOR" + ).split(",")[0] # Connect signal for automatic logging - if hasattr(request, 'user') and getattr(request.user, 'is_authenticated', False): - set_actor = partial(self.set_actor, user=request.user, signal_duid=threadlocal.auditlog['signal_duid']) - pre_save.connect(set_actor, sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid'], weak=False) + if hasattr(request, "user") and getattr( + request.user, "is_authenticated", False + ): + set_actor = partial( + self.set_actor, + user=request.user, + signal_duid=threadlocal.auditlog["signal_duid"], + ) + pre_save.connect( + set_actor, + sender=LogEntry, + dispatch_uid=threadlocal.auditlog["signal_duid"], + weak=False, + ) def process_response(self, request, response): """ Disconnects the signal receiver to prevent it from staying active. """ - if hasattr(threadlocal, 'auditlog'): - pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid']) + if hasattr(threadlocal, "auditlog"): + pre_save.disconnect( + sender=LogEntry, dispatch_uid=threadlocal.auditlog["signal_duid"] + ) return response @@ -51,8 +66,10 @@ class AuditlogMiddleware(MiddlewareMixin): """ Disconnects the signal receiver to prevent it from staying active in case of an exception. """ - if hasattr(threadlocal, 'auditlog'): - pre_save.disconnect(sender=LogEntry, dispatch_uid=threadlocal.auditlog['signal_duid']) + if hasattr(threadlocal, "auditlog"): + pre_save.disconnect( + sender=LogEntry, dispatch_uid=threadlocal.auditlog["signal_duid"] + ) return None @@ -62,15 +79,19 @@ class AuditlogMiddleware(MiddlewareMixin): Signal receiver with an extra, required 'user' kwarg. This method becomes a real (valid) signal receiver when it is curried with the actor. """ - if hasattr(threadlocal, 'auditlog'): - if signal_duid != threadlocal.auditlog['signal_duid']: + if hasattr(threadlocal, "auditlog"): + if signal_duid != threadlocal.auditlog["signal_duid"]: return try: - app_label, model_name = settings.AUTH_USER_MODEL.split('.') + app_label, model_name = settings.AUTH_USER_MODEL.split(".") auth_user_model = apps.get_model(app_label, model_name) except ValueError: - auth_user_model = apps.get_model('auth', 'user') - if sender == LogEntry and isinstance(user, auth_user_model) and instance.actor is None: + auth_user_model = apps.get_model("auth", "user") + if ( + sender == LogEntry + and isinstance(user, auth_user_model) + and instance.actor is None + ): instance.actor = user - instance.remote_addr = threadlocal.auditlog['remote_addr'] + instance.remote_addr = threadlocal.auditlog["remote_addr"] diff --git a/auditlog/migrations/0001_initial.py b/auditlog/migrations/0001_initial.py index 7abec05..0e2bd55 100644 --- a/auditlog/migrations/0001_initial.py +++ b/auditlog/migrations/0001_initial.py @@ -7,28 +7,71 @@ class Migration(migrations.Migration): dependencies = [ migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ('contenttypes', '0001_initial'), + ("contenttypes", "0001_initial"), ] operations = [ migrations.CreateModel( - name='LogEntry', + name="LogEntry", fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('object_pk', models.TextField(verbose_name='object pk')), - ('object_id', models.PositiveIntegerField(db_index=True, null=True, verbose_name='object id', blank=True)), - ('object_repr', models.TextField(verbose_name='object representation')), - ('action', models.PositiveSmallIntegerField(verbose_name='action', choices=[(0, 'create'), (1, 'update'), (2, 'delete')])), - ('changes', models.TextField(verbose_name='change message', blank=True)), - ('timestamp', models.DateTimeField(auto_now_add=True, verbose_name='timestamp')), - ('actor', models.ForeignKey(related_name='+', on_delete=django.db.models.deletion.SET_NULL, verbose_name='actor', blank=True, to=settings.AUTH_USER_MODEL, null=True)), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='+', verbose_name='content type', to='contenttypes.ContentType')), + ( + "id", + models.AutoField( + verbose_name="ID", + serialize=False, + auto_created=True, + primary_key=True, + ), + ), + ("object_pk", models.TextField(verbose_name="object pk")), + ( + "object_id", + models.PositiveIntegerField( + db_index=True, null=True, verbose_name="object id", blank=True + ), + ), + ("object_repr", models.TextField(verbose_name="object representation")), + ( + "action", + models.PositiveSmallIntegerField( + verbose_name="action", + choices=[(0, "create"), (1, "update"), (2, "delete")], + ), + ), + ( + "changes", + models.TextField(verbose_name="change message", blank=True), + ), + ( + "timestamp", + models.DateTimeField(auto_now_add=True, verbose_name="timestamp"), + ), + ( + "actor", + models.ForeignKey( + related_name="+", + on_delete=django.db.models.deletion.SET_NULL, + verbose_name="actor", + blank=True, + to=settings.AUTH_USER_MODEL, + null=True, + ), + ), + ( + "content_type", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + related_name="+", + verbose_name="content type", + to="contenttypes.ContentType", + ), + ), ], options={ - 'ordering': ['-timestamp'], - 'get_latest_by': 'timestamp', - 'verbose_name': 'log entry', - 'verbose_name_plural': 'log entries', + "ordering": ["-timestamp"], + "get_latest_by": "timestamp", + "verbose_name": "log entry", + "verbose_name_plural": "log entries", }, bases=(models.Model,), ), diff --git a/auditlog/migrations/0002_auto_support_long_primary_keys.py b/auditlog/migrations/0002_auto_support_long_primary_keys.py index e34b3c9..01c7d6e 100644 --- a/auditlog/migrations/0002_auto_support_long_primary_keys.py +++ b/auditlog/migrations/0002_auto_support_long_primary_keys.py @@ -4,13 +4,15 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0001_initial'), + ("auditlog", "0001_initial"), ] operations = [ migrations.AlterField( - model_name='logentry', - name='object_id', - field=models.BigIntegerField(db_index=True, null=True, verbose_name='object id', blank=True), + model_name="logentry", + name="object_id", + field=models.BigIntegerField( + db_index=True, null=True, verbose_name="object id", blank=True + ), ), ] diff --git a/auditlog/migrations/0003_logentry_remote_addr.py b/auditlog/migrations/0003_logentry_remote_addr.py index adf2c89..706dc4f 100644 --- a/auditlog/migrations/0003_logentry_remote_addr.py +++ b/auditlog/migrations/0003_logentry_remote_addr.py @@ -4,13 +4,15 @@ from django.db import models, migrations class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0002_auto_support_long_primary_keys'), + ("auditlog", "0002_auto_support_long_primary_keys"), ] operations = [ migrations.AddField( - model_name='logentry', - name='remote_addr', - field=models.GenericIPAddressField(null=True, verbose_name='remote address', blank=True), + model_name="logentry", + name="remote_addr", + field=models.GenericIPAddressField( + null=True, verbose_name="remote address", blank=True + ), ), ] diff --git a/auditlog/migrations/0004_logentry_detailed_object_repr.py b/auditlog/migrations/0004_logentry_detailed_object_repr.py index d6c8ee7..9e9deeb 100644 --- a/auditlog/migrations/0004_logentry_detailed_object_repr.py +++ b/auditlog/migrations/0004_logentry_detailed_object_repr.py @@ -5,13 +5,13 @@ import jsonfield.fields class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0003_logentry_remote_addr'), + ("auditlog", "0003_logentry_remote_addr"), ] operations = [ migrations.AddField( - model_name='logentry', - name='additional_data', + model_name="logentry", + name="additional_data", field=jsonfield.fields.JSONField(null=True, blank=True), ), ] diff --git a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py index 6554289..09e22d1 100644 --- a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py +++ b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py @@ -5,13 +5,15 @@ import jsonfield.fields class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0004_logentry_detailed_object_repr'), + ("auditlog", "0004_logentry_detailed_object_repr"), ] operations = [ migrations.AlterField( - model_name='logentry', - name='additional_data', - field=jsonfield.fields.JSONField(null=True, verbose_name='additional data', blank=True), + model_name="logentry", + name="additional_data", + field=jsonfield.fields.JSONField( + null=True, verbose_name="additional data", blank=True + ), ), ] diff --git a/auditlog/migrations/0006_object_pk_index.py b/auditlog/migrations/0006_object_pk_index.py index ac431c0..729ebe2 100644 --- a/auditlog/migrations/0006_object_pk_index.py +++ b/auditlog/migrations/0006_object_pk_index.py @@ -4,13 +4,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0005_logentry_additional_data_verbose_name'), + ("auditlog", "0005_logentry_additional_data_verbose_name"), ] operations = [ migrations.AlterField( - model_name='logentry', - name='object_pk', - field=models.CharField(verbose_name='object pk', max_length=255, db_index=True), + model_name="logentry", + name="object_pk", + field=models.CharField( + verbose_name="object pk", max_length=255, db_index=True + ), ), ] diff --git a/auditlog/migrations/0007_object_pk_type.py b/auditlog/migrations/0007_object_pk_type.py index 275db7e..d6514e4 100644 --- a/auditlog/migrations/0007_object_pk_type.py +++ b/auditlog/migrations/0007_object_pk_type.py @@ -4,13 +4,15 @@ from django.db import migrations, models class Migration(migrations.Migration): dependencies = [ - ('auditlog', '0006_object_pk_index'), + ("auditlog", "0006_object_pk_index"), ] operations = [ migrations.AlterField( - model_name='logentry', - name='object_pk', - field=models.CharField(verbose_name='object pk', max_length=255, db_index=True), + model_name="logentry", + name="object_pk", + field=models.CharField( + verbose_name="object pk", max_length=255, db_index=True + ), ), ] diff --git a/auditlog/mixins.py b/auditlog/mixins.py index 87225dd..a3f8f2e 100644 --- a/auditlog/mixins.py +++ b/auditlog/mixins.py @@ -12,62 +12,65 @@ MAX = 75 class LogEntryAdminMixin(object): - def created(self, obj): - return obj.timestamp.strftime('%Y-%m-%d %H:%M:%S') + return obj.timestamp.strftime("%Y-%m-%d %H:%M:%S") - created.short_description = 'Created' + created.short_description = "Created" def user_url(self, obj): if obj.actor: - app_label, model = settings.AUTH_USER_MODEL.split('.') - viewname = 'admin:%s_%s_change' % (app_label, model.lower()) + app_label, model = settings.AUTH_USER_MODEL.split(".") + viewname = "admin:%s_%s_change" % (app_label, model.lower()) try: link = urlresolvers.reverse(viewname, args=[obj.actor.pk]) except NoReverseMatch: - return u'%s' % (obj.actor) - return format_html(u'{}', link, obj.actor) + return "%s" % (obj.actor) + return format_html('{}', link, obj.actor) - return 'system' + return "system" - user_url.short_description = 'User' + user_url.short_description = "User" def resource_url(self, obj): app_label, model = obj.content_type.app_label, obj.content_type.model - viewname = 'admin:%s_%s_change' % (app_label, model) + viewname = "admin:%s_%s_change" % (app_label, model) try: args = [obj.object_pk] if obj.object_id is None else [obj.object_id] link = urlresolvers.reverse(viewname, args=args) except NoReverseMatch: return obj.object_repr else: - return format_html(u'{}', link, obj.object_repr) + return format_html('{}', link, obj.object_repr) - resource_url.short_description = 'Resource' + resource_url.short_description = "Resource" def msg_short(self, obj): if obj.action == LogEntry.Action.DELETE: - return '' # delete + return "" # delete changes = json.loads(obj.changes) - s = '' if len(changes) == 1 else 's' - fields = ', '.join(changes.keys()) + s = "" if len(changes) == 1 else "s" + fields = ", ".join(changes.keys()) if len(fields) > MAX: - i = fields.rfind(' ', 0, MAX) - fields = fields[:i] + ' ..' - return '%d change%s: %s' % (len(changes), s, fields) + i = fields.rfind(" ", 0, MAX) + fields = fields[:i] + " .." + return "%d change%s: %s" % (len(changes), s, fields) - msg_short.short_description = 'Changes' + msg_short.short_description = "Changes" def msg(self, obj): if obj.action == LogEntry.Action.DELETE: - return '' # delete + return "" # delete changes = json.loads(obj.changes) - msg = '' + msg = "
#FieldFromTo
" for i, field in enumerate(sorted(changes), 1): - value = [i, field] + (['***', '***'] if field == 'password' else changes[field]) - msg += format_html('', *value) + value = [i, field] + ( + ["***", "***"] if field == "password" else changes[field] + ) + msg += format_html( + "", *value + ) - msg += '
#FieldFromTo
{}{}{}{}
{}{}{}{}
' + msg += "" return mark_safe(msg) - msg.short_description = 'Changes' + msg.short_description = "Changes" diff --git a/auditlog/models.py b/auditlog/models.py index f04e562..c479acf 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -31,33 +31,49 @@ class LogEntryManager(models.Manager): :return: The new log entry or `None` if there were no changes. :rtype: LogEntry """ - changes = kwargs.get('changes', None) + changes = kwargs.get("changes", None) pk = self._get_pk_value(instance) if changes is not None: - kwargs.setdefault('content_type', ContentType.objects.get_for_model(instance)) - kwargs.setdefault('object_pk', pk) - kwargs.setdefault('object_repr', smart_str(instance)) + kwargs.setdefault( + "content_type", ContentType.objects.get_for_model(instance) + ) + kwargs.setdefault("object_pk", pk) + kwargs.setdefault("object_repr", smart_str(instance)) if isinstance(pk, int): - kwargs.setdefault('object_id', pk) + kwargs.setdefault("object_id", pk) - get_additional_data = getattr(instance, 'get_additional_data', None) + get_additional_data = getattr(instance, "get_additional_data", None) if callable(get_additional_data): - kwargs.setdefault('additional_data', get_additional_data()) + kwargs.setdefault("additional_data", get_additional_data()) # Delete log entries with the same pk as a newly created model. This should only be necessary when an pk is # used twice. - if kwargs.get('action', None) is LogEntry.Action.CREATE: - if kwargs.get('object_id', None) is not None and self.filter(content_type=kwargs.get('content_type'), - object_id=kwargs.get( - 'object_id')).exists(): - self.filter(content_type=kwargs.get('content_type'), object_id=kwargs.get('object_id')).delete() + if kwargs.get("action", None) is LogEntry.Action.CREATE: + if ( + kwargs.get("object_id", None) is not None + and self.filter( + content_type=kwargs.get("content_type"), + object_id=kwargs.get("object_id"), + ).exists() + ): + self.filter( + content_type=kwargs.get("content_type"), + object_id=kwargs.get("object_id"), + ).delete() else: - self.filter(content_type=kwargs.get('content_type'), object_pk=kwargs.get('object_pk', '')).delete() + self.filter( + content_type=kwargs.get("content_type"), + object_pk=kwargs.get("object_pk", ""), + ).delete() # save LogEntry to same database instance is using db = instance._state.db - return self.create(**kwargs) if db is None or db == '' else self.using(db).create(**kwargs) + return ( + self.create(**kwargs) + if db is None or db == "" + else self.using(db).create(**kwargs) + ) return None def get_for_object(self, instance): @@ -94,15 +110,29 @@ class LogEntryManager(models.Manager): return self.none() content_type = ContentType.objects.get_for_model(queryset.model) - primary_keys = list(queryset.values_list(queryset.model._meta.pk.name, flat=True)) + primary_keys = list( + queryset.values_list(queryset.model._meta.pk.name, flat=True) + ) if isinstance(primary_keys[0], int): - return self.filter(content_type=content_type).filter(Q(object_id__in=primary_keys)).distinct() + return ( + self.filter(content_type=content_type) + .filter(Q(object_id__in=primary_keys)) + .distinct() + ) elif isinstance(queryset.model._meta.pk, models.UUIDField): primary_keys = [smart_str(pk) for pk in primary_keys] - return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct() + return ( + self.filter(content_type=content_type) + .filter(Q(object_pk__in=primary_keys)) + .distinct() + ) else: - return self.filter(content_type=content_type).filter(Q(object_pk__in=primary_keys)).distinct() + return ( + self.filter(content_type=content_type) + .filter(Q(object_pk__in=primary_keys)) + .distinct() + ) def get_for_model(self, model): """ @@ -158,6 +188,7 @@ class LogEntry(models.Model): The valid actions are :py:attr:`Action.CREATE`, :py:attr:`Action.UPDATE` and :py:attr:`Action.DELETE`. """ + CREATE = 0 UPDATE = 1 DELETE = 2 @@ -168,24 +199,44 @@ class LogEntry(models.Model): (DELETE, _("delete")), ) - content_type = models.ForeignKey(to='contenttypes.ContentType', on_delete=models.CASCADE, related_name='+', - verbose_name=_("content type")) - object_pk = models.CharField(db_index=True, max_length=255, verbose_name=_("object pk")) - object_id = models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name=_("object id")) + content_type = models.ForeignKey( + to="contenttypes.ContentType", + on_delete=models.CASCADE, + related_name="+", + verbose_name=_("content type"), + ) + object_pk = models.CharField( + db_index=True, max_length=255, verbose_name=_("object pk") + ) + object_id = models.BigIntegerField( + blank=True, db_index=True, null=True, verbose_name=_("object id") + ) object_repr = models.TextField(verbose_name=_("object representation")) - action = models.PositiveSmallIntegerField(choices=Action.choices, verbose_name=_("action")) + action = models.PositiveSmallIntegerField( + choices=Action.choices, verbose_name=_("action") + ) changes = models.TextField(blank=True, verbose_name=_("change message")) - actor = models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, blank=True, null=True, - related_name='+', verbose_name=_("actor")) - remote_addr = models.GenericIPAddressField(blank=True, null=True, verbose_name=_("remote address")) + actor = models.ForeignKey( + to=settings.AUTH_USER_MODEL, + on_delete=models.SET_NULL, + blank=True, + null=True, + related_name="+", + verbose_name=_("actor"), + ) + remote_addr = models.GenericIPAddressField( + blank=True, null=True, verbose_name=_("remote address") + ) timestamp = models.DateTimeField(auto_now_add=True, verbose_name=_("timestamp")) - additional_data = JSONField(blank=True, null=True, verbose_name=_("additional data")) + additional_data = JSONField( + blank=True, null=True, verbose_name=_("additional data") + ) objects = LogEntryManager() class Meta: - get_latest_by = 'timestamp' - ordering = ['-timestamp'] + get_latest_by = "timestamp" + ordering = ["-timestamp"] verbose_name = _("log entry") verbose_name_plural = _("log entries") @@ -212,7 +263,7 @@ class LogEntry(models.Model): return {} @property - def changes_str(self, colon=': ', arrow=' \u2192 ', separator='; '): + def changes_str(self, colon=": ", arrow=" \u2192 ", separator="; "): """ Return the changes recorded in this log entry as a string. The formatting of the string can be customized by setting alternate values for colon, arrow and separator. If the formatting is still not satisfying, please use @@ -226,7 +277,7 @@ class LogEntry(models.Model): substrings = [] for field, values in self.changes_dict.items(): - substring = '{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}'.format( + substring = "{field_name:s}{colon:s}{old:s}{arrow:s}{new:s}".format( field_name=field, colon=colon, old=values[0], @@ -244,6 +295,7 @@ class LogEntry(models.Model): """ # Get the model and model_fields from auditlog.registry import auditlog + model = self.content_type.model_class() model_fields = auditlog.get_model_fields(model._meta.model) changes_display_dict = {} @@ -258,9 +310,14 @@ class LogEntry(models.Model): values_display = [] # handle choices fields and Postgres ArrayField to get human readable version choices_dict = None - if getattr(field, 'choices') and len(field.choices) > 0: + if getattr(field, "choices") and len(field.choices) > 0: choices_dict = dict(field.choices) - if hasattr(field, 'base_field') and isinstance(field.base_field, Field) and getattr(field.base_field, 'choices') and len(field.base_field.choices) > 0: + if ( + hasattr(field, "base_field") + and isinstance(field.base_field, Field) + and getattr(field.base_field, "choices") + and len(field.base_field.choices) > 0 + ): choices_dict = dict(field.base_field.choices) if choices_dict: @@ -268,13 +325,17 @@ class LogEntry(models.Model): try: value = ast.literal_eval(value) if type(value) is [].__class__: - values_display.append(', '.join([choices_dict.get(val, 'None') for val in value])) + values_display.append( + ", ".join( + [choices_dict.get(val, "None") for val in value] + ) + ) else: - values_display.append(choices_dict.get(value, 'None')) + values_display.append(choices_dict.get(value, "None")) except ValueError: - values_display.append(choices_dict.get(value, 'None')) + values_display.append(choices_dict.get(value, "None")) except: - values_display.append(choices_dict.get(value, 'None')) + values_display.append(choices_dict.get(value, "None")) else: try: field_type = field.get_internal_type() @@ -301,7 +362,9 @@ class LogEntry(models.Model): value = "{}...".format(value[:140]) values_display.append(value) - verbose_name = model_fields['mapping_fields'].get(field.name, getattr(field, 'verbose_name', field.name)) + verbose_name = model_fields["mapping_fields"].get( + field.name, getattr(field, "verbose_name", field.name) + ) changes_display_dict[verbose_name] = values_display return changes_display_dict @@ -327,14 +390,14 @@ class AuditlogHistoryField(GenericRelation): """ def __init__(self, pk_indexable=True, delete_related=True, **kwargs): - kwargs['to'] = LogEntry + kwargs["to"] = LogEntry if pk_indexable: - kwargs['object_id_field'] = 'object_id' + kwargs["object_id_field"] = "object_id" else: - kwargs['object_id_field'] = 'object_pk' + kwargs["object_id_field"] = "object_pk" - kwargs['content_type_field'] = 'content_type' + kwargs["content_type_field"] = "content_type" self.delete_related = delete_related super(AuditlogHistoryField, self).__init__(**kwargs) @@ -356,6 +419,8 @@ try: from south.modelsinspector import add_introspection_rules add_introspection_rules([], ["^auditlog\.models\.AuditlogHistoryField"]) - raise DeprecationWarning("South support will be dropped in django-auditlog 0.4.0 or later.") + raise DeprecationWarning( + "South support will be dropped in django-auditlog 0.4.0 or later." + ) except ImportError: pass diff --git a/auditlog/registry.py b/auditlog/registry.py index 538f5dd..d4a520a 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -12,8 +12,13 @@ class AuditlogModelRegistry(object): A registry that keeps track of the models that use Auditlog to track changes. """ - def __init__(self, create: bool = True, update: bool = True, delete: bool = True, - custom: Optional[Dict[ModelSignal, Callable]] = None): + def __init__( + self, + create: bool = True, + update: bool = True, + delete: bool = True, + custom: Optional[Dict[ModelSignal, Callable]] = None, + ): from auditlog.receivers import log_create, log_update, log_delete self._registry = {} @@ -29,8 +34,13 @@ class AuditlogModelRegistry(object): if custom is not None: self._signals.update(custom) - def register(self, model: ModelBase = None, include_fields: Optional[List[str]] = None, - exclude_fields: Optional[List[str]] = None, mapping_fields: Optional[Dict[str, str]] = None): + def register( + self, + model: ModelBase = None, + include_fields: Optional[List[str]] = None, + exclude_fields: Optional[List[str]] = None, + mapping_fields: Optional[Dict[str, str]] = None, + ): """ Register a model with auditlog. Auditlog will then track mutations on this model's instances. @@ -54,9 +64,9 @@ class AuditlogModelRegistry(object): raise TypeError("Supplied model is not a valid model.") self._registry[cls] = { - 'include_fields': include_fields, - 'exclude_fields': exclude_fields, - 'mapping_fields': mapping_fields, + "include_fields": include_fields, + "exclude_fields": exclude_fields, + "mapping_fields": mapping_fields, } self._connect_signals(cls) @@ -101,9 +111,9 @@ class AuditlogModelRegistry(object): def get_model_fields(self, model: ModelBase): return { - 'include_fields': list(self._registry[model]['include_fields']), - 'exclude_fields': list(self._registry[model]['exclude_fields']), - 'mapping_fields': dict(self._registry[model]['mapping_fields']), + "include_fields": list(self._registry[model]["include_fields"]), + "exclude_fields": list(self._registry[model]["exclude_fields"]), + "mapping_fields": dict(self._registry[model]["mapping_fields"]), } def _connect_signals(self, model): @@ -112,14 +122,18 @@ class AuditlogModelRegistry(object): """ for signal in self._signals: receiver = self._signals[signal] - signal.connect(receiver, sender=model, dispatch_uid=self._dispatch_uid(signal, model)) + signal.connect( + receiver, sender=model, dispatch_uid=self._dispatch_uid(signal, model) + ) def _disconnect_signals(self, model): """ Disconnect signals for the model. """ for signal, receiver in self._signals.items(): - signal.disconnect(sender=model, dispatch_uid=self._dispatch_uid(signal, model)) + signal.disconnect( + sender=model, dispatch_uid=self._dispatch_uid(signal, model) + ) def _dispatch_uid(self, signal, model) -> DispatchUID: """ diff --git a/auditlog_tests/__init__.py b/auditlog_tests/__init__.py index 1f12d80..fa61512 100644 --- a/auditlog_tests/__init__.py +++ b/auditlog_tests/__init__.py @@ -1 +1 @@ -default_app_config = 'auditlog_tests.apps.AuditlogTestConfig' +default_app_config = "auditlog_tests.apps.AuditlogTestConfig" diff --git a/auditlog_tests/apps.py b/auditlog_tests/apps.py index 85148cb..2bcc080 100644 --- a/auditlog_tests/apps.py +++ b/auditlog_tests/apps.py @@ -2,4 +2,4 @@ from django.apps import AppConfig class AuditlogTestConfig(AppConfig): - name = 'auditlog_tests' + name = "auditlog_tests" diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index 4eda784..a3b56b3 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -64,7 +64,7 @@ class RelatedModel(models.Model): A model with a foreign key. """ - related = models.ForeignKey(to='self', on_delete=models.CASCADE) + related = models.ForeignKey(to="self", on_delete=models.CASCADE) history = AuditlogHistoryField() @@ -74,12 +74,12 @@ class ManyRelatedModel(models.Model): A model with a many to many relation. """ - related = models.ManyToManyField('self') + related = models.ManyToManyField("self") history = AuditlogHistoryField() -@auditlog.register(include_fields=['label']) +@auditlog.register(include_fields=["label"]) class SimpleIncludeModel(models.Model): """ A simple model used for register's include_fields kwarg @@ -108,7 +108,7 @@ class SimpleMappingModel(models.Model): """ sku = models.CharField(max_length=100) - vtxt = models.CharField(verbose_name='Version', max_length=100) + vtxt = models.CharField(verbose_name="Version", max_length=100) not_mapped = models.CharField(max_length=100) history = AuditlogHistoryField() @@ -133,8 +133,8 @@ class AdditionalDataIncludedModel(models.Model): manager and added to each logentry instance on creation. """ object_details = { - 'related_model_id': self.related.id, - 'related_model_text': self.related.text + "related_model_id": self.related.id, + "related_model_text": self.related.text, } return object_details @@ -144,6 +144,7 @@ class DateTimeFieldModel(models.Model): A model with a DateTimeField, used to test DateTimeField changes are detected properly. """ + label = models.CharField(max_length=100) timestamp = models.DateTimeField() date = models.DateField() @@ -158,14 +159,15 @@ class ChoicesFieldModel(models.Model): A model with a CharField restricted to a set of choices. This model is used to test the changes_display_dict method. """ - RED = 'r' - YELLOW = 'y' - GREEN = 'g' + + RED = "r" + YELLOW = "y" + GREEN = "g" STATUS_CHOICES = ( - (RED, 'Red'), - (YELLOW, 'Yellow'), - (GREEN, 'Green'), + (RED, "Red"), + (YELLOW, "Yellow"), + (GREEN, "Green"), ) status = models.CharField(max_length=1, choices=STATUS_CHOICES) @@ -191,17 +193,20 @@ class PostgresArrayFieldModel(models.Model): """ Test auditlog with Postgres's ArrayField """ - RED = 'r' - YELLOW = 'y' - GREEN = 'g' + + RED = "r" + YELLOW = "y" + GREEN = "g" STATUS_CHOICES = ( - (RED, 'Red'), - (YELLOW, 'Yellow'), - (GREEN, 'Green'), + (RED, "Red"), + (YELLOW, "Yellow"), + (GREEN, "Green"), ) - arrayfield = ArrayField(models.CharField(max_length=1, choices=STATUS_CHOICES), size=3) + arrayfield = ArrayField( + models.CharField(max_length=1, choices=STATUS_CHOICES), size=3 + ) history = AuditlogHistoryField() @@ -218,8 +223,8 @@ auditlog.register(ProxyModel) auditlog.register(RelatedModel) auditlog.register(ManyRelatedModel) auditlog.register(ManyRelatedModel.related.through) -auditlog.register(SimpleExcludeModel, exclude_fields=['text']) -auditlog.register(SimpleMappingModel, mapping_fields={'sku': 'Product No.'}) +auditlog.register(SimpleExcludeModel, exclude_fields=["text"]) +auditlog.register(SimpleMappingModel, mapping_fields={"sku": "Product No."}) auditlog.register(AdditionalDataIncludedModel) auditlog.register(DateTimeFieldModel) auditlog.register(ChoicesFieldModel) diff --git a/auditlog_tests/test_settings.py b/auditlog_tests/test_settings.py index eb05b48..e2a036a 100644 --- a/auditlog_tests/test_settings.py +++ b/auditlog_tests/test_settings.py @@ -5,55 +5,55 @@ import os DEBUG = True -SECRET_KEY = 'test' +SECRET_KEY = "test" INSTALLED_APPS = [ - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.messages', - 'django.contrib.sessions', - 'django.contrib.admin', - 'django.contrib.staticfiles', - 'auditlog', - 'auditlog_tests', + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.messages", + "django.contrib.sessions", + "django.contrib.admin", + "django.contrib.staticfiles", + "auditlog", + "auditlog_tests", ] MIDDLEWARE = ( - 'django.middleware.common.CommonMiddleware', - 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.auth.middleware.AuthenticationMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', - 'auditlog.middleware.AuditlogMiddleware', + "django.middleware.common.CommonMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "auditlog.middleware.AuditlogMiddleware", ) DATABASES = { - 'default': { - 'ENGINE': 'django.db.backends.postgresql', - 'NAME': os.getenv('TEST_DB_NAME', 'auditlog_tests_db'), - 'USER': os.getenv('TEST_DB_USER', 'postgres'), - 'PASSWORD': os.getenv('TEST_DB_PASS', ''), - 'HOST': os.getenv('TEST_DB_HOST', '127.0.0.1'), - 'PORT': os.getenv('TEST_DB_PORT', '5432'), + "default": { + "ENGINE": "django.db.backends.postgresql", + "NAME": os.getenv("TEST_DB_NAME", "auditlog_tests_db"), + "USER": os.getenv("TEST_DB_USER", "postgres"), + "PASSWORD": os.getenv("TEST_DB_PASS", ""), + "HOST": os.getenv("TEST_DB_HOST", "127.0.0.1"), + "PORT": os.getenv("TEST_DB_PORT", "5432"), } } TEMPLATES = [ { - 'APP_DIRS': True, - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'OPTIONS': { - 'context_processors': [ - 'django.template.context_processors.request', - 'django.contrib.auth.context_processors.auth', - 'django.contrib.messages.context_processors.messages', + "APP_DIRS": True, + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", ] }, }, ] -STATIC_URL = '/static/' +STATIC_URL = "/static/" -ROOT_URLCONF = 'auditlog_tests.urls' +ROOT_URLCONF = "auditlog_tests.urls" USE_TZ = True diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 4dd68d7..b4c5c02 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -14,15 +14,28 @@ from dateutil.tz import gettz from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry from auditlog.registry import auditlog -from auditlog_tests.models import SimpleModel, AltPrimaryKeyModel, UUIDPrimaryKeyModel, \ - ProxyModel, SimpleIncludeModel, SimpleExcludeModel, SimpleMappingModel, RelatedModel, \ - ManyRelatedModel, AdditionalDataIncludedModel, DateTimeFieldModel, ChoicesFieldModel, \ - CharfieldTextfieldModel, PostgresArrayFieldModel, NoDeleteHistoryModel +from auditlog_tests.models import ( + SimpleModel, + AltPrimaryKeyModel, + UUIDPrimaryKeyModel, + ProxyModel, + SimpleIncludeModel, + SimpleExcludeModel, + SimpleMappingModel, + RelatedModel, + ManyRelatedModel, + AdditionalDataIncludedModel, + DateTimeFieldModel, + ChoicesFieldModel, + CharfieldTextfieldModel, + PostgresArrayFieldModel, + NoDeleteHistoryModel, +) class SimpleModelTest(TestCase): def setUp(self): - self.obj = SimpleModel.objects.create(text='I am not difficult.') + self.obj = SimpleModel.objects.create(text="I am not difficult.") def test_create(self): """Creation is logged correctly.""" @@ -37,8 +50,12 @@ class SimpleModelTest(TestCase): except obj.history.DoesNotExist: self.assertTrue(False, "Log entry exists") else: - self.assertEqual(history.action, LogEntry.Action.CREATE, msg="Action is 'CREATE'") - self.assertEqual(history.object_repr, str(obj), msg="Representation is equal") + self.assertEqual( + history.action, LogEntry.Action.CREATE, msg="Action is 'CREATE'" + ) + self.assertEqual( + history.object_repr, str(obj), msg="Representation is equal" + ) def test_update(self): """Updates are logged correctly.""" @@ -50,11 +67,18 @@ class SimpleModelTest(TestCase): obj.save() # Check for log entries - self.assertTrue(obj.history.filter(action=LogEntry.Action.UPDATE).count() == 1, msg="There is one log entry for 'UPDATE'") + self.assertTrue( + obj.history.filter(action=LogEntry.Action.UPDATE).count() == 1, + msg="There is one log entry for 'UPDATE'", + ) history = obj.history.get(action=LogEntry.Action.UPDATE) - self.assertJSONEqual(history.changes, '{"boolean": ["False", "True"]}', msg="The change is correctly logged") + self.assertJSONEqual( + history.changes, + '{"boolean": ["False", "True"]}', + msg="The change is correctly logged", + ) def test_delete(self): """Deletion is logged correctly.""" @@ -67,7 +91,15 @@ class SimpleModelTest(TestCase): obj.delete() # Check for log entries - self.assertTrue(LogEntry.objects.filter(content_type=history.content_type, object_pk=history.object_pk, action=LogEntry.Action.DELETE).count() == 1, msg="There is one log entry for 'DELETE'") + self.assertTrue( + LogEntry.objects.filter( + content_type=history.content_type, + object_pk=history.object_pk, + action=LogEntry.Action.DELETE, + ).count() + == 1, + msg="There is one log entry for 'DELETE'", + ) def test_recreate(self): SimpleModel.objects.all().delete() @@ -77,12 +109,14 @@ class SimpleModelTest(TestCase): class AltPrimaryKeyModelTest(SimpleModelTest): def setUp(self): - self.obj = AltPrimaryKeyModel.objects.create(key=str(datetime.datetime.now()), text='I am strange.') + self.obj = AltPrimaryKeyModel.objects.create( + key=str(datetime.datetime.now()), text="I am strange." + ) class UUIDPrimaryKeyModelModelTest(SimpleModelTest): def setUp(self): - self.obj = UUIDPrimaryKeyModel.objects.create(text='I am strange.') + self.obj = UUIDPrimaryKeyModel.objects.create(text="I am strange.") def test_get_for_object(self): self.obj.boolean = True @@ -94,41 +128,54 @@ class UUIDPrimaryKeyModelModelTest(SimpleModelTest): self.obj.boolean = True self.obj.save() - self.assertEqual(LogEntry.objects.get_for_objects(UUIDPrimaryKeyModel.objects.all()).count(), 2) + self.assertEqual( + LogEntry.objects.get_for_objects(UUIDPrimaryKeyModel.objects.all()).count(), + 2, + ) class ProxyModelTest(SimpleModelTest): def setUp(self): - self.obj = ProxyModel.objects.create(text='I am not what you think.') + self.obj = ProxyModel.objects.create(text="I am not what you think.") class ManyRelatedModelTest(TestCase): """ Test the behaviour of a many-to-many relationship. """ + def setUp(self): self.obj = ManyRelatedModel.objects.create() self.rel_obj = ManyRelatedModel.objects.create() self.obj.related.add(self.rel_obj) def test_related(self): - self.assertEqual(LogEntry.objects.get_for_objects(self.obj.related.all()).count(), self.rel_obj.history.count()) - self.assertEqual(LogEntry.objects.get_for_objects(self.obj.related.all()).first(), self.rel_obj.history.first()) + self.assertEqual( + LogEntry.objects.get_for_objects(self.obj.related.all()).count(), + self.rel_obj.history.count(), + ) + self.assertEqual( + LogEntry.objects.get_for_objects(self.obj.related.all()).first(), + self.rel_obj.history.first(), + ) class MiddlewareTest(TestCase): """ Test the middleware responsible for connecting and disconnecting the signals used in automatic logging. """ + def setUp(self): self.middleware = AuditlogMiddleware() self.factory = RequestFactory() - self.user = User.objects.create_user(username='test', email='test@example.com', password='top_secret') + self.user = User.objects.create_user( + username="test", email="test@example.com", password="top_secret" + ) def test_request_anonymous(self): """No actor will be logged when a user is not logged in.""" # Create a request - request = self.factory.get('/') + request = self.factory.get("/") request.user = AnonymousUser() # Run middleware @@ -143,7 +190,7 @@ class MiddlewareTest(TestCase): def test_request(self): """The actor will be logged when a user is logged in.""" # Create a request - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user # Run middleware self.middleware.process_request(request) @@ -157,12 +204,14 @@ class MiddlewareTest(TestCase): def test_response(self): """The signal will be disconnected when the request is processed.""" # Create a request - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user # Run middleware self.middleware.process_request(request) - self.assertTrue(pre_save.has_listeners(LogEntry)) # The signal should be present before trying to disconnect it. + self.assertTrue( + pre_save.has_listeners(LogEntry) + ) # The signal should be present before trying to disconnect it. self.middleware.process_response(request, HttpResponse()) # Validate result @@ -171,12 +220,14 @@ class MiddlewareTest(TestCase): def test_exception(self): """The signal will be disconnected when an exception is raised.""" # Create a request - request = self.factory.get('/') + request = self.factory.get("/") request.user = self.user # Run middleware self.middleware.process_request(request) - self.assertTrue(pre_save.has_listeners(LogEntry)) # The signal should be present before trying to disconnect it. + self.assertTrue( + pre_save.has_listeners(LogEntry) + ) # The signal should be present before trying to disconnect it. self.middleware.process_exception(request, ValidationError("Test")) # Validate result @@ -187,17 +238,17 @@ class SimpeIncludeModelTest(TestCase): """Log only changes in include_fields""" def test_register_include_fields(self): - sim = SimpleIncludeModel(label='Include model', text='Looong text') + sim = SimpleIncludeModel(label="Include model", text="Looong text") sim.save() self.assertTrue(sim.history.count() == 1, msg="There is one log entry") # Change label, record - sim.label = 'Changed label' + sim.label = "Changed label" sim.save() self.assertTrue(sim.history.count() == 2, msg="There are two log entries") # Change text, ignore - sim.text = 'Short text' + sim.text = "Short text" sim.save() self.assertTrue(sim.history.count() == 2, msg="There are two log entries") @@ -206,17 +257,17 @@ class SimpeExcludeModelTest(TestCase): """Log only changes that are not in exclude_fields""" def test_register_exclude_fields(self): - sem = SimpleExcludeModel(label='Exclude model', text='Looong text') + sem = SimpleExcludeModel(label="Exclude model", text="Looong text") sem.save() self.assertTrue(sem.history.count() == 1, msg="There is one log entry") # Change label, ignore - sem.label = 'Changed label' + sem.label = "Changed label" sem.save() self.assertTrue(sem.history.count() == 2, msg="There are two log entries") # Change text, record - sem.text = 'Short text' + sem.text = "Short text" sem.save() self.assertTrue(sem.history.count() == 2, msg="There are two log entries") @@ -225,38 +276,58 @@ class SimpleMappingModelTest(TestCase): """Diff displays fields as mapped field names where available through mapping_fields""" def test_register_mapping_fields(self): - smm = SimpleMappingModel(sku='ASD301301A6', vtxt='2.1.5', not_mapped='Not mapped') + smm = SimpleMappingModel( + sku="ASD301301A6", vtxt="2.1.5", not_mapped="Not mapped" + ) smm.save() - self.assertTrue(smm.history.latest().changes_dict['sku'][1] == 'ASD301301A6', - msg="The diff function retains 'sku' and can be retrieved.") - self.assertTrue(smm.history.latest().changes_dict['not_mapped'][1] == 'Not mapped', - msg="The diff function does not map 'not_mapped' and can be retrieved.") - self.assertTrue(smm.history.latest().changes_display_dict['Product No.'][1] == 'ASD301301A6', - msg="The diff function maps 'sku' as 'Product No.' and can be retrieved.") - self.assertTrue(smm.history.latest().changes_display_dict['Version'][1] == '2.1.5', - msg=("The diff function maps 'vtxt' as 'Version' through verbose_name" - " setting on the model field and can be retrieved.")) - self.assertTrue(smm.history.latest().changes_display_dict['not mapped'][1] == 'Not mapped', - msg=("The diff function uses the django default verbose name for 'not_mapped'" - " and can be retrieved.")) + self.assertTrue( + smm.history.latest().changes_dict["sku"][1] == "ASD301301A6", + msg="The diff function retains 'sku' and can be retrieved.", + ) + self.assertTrue( + smm.history.latest().changes_dict["not_mapped"][1] == "Not mapped", + msg="The diff function does not map 'not_mapped' and can be retrieved.", + ) + self.assertTrue( + smm.history.latest().changes_display_dict["Product No."][1] + == "ASD301301A6", + msg="The diff function maps 'sku' as 'Product No.' and can be retrieved.", + ) + self.assertTrue( + smm.history.latest().changes_display_dict["Version"][1] == "2.1.5", + msg=( + "The diff function maps 'vtxt' as 'Version' through verbose_name" + " setting on the model field and can be retrieved." + ), + ) + self.assertTrue( + smm.history.latest().changes_display_dict["not mapped"][1] == "Not mapped", + msg=( + "The diff function uses the django default verbose name for 'not_mapped'" + " and can be retrieved." + ), + ) class AdditionalDataModelTest(TestCase): """Log additional data if get_additional_data is defined in the model""" def test_model_without_additional_data(self): - obj_wo_additional_data = SimpleModel.objects.create(text='No additional ' - 'data') + obj_wo_additional_data = SimpleModel.objects.create( + text="No additional " "data" + ) obj_log_entry = obj_wo_additional_data.history.get() self.assertIsNone(obj_log_entry.additional_data) def test_model_with_additional_data(self): - related_model = SimpleModel.objects.create(text='Log my reference') + related_model = SimpleModel.objects.create(text="Log my reference") obj_with_additional_data = AdditionalDataIncludedModel( - label='Additional data to log entries', related=related_model) + label="Additional data to log entries", related=related_model + ) obj_with_additional_data.save() - self.assertTrue(obj_with_additional_data.history.count() == 1, - msg="There is 1 log entry") + self.assertTrue( + obj_with_additional_data.history.count() == 1, msg="There is 1 log entry" + ) log_entry = obj_with_additional_data.history.get() # FIXME: Work-around for the fact that additional_data isn't working # on Django 3.1 correctly (see https://github.com/jazzband/django-auditlog/issues/266) @@ -265,10 +336,14 @@ class AdditionalDataModelTest(TestCase): else: extra_data = log_entry.additional_data self.assertIsNotNone(extra_data) - self.assertTrue(extra_data['related_model_text'] == related_model.text, - msg="Related model's text is logged") - self.assertTrue(extra_data['related_model_id'] == related_model.id, - msg="Related model's id is logged") + self.assertTrue( + extra_data["related_model_text"] == related_model.text, + msg="Related model's text is logged", + ) + self.assertTrue( + extra_data["related_model_id"] == related_model.id, + msg="Related model's id is logged", + ) class DateTimeFieldModelTest(TestCase): @@ -281,7 +356,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -299,7 +380,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -315,7 +402,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -331,7 +424,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -347,7 +446,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -363,7 +468,13 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") @@ -379,85 +490,144 @@ class DateTimeFieldModelTest(TestCase): timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() localized_timestamp = timestamp.astimezone(gettz(settings.TIME_ZONE)) - self.assertTrue(dtm.history.latest().changes_display_dict["timestamp"][1] == \ - dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), - msg=("The datetime should be formatted according to Django's settings for" - " DATETIME_FORMAT")) + self.assertTrue( + dtm.history.latest().changes_display_dict["timestamp"][1] + == dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), + msg=( + "The datetime should be formatted according to Django's settings for" + " DATETIME_FORMAT" + ), + ) timestamp = timezone.now() dtm.timestamp = timestamp dtm.save() localized_timestamp = timestamp.astimezone(gettz(settings.TIME_ZONE)) - self.assertTrue(dtm.history.latest().changes_display_dict["timestamp"][1] == \ - dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), - msg=("The datetime should be formatted according to Django's settings for" - " DATETIME_FORMAT")) + self.assertTrue( + dtm.history.latest().changes_display_dict["timestamp"][1] + == dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), + msg=( + "The datetime should be formatted according to Django's settings for" + " DATETIME_FORMAT" + ), + ) # Change USE_L10N = True - with self.settings(USE_L10N=True, LANGUAGE_CODE='en-GB'): - self.assertTrue(dtm.history.latest().changes_display_dict["timestamp"][1] == \ - formats.localize(localized_timestamp), - msg=("The datetime should be formatted according to Django's settings for" - " USE_L10N is True with a different LANGUAGE_CODE.")) - + with self.settings(USE_L10N=True, LANGUAGE_CODE="en-GB"): + self.assertTrue( + dtm.history.latest().changes_display_dict["timestamp"][1] + == formats.localize(localized_timestamp), + msg=( + "The datetime should be formatted according to Django's settings for" + " USE_L10N is True with a different LANGUAGE_CODE." + ), + ) def test_changes_display_dict_date(self): timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() - self.assertTrue(dtm.history.latest().changes_display_dict["date"][1] == \ - dateformat.format(date, settings.DATE_FORMAT), - msg=("The date should be formatted according to Django's settings for" - " DATE_FORMAT unless USE_L10N is True.")) + self.assertTrue( + dtm.history.latest().changes_display_dict["date"][1] + == dateformat.format(date, settings.DATE_FORMAT), + msg=( + "The date should be formatted according to Django's settings for" + " DATE_FORMAT unless USE_L10N is True." + ), + ) date = datetime.date(2017, 1, 11) dtm.date = date dtm.save() - self.assertTrue(dtm.history.latest().changes_display_dict["date"][1] == \ - dateformat.format(date, settings.DATE_FORMAT), - msg=("The date should be formatted according to Django's settings for" - " DATE_FORMAT unless USE_L10N is True.")) + self.assertTrue( + dtm.history.latest().changes_display_dict["date"][1] + == dateformat.format(date, settings.DATE_FORMAT), + msg=( + "The date should be formatted according to Django's settings for" + " DATE_FORMAT unless USE_L10N is True." + ), + ) # Change USE_L10N = True - with self.settings(USE_L10N=True, LANGUAGE_CODE='en-GB'): - self.assertTrue(dtm.history.latest().changes_display_dict["date"][1] == \ - formats.localize(date), - msg=("The date should be formatted according to Django's settings for" - " USE_L10N is True with a different LANGUAGE_CODE.")) + with self.settings(USE_L10N=True, LANGUAGE_CODE="en-GB"): + self.assertTrue( + dtm.history.latest().changes_display_dict["date"][1] + == formats.localize(date), + msg=( + "The date should be formatted according to Django's settings for" + " USE_L10N is True with a different LANGUAGE_CODE." + ), + ) def test_changes_display_dict_time(self): timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() - self.assertTrue(dtm.history.latest().changes_display_dict["time"][1] == \ - dateformat.format(time, settings.TIME_FORMAT), - msg=("The time should be formatted according to Django's settings for" - " TIME_FORMAT unless USE_L10N is True.")) + self.assertTrue( + dtm.history.latest().changes_display_dict["time"][1] + == dateformat.format(time, settings.TIME_FORMAT), + msg=( + "The time should be formatted according to Django's settings for" + " TIME_FORMAT unless USE_L10N is True." + ), + ) time = datetime.time(6, 0) dtm.time = time dtm.save() - self.assertTrue(dtm.history.latest().changes_display_dict["time"][1] == \ - dateformat.format(time, settings.TIME_FORMAT), - msg=("The time should be formatted according to Django's settings for" - " TIME_FORMAT unless USE_L10N is True.")) + self.assertTrue( + dtm.history.latest().changes_display_dict["time"][1] + == dateformat.format(time, settings.TIME_FORMAT), + msg=( + "The time should be formatted according to Django's settings for" + " TIME_FORMAT unless USE_L10N is True." + ), + ) # Change USE_L10N = True - with self.settings(USE_L10N=True, LANGUAGE_CODE='en-GB'): - self.assertTrue(dtm.history.latest().changes_display_dict["time"][1] == \ - formats.localize(time), - msg=("The time should be formatted according to Django's settings for" - " USE_L10N is True with a different LANGUAGE_CODE.")) + with self.settings(USE_L10N=True, LANGUAGE_CODE="en-GB"): + self.assertTrue( + dtm.history.latest().changes_display_dict["time"][1] + == formats.localize(time), + msg=( + "The time should be formatted according to Django's settings for" + " USE_L10N is True with a different LANGUAGE_CODE." + ), + ) def test_update_naive_dt(self): timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) date = datetime.date(2017, 1, 10) time = datetime.time(12, 0) - dtm = DateTimeFieldModel(label='DateTimeField model', timestamp=timestamp, date=date, time=time, naive_dt=self.now) + dtm = DateTimeFieldModel( + label="DateTimeField model", + timestamp=timestamp, + date=date, + time=time, + naive_dt=self.now, + ) dtm.save() # Change with naive field doesnt raise error @@ -468,7 +638,7 @@ class DateTimeFieldModelTest(TestCase): class UnregisterTest(TestCase): def setUp(self): auditlog.unregister(SimpleModel) - self.obj = SimpleModel.objects.create(text='No history') + self.obj = SimpleModel.objects.create(text="No history") def tearDown(self): # Re-register for future tests @@ -507,32 +677,45 @@ class UnregisterTest(TestCase): class ChoicesFieldModelTest(TestCase): - def setUp(self): self.obj = ChoicesFieldModel.objects.create( status=ChoicesFieldModel.RED, - multiplechoice=[ChoicesFieldModel.RED, ChoicesFieldModel.YELLOW, ChoicesFieldModel.GREEN], + multiplechoice=[ + ChoicesFieldModel.RED, + ChoicesFieldModel.YELLOW, + ChoicesFieldModel.GREEN, + ], ) def test_changes_display_dict_single_choice(self): - self.assertTrue(self.obj.history.latest().changes_display_dict["status"][1] == "Red", - msg="The human readable text 'Red' is displayed.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["status"][1] == "Red", + msg="The human readable text 'Red' is displayed.", + ) self.obj.status = ChoicesFieldModel.GREEN self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["status"][1] == "Green", msg="The human readable text 'Green' is displayed.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["status"][1] == "Green", + msg="The human readable text 'Green' is displayed.", + ) def test_changes_display_dict_multiplechoice(self): - self.assertTrue(self.obj.history.latest().changes_display_dict["multiplechoice"][1] == "Red, Yellow, Green", - msg="The human readable text 'Red, Yellow, Green' is displayed.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["multiplechoice"][1] + == "Red, Yellow, Green", + msg="The human readable text 'Red, Yellow, Green' is displayed.", + ) self.obj.multiplechoice = ChoicesFieldModel.RED self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["multiplechoice"][1] == "Red", - msg="The human readable text 'Red' is displayed.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["multiplechoice"][1] + == "Red", + msg="The human readable text 'Red' is displayed.", + ) class CharfieldTextfieldModelTest(TestCase): - def setUp(self): self.PLACEHOLDER_LONGCHAR = "s" * 255 self.PLACEHOLDER_LONGTEXTFIELD = "s" * 1000 @@ -542,28 +725,38 @@ class CharfieldTextfieldModelTest(TestCase): ) def test_changes_display_dict_longchar(self): - self.assertTrue(self.obj.history.latest().changes_display_dict["longchar"][1] == \ - "{}...".format(self.PLACEHOLDER_LONGCHAR[:140]), - msg="The string should be truncated at 140 characters with an ellipsis at the end.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["longchar"][1] + == "{}...".format(self.PLACEHOLDER_LONGCHAR[:140]), + msg="The string should be truncated at 140 characters with an ellipsis at the end.", + ) SHORTENED_PLACEHOLDER = self.PLACEHOLDER_LONGCHAR[:139] self.obj.longchar = SHORTENED_PLACEHOLDER self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["longchar"][1] == SHORTENED_PLACEHOLDER, - msg="The field should display the entire string because it is less than 140 characters") + self.assertTrue( + self.obj.history.latest().changes_display_dict["longchar"][1] + == SHORTENED_PLACEHOLDER, + msg="The field should display the entire string because it is less than 140 characters", + ) def test_changes_display_dict_longtextfield(self): - self.assertTrue(self.obj.history.latest().changes_display_dict["longtextfield"][1] == \ - "{}...".format(self.PLACEHOLDER_LONGTEXTFIELD[:140]), - msg="The string should be truncated at 140 characters with an ellipsis at the end.") + self.assertTrue( + self.obj.history.latest().changes_display_dict["longtextfield"][1] + == "{}...".format(self.PLACEHOLDER_LONGTEXTFIELD[:140]), + msg="The string should be truncated at 140 characters with an ellipsis at the end.", + ) SHORTENED_PLACEHOLDER = self.PLACEHOLDER_LONGTEXTFIELD[:139] self.obj.longtextfield = SHORTENED_PLACEHOLDER self.obj.save() - self.assertTrue(self.obj.history.latest().changes_display_dict["longtextfield"][1] == SHORTENED_PLACEHOLDER, - msg="The field should display the entire string because it is less than 140 characters") + self.assertTrue( + self.obj.history.latest().changes_display_dict["longtextfield"][1] + == SHORTENED_PLACEHOLDER, + msg="The field should display the entire string because it is less than 140 characters", + ) class PostgresArrayFieldModelTest(TestCase): - databases = '__all__' + databases = "__all__" def setUp(self): self.obj = PostgresArrayFieldModel.objects.create( @@ -575,20 +768,28 @@ class PostgresArrayFieldModelTest(TestCase): return self.obj.history.latest().changes_display_dict["arrayfield"][1] def test_changes_display_dict_arrayfield(self): - self.assertTrue(self.latest_array_change == "Red, Green", - msg="The human readable text for the two choices, 'Red, Green' is displayed.") + self.assertTrue( + self.latest_array_change == "Red, Green", + msg="The human readable text for the two choices, 'Red, Green' is displayed.", + ) self.obj.arrayfield = [PostgresArrayFieldModel.GREEN] self.obj.save() - self.assertTrue(self.latest_array_change == "Green", - msg="The human readable text 'Green' is displayed.") + self.assertTrue( + self.latest_array_change == "Green", + msg="The human readable text 'Green' is displayed.", + ) self.obj.arrayfield = [] self.obj.save() - self.assertTrue(self.latest_array_change == "", - msg="The human readable text '' is displayed.") + self.assertTrue( + self.latest_array_change == "", + msg="The human readable text '' is displayed.", + ) self.obj.arrayfield = [PostgresArrayFieldModel.GREEN] self.obj.save() - self.assertTrue(self.latest_array_change == "Green", - msg="The human readable text 'Green' is displayed.") + self.assertTrue( + self.latest_array_change == "Green", + msg="The human readable text 'Green' is displayed.", + ) class AdminPanelTest(TestCase): @@ -602,7 +803,7 @@ class AdminPanelTest(TestCase): cls.user.is_superuser = True cls.user.is_active = True cls.user.save() - cls.obj = SimpleModel.objects.create(text='For admin logentry test') + cls.obj = SimpleModel.objects.create(text="For admin logentry test") def test_auditlog_admin(self): self.client.login(username=self.username, password=self.password) @@ -611,7 +812,9 @@ class AdminPanelTest(TestCase): assert res.status_code == 200 res = self.client.get("/admin/auditlog/logentry/add/") assert res.status_code == 200 - res = self.client.get("/admin/auditlog/logentry/{}/".format(log_pk), follow=True) + res = self.client.get( + "/admin/auditlog/logentry/{}/".format(log_pk), follow=True + ) assert res.status_code == 200 res = self.client.get("/admin/auditlog/logentry/{}/delete/".format(log_pk)) assert res.status_code == 200 @@ -628,7 +831,7 @@ class NoDeleteHistoryTest(TestCase): assert LogEntry.objects.all().count() == 2 instance.delete() - entries = LogEntry.objects.order_by('id') + entries = LogEntry.objects.order_by("id") # The "DELETE" record is always retained assert LogEntry.objects.all().count() == 1 @@ -642,9 +845,9 @@ class NoDeleteHistoryTest(TestCase): self.assertEqual(LogEntry.objects.all().count(), 2) instance.delete() - entries = LogEntry.objects.order_by('id') + entries = LogEntry.objects.order_by("id") self.assertEqual(entries.count(), 3) self.assertEqual( - list(entries.values_list('action', flat=True)), - [LogEntry.Action.CREATE, LogEntry.Action.UPDATE, LogEntry.Action.DELETE] + list(entries.values_list("action", flat=True)), + [LogEntry.Action.CREATE, LogEntry.Action.UPDATE, LogEntry.Action.DELETE], ) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..f040b26 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,2 @@ +[tool.black] +target-version = ["py35"] diff --git a/runtests.py b/runtests.py index b7df405..dea36f6 100644 --- a/runtests.py +++ b/runtests.py @@ -7,7 +7,7 @@ from django.conf import settings from django.test.utils import get_runner if __name__ == "__main__": - os.environ['DJANGO_SETTINGS_MODULE'] = 'auditlog_tests.test_settings' + os.environ["DJANGO_SETTINGS_MODULE"] = "auditlog_tests.test_settings" django.setup() TestRunner = get_runner(settings) test_runner = TestRunner() diff --git a/setup.py b/setup.py index 7fc063b..ef3be65 100644 --- a/setup.py +++ b/setup.py @@ -7,29 +7,31 @@ with open(os.path.join(os.path.dirname(__file__), "README.md"), "r") as readme_f long_description = readme_file.read() setup( - name='django-auditlog', + name="django-auditlog", use_scm_version={"version_scheme": "post-release"}, setup_requires=["setuptools_scm"], - packages=['auditlog', 'auditlog.migrations', 'auditlog.management', 'auditlog.management.commands'], - url='https://github.com/jazzband/django-auditlog', - license='MIT', - author='Jan-Jelle Kester', - description='Audit log app for Django', - long_description=long_description, - long_description_content_type='text/markdown', - install_requires=[ - 'django-jsonfield>=1.0.0', - 'python-dateutil>=2.6.0' + packages=[ + "auditlog", + "auditlog.migrations", + "auditlog.management", + "auditlog.management.commands", ], + url="https://github.com/jazzband/django-auditlog", + license="MIT", + author="Jan-Jelle Kester", + description="Audit log app for Django", + long_description=long_description, + long_description_content_type="text/markdown", + install_requires=["django-jsonfield>=1.0.0", "python-dateutil>=2.6.0"], zip_safe=False, classifiers=[ - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'License :: OSI Approved :: MIT License', + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.4", + "Programming Language :: Python :: 3.5", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "License :: OSI Approved :: MIT License", ], ) diff --git a/tox.ini b/tox.ini index 2a5eb8c..0ddd609 100644 --- a/tox.ini +++ b/tox.ini @@ -3,6 +3,7 @@ envlist = {py35,py36,py37,py38,py39}-django-22 {py36,py37,py38,py39}-django-{30,31} py38-docs + py38-qa [testenv] commands = @@ -35,6 +36,13 @@ changedir = docs/source deps = -rdocs/requirements.txt commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html +[testenv:py38-qa] +basepython = python3.8 +deps = + black +commands = + black --check --diff auditlog auditlog_tests setup.py runtests.py + [gh-actions] python = 3.5: py35 From 497c83fc83d03e394a519fca034b169d03c25021 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 6 Dec 2020 21:36:46 +0100 Subject: [PATCH 22/41] Add isort and sort orders with isort. --- auditlog/__init__.py | 2 +- auditlog/admin.py | 5 +-- auditlog/diff.py | 2 +- auditlog/migrations/0001_initial.py | 2 +- .../0002_auto_support_long_primary_keys.py | 2 +- .../migrations/0003_logentry_remote_addr.py | 2 +- .../0004_logentry_detailed_object_repr.py | 2 +- ...5_logentry_additional_data_verbose_name.py | 2 +- auditlog/models.py | 4 +-- auditlog/registry.py | 6 ++-- auditlog_tests/models.py | 1 + auditlog_tests/tests.py | 33 ++++++++++--------- auditlog_tests/urls.py | 3 +- pyproject.toml | 4 +++ tox.ini | 2 ++ 15 files changed, 40 insertions(+), 32 deletions(-) diff --git a/auditlog/__init__.py b/auditlog/__init__.py index b3d77c3..e96d652 100644 --- a/auditlog/__init__.py +++ b/auditlog/__init__.py @@ -1,4 +1,4 @@ -from pkg_resources import get_distribution, DistributionNotFound +from pkg_resources import DistributionNotFound, get_distribution try: __version__ = get_distribution("django-auditlog").version diff --git a/auditlog/admin.py b/auditlog/admin.py index 2f32ccb..8315be8 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -1,7 +1,8 @@ from django.contrib import admin -from .models import LogEntry -from .mixins import LogEntryAdminMixin + from .filters import ResourceTypeFilter +from .mixins import LogEntryAdminMixin +from .models import LogEntry class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): diff --git a/auditlog/diff.py b/auditlog/diff.py index 3774e60..9dc642a 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -1,6 +1,6 @@ from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Model, NOT_PROVIDED, DateTimeField +from django.db.models import NOT_PROVIDED, DateTimeField, Model from django.utils import timezone from django.utils.encoding import smart_text diff --git a/auditlog/migrations/0001_initial.py b/auditlog/migrations/0001_initial.py index 0e2bd55..22b1ee4 100644 --- a/auditlog/migrations/0001_initial.py +++ b/auditlog/migrations/0001_initial.py @@ -1,6 +1,6 @@ -from django.db import models, migrations import django.db.models.deletion from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/auditlog/migrations/0002_auto_support_long_primary_keys.py b/auditlog/migrations/0002_auto_support_long_primary_keys.py index 01c7d6e..e767b92 100644 --- a/auditlog/migrations/0002_auto_support_long_primary_keys.py +++ b/auditlog/migrations/0002_auto_support_long_primary_keys.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/auditlog/migrations/0003_logentry_remote_addr.py b/auditlog/migrations/0003_logentry_remote_addr.py index 706dc4f..c363e82 100644 --- a/auditlog/migrations/0003_logentry_remote_addr.py +++ b/auditlog/migrations/0003_logentry_remote_addr.py @@ -1,4 +1,4 @@ -from django.db import models, migrations +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/auditlog/migrations/0004_logentry_detailed_object_repr.py b/auditlog/migrations/0004_logentry_detailed_object_repr.py index 9e9deeb..4a242cd 100644 --- a/auditlog/migrations/0004_logentry_detailed_object_repr.py +++ b/auditlog/migrations/0004_logentry_detailed_object_repr.py @@ -1,5 +1,5 @@ -from django.db import models, migrations import jsonfield.fields +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py index 09e22d1..67edeed 100644 --- a/auditlog/migrations/0005_logentry_additional_data_verbose_name.py +++ b/auditlog/migrations/0005_logentry_additional_data_verbose_name.py @@ -1,5 +1,5 @@ -from django.db import migrations, models import jsonfield.fields +from django.db import migrations, models class Migration(migrations.Migration): diff --git a/auditlog/models.py b/auditlog/models.py index c479acf..8c4a73c 100644 --- a/auditlog/models.py +++ b/auditlog/models.py @@ -7,8 +7,8 @@ from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation from django.contrib.contenttypes.models import ContentType from django.core.exceptions import FieldDoesNotExist -from django.db import models, DEFAULT_DB_ALIAS -from django.db.models import QuerySet, Q, Field +from django.db import DEFAULT_DB_ALIAS, models +from django.db.models import Field, Q, QuerySet from django.utils import formats, timezone from django.utils.encoding import smart_str from django.utils.translation import ugettext_lazy as _ diff --git a/auditlog/registry.py b/auditlog/registry.py index d4a520a..08af299 100644 --- a/auditlog/registry.py +++ b/auditlog/registry.py @@ -1,8 +1,8 @@ -from typing import Dict, Callable, Optional, List, Tuple +from typing import Callable, Dict, List, Optional, Tuple from django.db.models import Model from django.db.models.base import ModelBase -from django.db.models.signals import pre_save, post_save, post_delete, ModelSignal +from django.db.models.signals import ModelSignal, post_delete, post_save, pre_save DispatchUID = Tuple[int, str, int] @@ -19,7 +19,7 @@ class AuditlogModelRegistry(object): delete: bool = True, custom: Optional[Dict[ModelSignal, Callable]] = None, ): - from auditlog.receivers import log_create, log_update, log_delete + from auditlog.receivers import log_create, log_delete, log_update self._registry = {} self._signals = {} diff --git a/auditlog_tests/models.py b/auditlog_tests/models.py index a3b56b3..eddede4 100644 --- a/auditlog_tests/models.py +++ b/auditlog_tests/models.py @@ -2,6 +2,7 @@ import uuid from django.contrib.postgres.fields import ArrayField from django.db import models + from auditlog.models import AuditlogHistoryField from auditlog.registry import auditlog diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index b4c5c02..eb6b858 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1,35 +1,36 @@ import datetime -import django import json + +import django +from dateutil.tz import gettz from django.conf import settings from django.contrib import auth -from django.contrib.auth.models import User, AnonymousUser +from django.contrib.auth.models import AnonymousUser, User from django.core.exceptions import ValidationError from django.db.models.signals import pre_save from django.http import HttpResponse -from django.test import TestCase, RequestFactory +from django.test import RequestFactory, TestCase from django.utils import dateformat, formats, timezone -from dateutil.tz import gettz from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry from auditlog.registry import auditlog from auditlog_tests.models import ( - SimpleModel, - AltPrimaryKeyModel, - UUIDPrimaryKeyModel, - ProxyModel, - SimpleIncludeModel, - SimpleExcludeModel, - SimpleMappingModel, - RelatedModel, - ManyRelatedModel, AdditionalDataIncludedModel, - DateTimeFieldModel, - ChoicesFieldModel, + AltPrimaryKeyModel, CharfieldTextfieldModel, - PostgresArrayFieldModel, + ChoicesFieldModel, + DateTimeFieldModel, + ManyRelatedModel, NoDeleteHistoryModel, + PostgresArrayFieldModel, + ProxyModel, + RelatedModel, + SimpleExcludeModel, + SimpleIncludeModel, + SimpleMappingModel, + SimpleModel, + UUIDPrimaryKeyModel, ) diff --git a/auditlog_tests/urls.py b/auditlog_tests/urls.py index df5b838..083932c 100644 --- a/auditlog_tests/urls.py +++ b/auditlog_tests/urls.py @@ -1,6 +1,5 @@ -from django.urls import path from django.contrib import admin - +from django.urls import path urlpatterns = [ path("admin/", admin.site.urls), diff --git a/pyproject.toml b/pyproject.toml index f040b26..e8ee0b0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,2 +1,6 @@ [tool.black] target-version = ["py35"] + +# black compatible isort +[tool.isort] +profile = "black" diff --git a/tox.ini b/tox.ini index 0ddd609..b609ddb 100644 --- a/tox.ini +++ b/tox.ini @@ -40,8 +40,10 @@ commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html basepython = python3.8 deps = black + isort commands = black --check --diff auditlog auditlog_tests setup.py runtests.py + isort --check-only --diff auditlog auditlog_tests setup.py runtests.py [gh-actions] python = From 6131430ff7d1e9e8cd95f8d2793e82cc72679d81 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 6 Dec 2020 21:40:28 +0100 Subject: [PATCH 23/41] Change relative imports to absolute. --- auditlog/admin.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auditlog/admin.py b/auditlog/admin.py index 8315be8..103d000 100644 --- a/auditlog/admin.py +++ b/auditlog/admin.py @@ -1,8 +1,8 @@ from django.contrib import admin -from .filters import ResourceTypeFilter -from .mixins import LogEntryAdminMixin -from .models import LogEntry +from auditlog.filters import ResourceTypeFilter +from auditlog.mixins import LogEntryAdminMixin +from auditlog.models import LogEntry class LogEntryAdmin(admin.ModelAdmin, LogEntryAdminMixin): From 3e8d398c8e9febe3642cefb963c338c846079895 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Sun, 6 Dec 2020 22:55:46 +0100 Subject: [PATCH 24/41] Remove Python 3.4 from setup.py classifiers. --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index ef3be65..ea7932a 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,6 @@ setup( zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", From d77803b3b48b83c396929d7aed6653c976ed470c Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 7 Dec 2020 21:11:01 +0100 Subject: [PATCH 25/41] Add Django supported versions to setup.py classifiers. --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index ea7932a..d243f8b 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,10 @@ setup( "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", + "Framework :: Django", + "Framework :: Django :: 2.2", + "Framework :: Django :: 3.0", + "Framework :: Django :: 3.1", "License :: OSI Approved :: MIT License", ], ) From 2c6bf286b479d70a632b58d55e4e388b35f84d48 Mon Sep 17 00:00:00 2001 From: Jannis Leidel Date: Mon, 26 Apr 2021 10:07:06 +0200 Subject: [PATCH 26/41] Remove note about maintenance. --- docs/source/index.rst | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/source/index.rst b/docs/source/index.rst index a9c0016..bf58a72 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -27,12 +27,6 @@ Contents Contribute to Auditlog ---------------------- -.. note:: - Due to multiple reasons the development of Auditlog is not a priority for me at this moment. Therefore progress might - be slow. This does not mean that this project is abandoned! Community involvement in the form of pull requests is - very much appreciated. Also, if you like to take Auditlog to the next level and be a permanent contributor, please - contact the author. Contact information can be found via GitHub. - If you discovered a bug or want to improve the code, please submit an issue and/or pull request via GitHub. Before submitting a new issue, please make sure there is no issue submitted that involves the same problem. From 2e477ab04ad7ff0a869ca4d3f48e4140e113beea Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 24 May 2021 01:25:46 +0200 Subject: [PATCH 27/41] Remove Python 3.5 support. (#301) --- .github/workflows/test.yml | 2 +- docs/source/installation.rst | 4 ++-- pyproject.toml | 2 +- setup.py | 1 - tox.ini | 5 +---- 5 files changed, 5 insertions(+), 9 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40c56e4..40a3364 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -9,7 +9,7 @@ jobs: fail-fast: false max-parallel: 5 matrix: - python-version: ['3.5', '3.6', '3.7', '3.8', '3.9'] + python-version: ['3.6', '3.7', '3.8', '3.9'] services: postgres: diff --git a/docs/source/installation.rst b/docs/source/installation.rst index a879370..820d0e8 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -11,10 +11,10 @@ The repository can be found at https://github.com/jazzband/django-auditlog/. **Requirements** -- Python 3.5 or higher +- Python 3.6 or higher - Django 2.2 or higher -Auditlog is currently tested with Python 3.5 - 3.8 and Django 2.2, 3.0 and 3.1. The latest test report can be found +Auditlog is currently tested with Python 3.6 - 3.8 and Django 2.2, 3.0 and 3.1. The latest test report can be found at https://github.com/jazzband/django-auditlog/actions. Adding Auditlog to your Django application diff --git a/pyproject.toml b/pyproject.toml index e8ee0b0..8c3cf0d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [tool.black] -target-version = ["py35"] +target-version = ["py36"] # black compatible isort [tool.isort] diff --git a/setup.py b/setup.py index d243f8b..d4cd676 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,6 @@ setup( zip_safe=False, classifiers=[ "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", diff --git a/tox.ini b/tox.ini index b609ddb..d36823c 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,6 @@ [tox] envlist = - {py35,py36,py37,py38,py39}-django-22 - {py36,py37,py38,py39}-django-{30,31} + {py36,py37,py38,py39}-django-{22,30,31} py38-docs py38-qa @@ -29,7 +28,6 @@ basepython = py38: python3.8 py37: python3.7 py36: python3.6 - py35: python3.5 [testenv:py38-docs] changedir = docs/source @@ -47,7 +45,6 @@ commands = [gh-actions] python = - 3.5: py35 3.6: py36 3.7: py37 3.8: py38 From 55a66fc73afd9d01d714aa96d6adf2ba63fee4bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Blas=20Isaias=20Fern=C3=A1ndez?= Date: Sun, 11 Apr 2021 15:32:36 -0400 Subject: [PATCH 28/41] dict.iteritems was removed from python 3 --- docs/source/usage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/usage.rst b/docs/source/usage.rst index 8aa15f0..b40f296 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -135,7 +135,7 @@ The :py:class:`AuditlogHistoryField` provides easy access to :py:class:`LogEntry - {% for key, value in mymodel.history.latest.changes_dict.iteritems %} + {% for key, value in mymodel.history.latest.changes_dict.items %} {{ key }} {{ value.0|default:"None" }} From 457b04b44800c27f62e96534c89c8a4ff8d9d614 Mon Sep 17 00:00:00 2001 From: Panagiotis Simakis Date: Mon, 24 May 2021 23:16:03 +0200 Subject: [PATCH 29/41] Replace deprecated smart_text() with smart_str(). --- auditlog/diff.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/auditlog/diff.py b/auditlog/diff.py index 9dc642a..a7b30dc 100644 --- a/auditlog/diff.py +++ b/auditlog/diff.py @@ -2,7 +2,7 @@ from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from django.db.models import NOT_PROVIDED, DateTimeField, Model from django.utils import timezone -from django.utils.encoding import smart_text +from django.utils.encoding import smart_str def track_field(field): @@ -69,7 +69,7 @@ def get_field_value(obj, field): value = field.default if field.default is not NOT_PROVIDED else None else: try: - value = smart_text(getattr(obj, field.name, None)) + value = smart_str(getattr(obj, field.name, None)) except ObjectDoesNotExist: value = field.default if field.default is not NOT_PROVIDED else None @@ -139,7 +139,7 @@ def model_instance_diff(old, new): new_value = get_field_value(new, field) if old_value != new_value: - diff[field.name] = (smart_text(old_value), smart_text(new_value)) + diff[field.name] = (smart_str(old_value), smart_str(new_value)) if len(diff) == 0: diff = None From ca5aa82714f9f111e3cc82fb9fa08cf04d67bf8a Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 24 May 2021 23:20:14 +0200 Subject: [PATCH 30/41] Remove Django 3.0 support. --- CHANGES.rst | 8 ++++++++ docs/source/installation.rst | 2 +- setup.py | 1 - tox.ini | 3 +-- 4 files changed, 10 insertions(+), 4 deletions(-) create mode 100644 CHANGES.rst diff --git a/CHANGES.rst b/CHANGES.rst new file mode 100644 index 0000000..eaa8376 --- /dev/null +++ b/CHANGES.rst @@ -0,0 +1,8 @@ +Changes +======= + + +Unreleased +---------- + +- Remove Django 3.0 support. diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 820d0e8..c659deb 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -14,7 +14,7 @@ The repository can be found at https://github.com/jazzband/django-auditlog/. - Python 3.6 or higher - Django 2.2 or higher -Auditlog is currently tested with Python 3.6 - 3.8 and Django 2.2, 3.0 and 3.1. The latest test report can be found +Auditlog is currently tested with Python 3.6 - 3.8 and Django 2.2 and 3.1. The latest test report can be found at https://github.com/jazzband/django-auditlog/actions. Adding Auditlog to your Django application diff --git a/setup.py b/setup.py index d4cd676..1e46d5e 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,6 @@ setup( "Programming Language :: Python :: 3.9", "Framework :: Django", "Framework :: Django :: 2.2", - "Framework :: Django :: 3.0", "Framework :: Django :: 3.1", "License :: OSI Approved :: MIT License", ], diff --git a/tox.ini b/tox.ini index d36823c..90bbc5f 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - {py36,py37,py38,py39}-django-{22,30,31} + {py36,py37,py38,py39}-django-{22,31} py38-docs py38-qa @@ -10,7 +10,6 @@ commands = coverage xml deps = django-22: Django>=2.2,<2.3 - django-30: Django>=3.0,<3.1 django-31: Django>=3.1,<3.2 # Test requirements coverage From da7b1441d070961ef4d1b7e2118eefce241a37cc Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 24 May 2021 23:21:49 +0200 Subject: [PATCH 31/41] Add Django 3.2 support. --- CHANGES.rst | 1 + docs/source/installation.rst | 2 +- setup.py | 1 + tox.ini | 3 ++- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index eaa8376..984a404 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -6,3 +6,4 @@ Unreleased ---------- - Remove Django 3.0 support. +- Add Django 3.2 support. diff --git a/docs/source/installation.rst b/docs/source/installation.rst index c659deb..1467bcc 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -14,7 +14,7 @@ The repository can be found at https://github.com/jazzband/django-auditlog/. - Python 3.6 or higher - Django 2.2 or higher -Auditlog is currently tested with Python 3.6 - 3.8 and Django 2.2 and 3.1. The latest test report can be found +Auditlog is currently tested with Python 3.6 - 3.8 and Django 2.2, 3.1 and 3.2. The latest test report can be found at https://github.com/jazzband/django-auditlog/actions. Adding Auditlog to your Django application diff --git a/setup.py b/setup.py index 1e46d5e..031ea88 100644 --- a/setup.py +++ b/setup.py @@ -33,6 +33,7 @@ setup( "Framework :: Django", "Framework :: Django :: 2.2", "Framework :: Django :: 3.1", + "Framework :: Django :: 3.2", "License :: OSI Approved :: MIT License", ], ) diff --git a/tox.ini b/tox.ini index 90bbc5f..d95efe1 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] envlist = - {py36,py37,py38,py39}-django-{22,31} + {py36,py37,py38,py39}-django-{22,31,32} py38-docs py38-qa @@ -11,6 +11,7 @@ commands = deps = django-22: Django>=2.2,<2.3 django-31: Django>=3.1,<3.2 + django-32: Django>=3.2,<3.3 # Test requirements coverage codecov From 1a437f4e4052a565ea8dff069d87a85a76519f38 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Mon, 24 May 2021 23:22:34 +0200 Subject: [PATCH 32/41] Add DEFAULT_AUTO_FIELD to test settings. --- auditlog_tests/test_settings.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/auditlog_tests/test_settings.py b/auditlog_tests/test_settings.py index e2a036a..1ebfdc7 100644 --- a/auditlog_tests/test_settings.py +++ b/auditlog_tests/test_settings.py @@ -57,3 +57,5 @@ STATIC_URL = "/static/" ROOT_URLCONF = "auditlog_tests.urls" USE_TZ = True + +DEFAULT_AUTO_FIELD = "django.db.models.AutoField" From e2913da1bbbc8b840409addae52ef28743d1d535 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Fri, 28 May 2021 22:19:48 +0200 Subject: [PATCH 33/41] Replace MIDDLEWARE_CLASSES with MIDDLEWARE in docs. --- docs/source/installation.rst | 2 +- docs/source/usage.rst | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/installation.rst b/docs/source/installation.rst index 1467bcc..4b07966 100644 --- a/docs/source/installation.rst +++ b/docs/source/installation.rst @@ -24,5 +24,5 @@ To use Auditlog in your application, just add ``'auditlog'`` to your project's ` ``manage.py migrate`` to create/upgrade the necessary database structure. If you want Auditlog to automatically set the actor for log entries you also need to enable the middleware by adding -``'auditlog.middleware.AuditlogMiddleware'`` to your ``MIDDLEWARE_CLASSES`` setting. Please check :doc:`usage` for more +``'auditlog.middleware.AuditlogMiddleware'`` to your ``MIDDLEWARE`` setting. Please check :doc:`usage` for more information. diff --git a/docs/source/usage.rst b/docs/source/usage.rst index b40f296..101c687 100644 --- a/docs/source/usage.rst +++ b/docs/source/usage.rst @@ -83,10 +83,10 @@ When using automatic logging, the actor is empty by default. However, auditlog c request automatically. This does not need any custom code, adding a middleware class is enough. When an actor is logged the remote address of that actor will be logged as well. -To enable the automatic logging of the actors, simply add the following to your ``MIDDLEWARE_CLASSES`` setting in your +To enable the automatic logging of the actors, simply add the following to your ``MIDDLEWARE`` setting in your project's configuration file:: - MIDDLEWARE_CLASSES = ( + MIDDLEWARE = ( # Request altering middleware, e.g., Django's default middleware classes 'auditlog.middleware.AuditlogMiddleware', # Other middleware From 3eb5d66c39644e2634338adec332480278d7c821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Fri, 10 May 2019 18:46:44 +0300 Subject: [PATCH 34/41] Use get_user_model --- auditlog/middleware.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/auditlog/middleware.py b/auditlog/middleware.py index ec1f563..de18037 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -2,8 +2,7 @@ import threading import time from functools import partial -from django.apps import apps -from django.conf import settings +from django.contrib.auth import get_user_model from django.db.models.signals import pre_save from django.utils.deprecation import MiddlewareMixin @@ -82,14 +81,9 @@ class AuditlogMiddleware(MiddlewareMixin): if hasattr(threadlocal, "auditlog"): if signal_duid != threadlocal.auditlog["signal_duid"]: return - try: - app_label, model_name = settings.AUTH_USER_MODEL.split(".") - auth_user_model = apps.get_model(app_label, model_name) - except ValueError: - auth_user_model = apps.get_model("auth", "user") if ( sender == LogEntry - and isinstance(user, auth_user_model) + and isinstance(user, get_user_model()) and instance.actor is None ): instance.actor = user From 9629f3f8d7aef2fa3ddec504e990af55ecbf235f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Sat, 11 May 2019 13:11:12 +0300 Subject: [PATCH 35/41] Move signal management to a context manager This change allows setting the same signals when the request is not present, i.e. in a celery task. --- auditlog/context.py | 66 +++++++++++++++++++++++++++ auditlog/middleware.py | 98 +++++++++-------------------------------- auditlog_tests/tests.py | 75 +++++++++++++++---------------- tox.ini | 1 + 4 files changed, 124 insertions(+), 116 deletions(-) create mode 100644 auditlog/context.py diff --git a/auditlog/context.py b/auditlog/context.py new file mode 100644 index 0000000..251a240 --- /dev/null +++ b/auditlog/context.py @@ -0,0 +1,66 @@ +import contextlib +import threading +import time +from functools import partial + +from django.contrib.auth import get_user_model +from django.db.models.signals import pre_save + +from auditlog.models import LogEntry + +threadlocal = threading.local() + + +@contextlib.contextmanager +def set_actor(actor, remote_addr=None): + """Connect a signal receiver with current user attached.""" + # Initialize thread local storage + threadlocal.auditlog = { + "signal_duid": ("set_actor", time.time()), + "remote_addr": remote_addr, + } + + # Connect signal for automatic logging + set_actor = partial( + _set_actor, user=actor, signal_duid=threadlocal.auditlog["signal_duid"] + ) + pre_save.connect( + set_actor, + sender=LogEntry, + dispatch_uid=threadlocal.auditlog["signal_duid"], + weak=False, + ) + + try: + yield + + finally: + try: + auditlog = threadlocal.auditlog + except AttributeError: + pass + else: + pre_save.disconnect(sender=LogEntry, dispatch_uid=auditlog["signal_duid"]) + + +def _set_actor(user, sender, instance, signal_duid, **kwargs): + """Signal receiver with an extra 'user' kwarg. + + This function becomes a valid signal receiver when it is curried with the actor. + """ + try: + auditlog = threadlocal.auditlog + except AttributeError: + pass + else: + if signal_duid != auditlog["signal_duid"]: + return + auth_user_model = get_user_model() + if ( + sender == LogEntry + and isinstance(user, auth_user_model) + and instance.actor is None + ): + instance.actor = user + + instance.remote_addr = auditlog["remote_addr"] diff --git a/auditlog/middleware.py b/auditlog/middleware.py index de18037..18fbd0e 100644 --- a/auditlog/middleware.py +++ b/auditlog/middleware.py @@ -1,91 +1,37 @@ -import threading -import time -from functools import partial +import contextlib -from django.contrib.auth import get_user_model -from django.db.models.signals import pre_save -from django.utils.deprecation import MiddlewareMixin - -from auditlog.models import LogEntry - -threadlocal = threading.local() +from auditlog.context import set_actor -class AuditlogMiddleware(MiddlewareMixin): +@contextlib.contextmanager +def nullcontext(): + """Equivalent to contextlib.nullcontext(None) from Python 3.7.""" + yield + + +class AuditlogMiddleware(object): """ Middleware to couple the request's user to log items. This is accomplished by currying the signal receiver with the user from the request (or None if the user is not authenticated). """ - def process_request(self, request): - """ - Gets the current user from the request and prepares and connects a signal receiver with the user already - attached to it. - """ - # Initialize thread local storage - threadlocal.auditlog = { - "signal_duid": (self.__class__, time.time()), - "remote_addr": request.META.get("REMOTE_ADDR"), - } + def __init__(self, get_response=None): + self.get_response = get_response + + def __call__(self, request): - # In case of proxy, set 'original' address if request.META.get("HTTP_X_FORWARDED_FOR"): - threadlocal.auditlog["remote_addr"] = request.META.get( - "HTTP_X_FORWARDED_FOR" - ).split(",")[0] + # In case of proxy, set 'original' address + remote_addr = request.META.get("HTTP_X_FORWARDED_FOR").split(",")[0] + else: + remote_addr = request.META.get("REMOTE_ADDR") - # Connect signal for automatic logging if hasattr(request, "user") and getattr( request.user, "is_authenticated", False ): - set_actor = partial( - self.set_actor, - user=request.user, - signal_duid=threadlocal.auditlog["signal_duid"], - ) - pre_save.connect( - set_actor, - sender=LogEntry, - dispatch_uid=threadlocal.auditlog["signal_duid"], - weak=False, - ) + context = set_actor(actor=request.user, remote_addr=remote_addr) + else: + context = nullcontext() - def process_response(self, request, response): - """ - Disconnects the signal receiver to prevent it from staying active. - """ - if hasattr(threadlocal, "auditlog"): - pre_save.disconnect( - sender=LogEntry, dispatch_uid=threadlocal.auditlog["signal_duid"] - ) - - return response - - def process_exception(self, request, exception): - """ - Disconnects the signal receiver to prevent it from staying active in case of an exception. - """ - if hasattr(threadlocal, "auditlog"): - pre_save.disconnect( - sender=LogEntry, dispatch_uid=threadlocal.auditlog["signal_duid"] - ) - - return None - - @staticmethod - def set_actor(user, sender, instance, signal_duid, **kwargs): - """ - Signal receiver with an extra, required 'user' kwarg. This method becomes a real (valid) signal receiver when - it is curried with the actor. - """ - if hasattr(threadlocal, "auditlog"): - if signal_duid != threadlocal.auditlog["signal_duid"]: - return - if ( - sender == LogEntry - and isinstance(user, get_user_model()) - and instance.actor is None - ): - instance.actor = user - - instance.remote_addr = threadlocal.auditlog["remote_addr"] + with context: + return self.get_response(request) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index eb6b858..3c47f1b 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -2,6 +2,7 @@ import datetime import json import django +import mock from dateutil.tz import gettz from django.conf import settings from django.contrib import auth @@ -167,72 +168,66 @@ class MiddlewareTest(TestCase): """ def setUp(self): - self.middleware = AuditlogMiddleware() + self.get_response_mock = mock.Mock() + self.response_mock = mock.Mock() + self.middleware = AuditlogMiddleware(get_response=self.get_response_mock) self.factory = RequestFactory() self.user = User.objects.create_user( username="test", email="test@example.com", password="top_secret" ) + def side_effect(self, assertion): + def inner(request): + assertion() + return self.response_mock + + return inner + + def assert_has_listeners(self): + self.assertTrue(pre_save.has_listeners(LogEntry)) + + def assert_no_listeners(self): + self.assertFalse(pre_save.has_listeners(LogEntry)) + def test_request_anonymous(self): """No actor will be logged when a user is not logged in.""" - # Create a request request = self.factory.get("/") request.user = AnonymousUser() - # Run middleware - self.middleware.process_request(request) + self.get_response_mock.side_effect = self.side_effect(self.assert_no_listeners) - # Validate result - self.assertFalse(pre_save.has_listeners(LogEntry)) + response = self.middleware(request) - # Finalize transaction - self.middleware.process_exception(request, None) + self.assertIs(response, self.response_mock) + self.get_response_mock.assert_called_once_with(request) + self.assert_no_listeners() def test_request(self): """The actor will be logged when a user is logged in.""" - # Create a request - request = self.factory.get("/") - request.user = self.user - # Run middleware - self.middleware.process_request(request) - - # Validate result - self.assertTrue(pre_save.has_listeners(LogEntry)) - - # Finalize transaction - self.middleware.process_exception(request, None) - - def test_response(self): - """The signal will be disconnected when the request is processed.""" - # Create a request request = self.factory.get("/") request.user = self.user - # Run middleware - self.middleware.process_request(request) - self.assertTrue( - pre_save.has_listeners(LogEntry) - ) # The signal should be present before trying to disconnect it. - self.middleware.process_response(request, HttpResponse()) + self.get_response_mock.side_effect = self.side_effect(self.assert_has_listeners) - # Validate result - self.assertFalse(pre_save.has_listeners(LogEntry)) + response = self.middleware(request) + + self.assertIs(response, self.response_mock) + self.get_response_mock.assert_called_once_with(request) + self.assert_no_listeners() def test_exception(self): """The signal will be disconnected when an exception is raised.""" - # Create a request request = self.factory.get("/") request.user = self.user - # Run middleware - self.middleware.process_request(request) - self.assertTrue( - pre_save.has_listeners(LogEntry) - ) # The signal should be present before trying to disconnect it. - self.middleware.process_exception(request, ValidationError("Test")) + SomeException = type("SomeException", (Exception,), {}) - # Validate result - self.assertFalse(pre_save.has_listeners(LogEntry)) + self.get_response_mock.side_effect = SomeException + + with self.assertRaises(SomeException): + self.middleware(request) + + self.assert_no_listeners() class SimpeIncludeModelTest(TestCase): diff --git a/tox.ini b/tox.ini index d95efe1..e6e8f74 100644 --- a/tox.ini +++ b/tox.ini @@ -15,6 +15,7 @@ deps = # Test requirements coverage codecov + mock psycopg2-binary passenv= TEST_DB_HOST From 13cad5b25af50f11bce96f9a30a3201a6b034487 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Wed, 9 Sep 2020 11:11:59 +0300 Subject: [PATCH 36/41] Explicitly unset the threadlocal --- auditlog/context.py | 1 + 1 file changed, 1 insertion(+) diff --git a/auditlog/context.py b/auditlog/context.py index 251a240..3141b19 100644 --- a/auditlog/context.py +++ b/auditlog/context.py @@ -41,6 +41,7 @@ def set_actor(actor, remote_addr=None): pass else: pre_save.disconnect(sender=LogEntry, dispatch_uid=auditlog["signal_duid"]) + del threadlocal.auditlog def _set_actor(user, sender, instance, signal_duid, **kwargs): From 034ba57d938b285941206626c568b97b1295ae68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Wed, 9 Sep 2020 11:15:14 +0300 Subject: [PATCH 37/41] Fix a misleading docstring --- auditlog/context.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/auditlog/context.py b/auditlog/context.py index 3141b19..adac85d 100644 --- a/auditlog/context.py +++ b/auditlog/context.py @@ -45,9 +45,9 @@ def set_actor(actor, remote_addr=None): def _set_actor(user, sender, instance, signal_duid, **kwargs): - """Signal receiver with an extra 'user' kwarg. + """Signal receiver with extra 'user' and 'signal_duid' kwargs. - This function becomes a valid signal receiver when it is curried with the actor. + This function becomes a valid signal receiver when it is curried with the actor and a dispatch id. """ try: auditlog = threadlocal.auditlog From 6c0c83e7e597e6d3e0e21dbe5b68e49cbe489cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Mon, 5 Oct 2020 13:20:59 +0300 Subject: [PATCH 38/41] Remove unused imports Import of RelatedModel was left in place as it just lacks respective tests. --- auditlog_tests/tests.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 3c47f1b..34f371f 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -5,11 +5,8 @@ import django import mock from dateutil.tz import gettz from django.conf import settings -from django.contrib import auth from django.contrib.auth.models import AnonymousUser, User -from django.core.exceptions import ValidationError from django.db.models.signals import pre_save -from django.http import HttpResponse from django.test import RequestFactory, TestCase from django.utils import dateformat, formats, timezone From 2b44eebd50dcff2fe951f674d5b0a6906d310509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Mon, 5 Oct 2020 13:21:46 +0300 Subject: [PATCH 39/41] Use assertEqual to assert equality --- auditlog_tests/tests.py | 215 +++++++++++++++++++++------------------- 1 file changed, 113 insertions(+), 102 deletions(-) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 34f371f..ee267cc 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -42,7 +42,7 @@ class SimpleModelTest(TestCase): obj = self.obj # Check for log entries - self.assertTrue(obj.history.count() == 1, msg="There is one log entry") + self.assertEqual(obj.history.count(), 1, msg="There is one log entry") try: history = obj.history.get() @@ -66,8 +66,9 @@ class SimpleModelTest(TestCase): obj.save() # Check for log entries - self.assertTrue( - obj.history.filter(action=LogEntry.Action.UPDATE).count() == 1, + self.assertEqual( + obj.history.filter(action=LogEntry.Action.UPDATE).count(), + 1, msg="There is one log entry for 'UPDATE'", ) @@ -90,13 +91,13 @@ class SimpleModelTest(TestCase): obj.delete() # Check for log entries - self.assertTrue( + self.assertEqual( LogEntry.objects.filter( content_type=history.content_type, object_pk=history.object_pk, action=LogEntry.Action.DELETE, - ).count() - == 1, + ).count(), + 1, msg="There is one log entry for 'DELETE'", ) @@ -233,17 +234,17 @@ class SimpeIncludeModelTest(TestCase): def test_register_include_fields(self): sim = SimpleIncludeModel(label="Include model", text="Looong text") sim.save() - self.assertTrue(sim.history.count() == 1, msg="There is one log entry") + self.assertEqual(sim.history.count(), 1, msg="There is one log entry") # Change label, record sim.label = "Changed label" sim.save() - self.assertTrue(sim.history.count() == 2, msg="There are two log entries") + self.assertEqual(sim.history.count(), 2, msg="There are two log entries") # Change text, ignore sim.text = "Short text" sim.save() - self.assertTrue(sim.history.count() == 2, msg="There are two log entries") + self.assertEqual(sim.history.count(), 2, msg="There are two log entries") class SimpeExcludeModelTest(TestCase): @@ -252,17 +253,17 @@ class SimpeExcludeModelTest(TestCase): def test_register_exclude_fields(self): sem = SimpleExcludeModel(label="Exclude model", text="Looong text") sem.save() - self.assertTrue(sem.history.count() == 1, msg="There is one log entry") + self.assertEqual(sem.history.count(), 1, msg="There is one log entry") # Change label, ignore sem.label = "Changed label" sem.save() - self.assertTrue(sem.history.count() == 2, msg="There are two log entries") + self.assertEqual(sem.history.count(), 2, msg="There are two log entries") # Change text, record sem.text = "Short text" sem.save() - self.assertTrue(sem.history.count() == 2, msg="There are two log entries") + self.assertEqual(sem.history.count(), 2, msg="There are two log entries") class SimpleMappingModelTest(TestCase): @@ -273,28 +274,32 @@ class SimpleMappingModelTest(TestCase): sku="ASD301301A6", vtxt="2.1.5", not_mapped="Not mapped" ) smm.save() - self.assertTrue( - smm.history.latest().changes_dict["sku"][1] == "ASD301301A6", + self.assertEqual( + smm.history.latest().changes_dict["sku"][1], + "ASD301301A6", msg="The diff function retains 'sku' and can be retrieved.", ) - self.assertTrue( - smm.history.latest().changes_dict["not_mapped"][1] == "Not mapped", + self.assertEqual( + smm.history.latest().changes_dict["not_mapped"][1], + "Not mapped", msg="The diff function does not map 'not_mapped' and can be retrieved.", ) - self.assertTrue( - smm.history.latest().changes_display_dict["Product No."][1] - == "ASD301301A6", + self.assertEqual( + smm.history.latest().changes_display_dict["Product No."][1], + "ASD301301A6", msg="The diff function maps 'sku' as 'Product No.' and can be retrieved.", ) - self.assertTrue( - smm.history.latest().changes_display_dict["Version"][1] == "2.1.5", + self.assertEqual( + smm.history.latest().changes_display_dict["Version"][1], + "2.1.5", msg=( "The diff function maps 'vtxt' as 'Version' through verbose_name" " setting on the model field and can be retrieved." ), ) - self.assertTrue( - smm.history.latest().changes_display_dict["not mapped"][1] == "Not mapped", + self.assertEqual( + smm.history.latest().changes_display_dict["not mapped"][1], + "Not mapped", msg=( "The diff function uses the django default verbose name for 'not_mapped'" " and can be retrieved." @@ -318,8 +323,8 @@ class AdditionalDataModelTest(TestCase): label="Additional data to log entries", related=related_model ) obj_with_additional_data.save() - self.assertTrue( - obj_with_additional_data.history.count() == 1, msg="There is 1 log entry" + self.assertEqual( + obj_with_additional_data.history.count(), 1, msg="There is 1 log entry" ) log_entry = obj_with_additional_data.history.get() # FIXME: Work-around for the fact that additional_data isn't working @@ -329,12 +334,14 @@ class AdditionalDataModelTest(TestCase): else: extra_data = log_entry.additional_data self.assertIsNotNone(extra_data) - self.assertTrue( - extra_data["related_model_text"] == related_model.text, + self.assertEqual( + extra_data["related_model_text"], + related_model.text, msg="Related model's text is logged", ) - self.assertTrue( - extra_data["related_model_id"] == related_model.id, + self.assertEqual( + extra_data["related_model_id"], + related_model.id, msg="Related model's id is logged", ) @@ -357,7 +364,7 @@ class DateTimeFieldModelTest(TestCase): naive_dt=self.now, ) dtm.save() - self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") + self.assertEqual(dtm.history.count(), 1, msg="There is one log entry") # Change timestamp to same datetime and timezone timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) @@ -367,7 +374,7 @@ class DateTimeFieldModelTest(TestCase): dtm.save() # Nothing should have changed - self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") + self.assertEqual(dtm.history.count(), 1, msg="There is one log entry") def test_model_with_different_timezone(self): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) @@ -381,7 +388,7 @@ class DateTimeFieldModelTest(TestCase): naive_dt=self.now, ) dtm.save() - self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") + self.assertEqual(dtm.history.count(), 1, msg="There is one log entry") # Change timestamp to same datetime in another timezone timestamp = datetime.datetime(2017, 1, 10, 13, 0, tzinfo=self.utc_plus_one) @@ -389,7 +396,7 @@ class DateTimeFieldModelTest(TestCase): dtm.save() # Nothing should have changed - self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") + self.assertEqual(dtm.history.count(), 1, msg="There is one log entry") def test_model_with_different_datetime(self): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) @@ -403,7 +410,7 @@ class DateTimeFieldModelTest(TestCase): naive_dt=self.now, ) dtm.save() - self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") + self.assertEqual(dtm.history.count(), 1, msg="There is one log entry") # Change timestamp to another datetime in the same timezone timestamp = datetime.datetime(2017, 1, 10, 13, 0, tzinfo=timezone.utc) @@ -411,7 +418,7 @@ class DateTimeFieldModelTest(TestCase): dtm.save() # The time should have changed. - self.assertTrue(dtm.history.count() == 2, msg="There are two log entries") + self.assertEqual(dtm.history.count(), 2, msg="There are two log entries") def test_model_with_different_date(self): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) @@ -425,7 +432,7 @@ class DateTimeFieldModelTest(TestCase): naive_dt=self.now, ) dtm.save() - self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") + self.assertEqual(dtm.history.count(), 1, msg="There is one log entry") # Change timestamp to another datetime in the same timezone date = datetime.datetime(2017, 1, 11) @@ -433,7 +440,7 @@ class DateTimeFieldModelTest(TestCase): dtm.save() # The time should have changed. - self.assertTrue(dtm.history.count() == 2, msg="There are two log entries") + self.assertEqual(dtm.history.count(), 2, msg="There are two log entries") def test_model_with_different_time(self): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) @@ -447,7 +454,7 @@ class DateTimeFieldModelTest(TestCase): naive_dt=self.now, ) dtm.save() - self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") + self.assertEqual(dtm.history.count(), 1, msg="There is one log entry") # Change timestamp to another datetime in the same timezone time = datetime.time(6, 0) @@ -455,7 +462,7 @@ class DateTimeFieldModelTest(TestCase): dtm.save() # The time should have changed. - self.assertTrue(dtm.history.count() == 2, msg="There are two log entries") + self.assertEqual(dtm.history.count(), 2, msg="There are two log entries") def test_model_with_different_time_and_timezone(self): timestamp = datetime.datetime(2017, 1, 10, 12, 0, tzinfo=timezone.utc) @@ -469,7 +476,7 @@ class DateTimeFieldModelTest(TestCase): naive_dt=self.now, ) dtm.save() - self.assertTrue(dtm.history.count() == 1, msg="There is one log entry") + self.assertEqual(dtm.history.count(), 1, msg="There is one log entry") # Change timestamp to another datetime and another timezone timestamp = datetime.datetime(2017, 1, 10, 14, 0, tzinfo=self.utc_plus_one) @@ -477,7 +484,7 @@ class DateTimeFieldModelTest(TestCase): dtm.save() # The time should have changed. - self.assertTrue(dtm.history.count() == 2, msg="There are two log entries") + self.assertEqual(dtm.history.count(), 2, msg="There are two log entries") def test_changes_display_dict_datetime(self): timestamp = datetime.datetime(2017, 1, 10, 15, 0, tzinfo=timezone.utc) @@ -492,9 +499,9 @@ class DateTimeFieldModelTest(TestCase): ) dtm.save() localized_timestamp = timestamp.astimezone(gettz(settings.TIME_ZONE)) - self.assertTrue( - dtm.history.latest().changes_display_dict["timestamp"][1] - == dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), + self.assertEqual( + dtm.history.latest().changes_display_dict["timestamp"][1], + dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), msg=( "The datetime should be formatted according to Django's settings for" " DATETIME_FORMAT" @@ -504,9 +511,9 @@ class DateTimeFieldModelTest(TestCase): dtm.timestamp = timestamp dtm.save() localized_timestamp = timestamp.astimezone(gettz(settings.TIME_ZONE)) - self.assertTrue( - dtm.history.latest().changes_display_dict["timestamp"][1] - == dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), + self.assertEqual( + dtm.history.latest().changes_display_dict["timestamp"][1], + dateformat.format(localized_timestamp, settings.DATETIME_FORMAT), msg=( "The datetime should be formatted according to Django's settings for" " DATETIME_FORMAT" @@ -515,9 +522,9 @@ class DateTimeFieldModelTest(TestCase): # Change USE_L10N = True with self.settings(USE_L10N=True, LANGUAGE_CODE="en-GB"): - self.assertTrue( - dtm.history.latest().changes_display_dict["timestamp"][1] - == formats.localize(localized_timestamp), + self.assertEqual( + dtm.history.latest().changes_display_dict["timestamp"][1], + formats.localize(localized_timestamp), msg=( "The datetime should be formatted according to Django's settings for" " USE_L10N is True with a different LANGUAGE_CODE." @@ -536,9 +543,9 @@ class DateTimeFieldModelTest(TestCase): naive_dt=self.now, ) dtm.save() - self.assertTrue( - dtm.history.latest().changes_display_dict["date"][1] - == dateformat.format(date, settings.DATE_FORMAT), + self.assertEqual( + dtm.history.latest().changes_display_dict["date"][1], + dateformat.format(date, settings.DATE_FORMAT), msg=( "The date should be formatted according to Django's settings for" " DATE_FORMAT unless USE_L10N is True." @@ -547,9 +554,9 @@ class DateTimeFieldModelTest(TestCase): date = datetime.date(2017, 1, 11) dtm.date = date dtm.save() - self.assertTrue( - dtm.history.latest().changes_display_dict["date"][1] - == dateformat.format(date, settings.DATE_FORMAT), + self.assertEqual( + dtm.history.latest().changes_display_dict["date"][1], + dateformat.format(date, settings.DATE_FORMAT), msg=( "The date should be formatted according to Django's settings for" " DATE_FORMAT unless USE_L10N is True." @@ -558,9 +565,9 @@ class DateTimeFieldModelTest(TestCase): # Change USE_L10N = True with self.settings(USE_L10N=True, LANGUAGE_CODE="en-GB"): - self.assertTrue( - dtm.history.latest().changes_display_dict["date"][1] - == formats.localize(date), + self.assertEqual( + dtm.history.latest().changes_display_dict["date"][1], + formats.localize(date), msg=( "The date should be formatted according to Django's settings for" " USE_L10N is True with a different LANGUAGE_CODE." @@ -579,9 +586,9 @@ class DateTimeFieldModelTest(TestCase): naive_dt=self.now, ) dtm.save() - self.assertTrue( - dtm.history.latest().changes_display_dict["time"][1] - == dateformat.format(time, settings.TIME_FORMAT), + self.assertEqual( + dtm.history.latest().changes_display_dict["time"][1], + dateformat.format(time, settings.TIME_FORMAT), msg=( "The time should be formatted according to Django's settings for" " TIME_FORMAT unless USE_L10N is True." @@ -590,9 +597,9 @@ class DateTimeFieldModelTest(TestCase): time = datetime.time(6, 0) dtm.time = time dtm.save() - self.assertTrue( - dtm.history.latest().changes_display_dict["time"][1] - == dateformat.format(time, settings.TIME_FORMAT), + self.assertEqual( + dtm.history.latest().changes_display_dict["time"][1], + dateformat.format(time, settings.TIME_FORMAT), msg=( "The time should be formatted according to Django's settings for" " TIME_FORMAT unless USE_L10N is True." @@ -601,9 +608,9 @@ class DateTimeFieldModelTest(TestCase): # Change USE_L10N = True with self.settings(USE_L10N=True, LANGUAGE_CODE="en-GB"): - self.assertTrue( - dtm.history.latest().changes_display_dict["time"][1] - == formats.localize(time), + self.assertEqual( + dtm.history.latest().changes_display_dict["time"][1], + formats.localize(time), msg=( "The time should be formatted according to Django's settings for" " USE_L10N is True with a different LANGUAGE_CODE." @@ -643,7 +650,7 @@ class UnregisterTest(TestCase): obj = self.obj # Check for log entries - self.assertTrue(obj.history.count() == 0, msg="There are no log entries") + self.assertEqual(obj.history.count(), 0, msg="There are no log entries") def test_unregister_update(self): """Updates are not logged after unregistering.""" @@ -655,7 +662,7 @@ class UnregisterTest(TestCase): obj.save() # Check for log entries - self.assertTrue(obj.history.count() == 0, msg="There are no log entries") + self.assertEqual(obj.history.count(), 0, msg="There are no log entries") def test_unregister_delete(self): """Deletion is not logged after unregistering.""" @@ -666,7 +673,7 @@ class UnregisterTest(TestCase): obj.delete() # Check for log entries - self.assertTrue(LogEntry.objects.count() == 0, msg="There are no log entries") + self.assertEqual(LogEntry.objects.count(), 0, msg="There are no log entries") class ChoicesFieldModelTest(TestCase): @@ -682,28 +689,30 @@ class ChoicesFieldModelTest(TestCase): def test_changes_display_dict_single_choice(self): - self.assertTrue( - self.obj.history.latest().changes_display_dict["status"][1] == "Red", + self.assertEqual( + self.obj.history.latest().changes_display_dict["status"][1], + "Red", msg="The human readable text 'Red' is displayed.", ) self.obj.status = ChoicesFieldModel.GREEN self.obj.save() - self.assertTrue( - self.obj.history.latest().changes_display_dict["status"][1] == "Green", + self.assertEqual( + self.obj.history.latest().changes_display_dict["status"][1], + "Green", msg="The human readable text 'Green' is displayed.", ) def test_changes_display_dict_multiplechoice(self): - self.assertTrue( - self.obj.history.latest().changes_display_dict["multiplechoice"][1] - == "Red, Yellow, Green", + self.assertEqual( + self.obj.history.latest().changes_display_dict["multiplechoice"][1], + "Red, Yellow, Green", msg="The human readable text 'Red, Yellow, Green' is displayed.", ) self.obj.multiplechoice = ChoicesFieldModel.RED self.obj.save() - self.assertTrue( - self.obj.history.latest().changes_display_dict["multiplechoice"][1] - == "Red", + self.assertEqual( + self.obj.history.latest().changes_display_dict["multiplechoice"][1], + "Red", msg="The human readable text 'Red' is displayed.", ) @@ -718,32 +727,32 @@ class CharfieldTextfieldModelTest(TestCase): ) def test_changes_display_dict_longchar(self): - self.assertTrue( - self.obj.history.latest().changes_display_dict["longchar"][1] - == "{}...".format(self.PLACEHOLDER_LONGCHAR[:140]), + self.assertEqual( + self.obj.history.latest().changes_display_dict["longchar"][1], + "{}...".format(self.PLACEHOLDER_LONGCHAR[:140]), msg="The string should be truncated at 140 characters with an ellipsis at the end.", ) SHORTENED_PLACEHOLDER = self.PLACEHOLDER_LONGCHAR[:139] self.obj.longchar = SHORTENED_PLACEHOLDER self.obj.save() - self.assertTrue( - self.obj.history.latest().changes_display_dict["longchar"][1] - == SHORTENED_PLACEHOLDER, + self.assertEqual( + self.obj.history.latest().changes_display_dict["longchar"][1], + SHORTENED_PLACEHOLDER, msg="The field should display the entire string because it is less than 140 characters", ) def test_changes_display_dict_longtextfield(self): - self.assertTrue( - self.obj.history.latest().changes_display_dict["longtextfield"][1] - == "{}...".format(self.PLACEHOLDER_LONGTEXTFIELD[:140]), + self.assertEqual( + self.obj.history.latest().changes_display_dict["longtextfield"][1], + "{}...".format(self.PLACEHOLDER_LONGTEXTFIELD[:140]), msg="The string should be truncated at 140 characters with an ellipsis at the end.", ) SHORTENED_PLACEHOLDER = self.PLACEHOLDER_LONGTEXTFIELD[:139] self.obj.longtextfield = SHORTENED_PLACEHOLDER self.obj.save() - self.assertTrue( - self.obj.history.latest().changes_display_dict["longtextfield"][1] - == SHORTENED_PLACEHOLDER, + self.assertEqual( + self.obj.history.latest().changes_display_dict["longtextfield"][1], + SHORTENED_PLACEHOLDER, msg="The field should display the entire string because it is less than 140 characters", ) @@ -761,26 +770,28 @@ class PostgresArrayFieldModelTest(TestCase): return self.obj.history.latest().changes_display_dict["arrayfield"][1] def test_changes_display_dict_arrayfield(self): - self.assertTrue( - self.latest_array_change == "Red, Green", + self.assertEqual( + self.latest_array_change, + "Red, Green", msg="The human readable text for the two choices, 'Red, Green' is displayed.", ) self.obj.arrayfield = [PostgresArrayFieldModel.GREEN] self.obj.save() - self.assertTrue( - self.latest_array_change == "Green", + self.assertEqual( + self.latest_array_change, + "Green", msg="The human readable text 'Green' is displayed.", ) self.obj.arrayfield = [] self.obj.save() - self.assertTrue( - self.latest_array_change == "", - msg="The human readable text '' is displayed.", + self.assertEqual( + self.latest_array_change, "", msg="The human readable text '' is displayed." ) self.obj.arrayfield = [PostgresArrayFieldModel.GREEN] self.obj.save() - self.assertTrue( - self.latest_array_change == "Green", + self.assertEqual( + self.latest_array_change, + "Green", msg="The human readable text 'Green' is displayed.", ) From f6479662109b9e60905ba4707bae4d5eacd7c48b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Mon, 5 Oct 2020 13:22:22 +0300 Subject: [PATCH 40/41] Stop handling an impossible case We check eliminate the case with zero log entries when checking that obj.history.count() is exactly 1. --- auditlog_tests/tests.py | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index ee267cc..69c2a4b 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -44,17 +44,12 @@ class SimpleModelTest(TestCase): # Check for log entries self.assertEqual(obj.history.count(), 1, msg="There is one log entry") - try: - history = obj.history.get() - except obj.history.DoesNotExist: - self.assertTrue(False, "Log entry exists") - else: - self.assertEqual( - history.action, LogEntry.Action.CREATE, msg="Action is 'CREATE'" - ) - self.assertEqual( - history.object_repr, str(obj), msg="Representation is equal" - ) + history = obj.history.get() + + self.assertEqual( + history.action, LogEntry.Action.CREATE, msg="Action is 'CREATE'" + ) + self.assertEqual(history.object_repr, str(obj), msg="Representation is equal") def test_update(self): """Updates are logged correctly.""" From 5d2bc88b2dfbc3ca04fc9eff3d484eef27bb9c94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alieh=20Ryma=C5=A1e=C5=ADski?= Date: Mon, 5 Oct 2020 14:33:27 +0300 Subject: [PATCH 41/41] Test LogEntry.actor field --- auditlog_tests/tests.py | 146 +++++++++++++++++++++++++++++++++------- 1 file changed, 122 insertions(+), 24 deletions(-) diff --git a/auditlog_tests/tests.py b/auditlog_tests/tests.py index 69c2a4b..c8bbd35 100644 --- a/auditlog_tests/tests.py +++ b/auditlog_tests/tests.py @@ -1,15 +1,19 @@ import datetime +import itertools import json import django import mock from dateutil.tz import gettz from django.conf import settings +from django.contrib.auth import get_user_model from django.contrib.auth.models import AnonymousUser, User +from django.contrib.contenttypes.models import ContentType from django.db.models.signals import pre_save from django.test import RequestFactory, TestCase from django.utils import dateformat, formats, timezone +from auditlog.context import set_actor from auditlog.middleware import AuditlogMiddleware from auditlog.models import LogEntry from auditlog.registry import auditlog @@ -34,7 +38,11 @@ from auditlog_tests.models import ( class SimpleModelTest(TestCase): def setUp(self): - self.obj = SimpleModel.objects.create(text="I am not difficult.") + self.obj = self.make_object() + super().setUp() + + def make_object(self): + return SimpleModel.objects.create(text="I am not difficult.") def test_create(self): """Creation is logged correctly.""" @@ -45,7 +53,9 @@ class SimpleModelTest(TestCase): self.assertEqual(obj.history.count(), 1, msg="There is one log entry") history = obj.history.get() + self.check_create_log_entry(obj, history) + def check_create_log_entry(self, obj, history): self.assertEqual( history.action, LogEntry.Action.CREATE, msg="Action is 'CREATE'" ) @@ -57,8 +67,7 @@ class SimpleModelTest(TestCase): obj = self.obj # Change something - obj.boolean = True - obj.save() + self.update(obj) # Check for log entries self.assertEqual( @@ -68,7 +77,13 @@ class SimpleModelTest(TestCase): ) history = obj.history.get(action=LogEntry.Action.UPDATE) + self.check_update_log_entry(obj, history) + def update(self, obj): + obj.boolean = True + obj.save() + + def check_update_log_entry(self, obj, history): self.assertJSONEqual( history.changes, '{"boolean": ["False", "True"]}', @@ -79,39 +94,104 @@ class SimpleModelTest(TestCase): """Deletion is logged correctly.""" # Get the object to work with obj = self.obj - - history = obj.history.latest() + content_type = ContentType.objects.get_for_model(obj.__class__) + pk = obj.pk # Delete the object - obj.delete() + self.delete(obj) # Check for log entries - self.assertEqual( - LogEntry.objects.filter( - content_type=history.content_type, - object_pk=history.object_pk, - action=LogEntry.Action.DELETE, - ).count(), - 1, - msg="There is one log entry for 'DELETE'", - ) + qs = LogEntry.objects.filter(content_type=content_type, object_pk=pk) + self.assertEqual(qs.count(), 1, msg="There is one log entry for 'DELETE'") + + history = qs.get() + self.check_delete_log_entry(obj, history) + + def delete(self, obj): + obj.delete() + + def check_delete_log_entry(self, obj, history): + pass def test_recreate(self): - SimpleModel.objects.all().delete() + self.obj.delete() self.setUp() self.test_create() -class AltPrimaryKeyModelTest(SimpleModelTest): +class NoActorMixin: + def check_create_log_entry(self, obj, log_entry): + super().check_create_log_entry(obj, log_entry) + self.assertIsNone(log_entry.actor) + + def check_update_log_entry(self, obj, log_entry): + super().check_update_log_entry(obj, log_entry) + self.assertIsNone(log_entry.actor) + + def check_delete_log_entry(self, obj, log_entry): + super().check_delete_log_entry(obj, log_entry) + self.assertIsNone(log_entry.actor) + + +class WithActorMixin: + sequence = itertools.count() + def setUp(self): - self.obj = AltPrimaryKeyModel.objects.create( + username = "actor_{}".format(next(self.sequence)) + self.user = get_user_model().objects.create( + username=username, + email="{}@example.com".format(username), + password="secret", + ) + super().setUp() + + def tearDown(self): + self.user.delete() + super().tearDown() + + def make_object(self): + with set_actor(self.user): + return super().make_object() + + def check_create_log_entry(self, obj, log_entry): + super().check_create_log_entry(obj, log_entry) + self.assertEqual(log_entry.actor, self.user) + + def update(self, obj): + with set_actor(self.user): + return super().update(obj) + + def check_update_log_entry(self, obj, log_entry): + super().check_update_log_entry(obj, log_entry) + self.assertEqual(log_entry.actor, self.user) + + def delete(self, obj): + with set_actor(self.user): + return super().delete(obj) + + def check_delete_log_entry(self, obj, log_entry): + super().check_delete_log_entry(obj, log_entry) + self.assertEqual(log_entry.actor, self.user) + + +class AltPrimaryKeyModelBase(SimpleModelTest): + def make_object(self): + return AltPrimaryKeyModel.objects.create( key=str(datetime.datetime.now()), text="I am strange." ) -class UUIDPrimaryKeyModelModelTest(SimpleModelTest): - def setUp(self): - self.obj = UUIDPrimaryKeyModel.objects.create(text="I am strange.") +class AltPrimaryKeyModelTest(NoActorMixin, AltPrimaryKeyModelBase): + pass + + +class AltPrimaryKeyModelWithActorTest(WithActorMixin, AltPrimaryKeyModelBase): + pass + + +class UUIDPrimaryKeyModelModelBase(SimpleModelTest): + def make_object(self): + return UUIDPrimaryKeyModel.objects.create(text="I am strange.") def test_get_for_object(self): self.obj.boolean = True @@ -129,9 +209,27 @@ class UUIDPrimaryKeyModelModelTest(SimpleModelTest): ) -class ProxyModelTest(SimpleModelTest): - def setUp(self): - self.obj = ProxyModel.objects.create(text="I am not what you think.") +class UUIDPrimaryKeyModelModelTest(NoActorMixin, UUIDPrimaryKeyModelModelBase): + pass + + +class UUIDPrimaryKeyModelModelWithActorTest( + WithActorMixin, UUIDPrimaryKeyModelModelBase +): + pass + + +class ProxyModelBase(SimpleModelTest): + def make_object(self): + return ProxyModel.objects.create(text="I am not what you think.") + + +class ProxyModelTest(NoActorMixin, ProxyModelBase): + pass + + +class ProxyModelWithActorTest(WithActorMixin, ProxyModelBase): + pass class ManyRelatedModelTest(TestCase):