8 | {% blocktrans %}Choose a date from the list below to revert to a previous version of this object.{% endblocktrans %}
{% endif %} 9 |{% trans "This object doesn't have a change history." %}
14 | {% endif %} 15 |├── docs ├── _static │ └── .keep ├── screens │ ├── 2_revert.png │ ├── 1_poll_history.png │ ├── 3_poll_reverted.png │ ├── 10_revert_disabled.png │ ├── 5_history_list_display.png │ └── 4_history_after_poll_reverted.png ├── multiple_dbs.rst ├── utils.rst ├── signals.rst ├── index.rst ├── history_diffing.rst ├── quick_start.rst ├── user_tracking.rst ├── Makefile └── make.bat ├── simple_history ├── tests │ ├── __init__.py │ ├── external │ │ ├── __init__.py │ │ └── models.py │ ├── custom_user │ │ ├── __init__.py │ │ ├── models.py │ │ └── admin.py │ ├── generated_file_checks │ │ ├── __init__.py │ │ └── check_translations.py │ ├── tests │ │ ├── __init__.py │ │ ├── test_deprecation.py │ │ ├── test_templatetags.py │ │ ├── test_index.py │ │ ├── utils.py │ │ └── test_signals.py │ ├── other_admin.py │ ├── urls.py │ ├── admin.py │ └── view.py ├── management │ ├── __init__.py │ └── commands │ │ ├── __init__.py │ │ ├── clean_old_history.py │ │ ├── clean_duplicate_history.py │ │ └── populate_history.py ├── registry_tests │ ├── __init__.py │ ├── models.py │ └── migration_test_app │ │ ├── __init__.py │ │ ├── migrations │ │ ├── __init__.py │ │ ├── 0004_history_date_indexing.py │ │ ├── 0006_alter_historicalmodelwithcustomattronetoonefield_options_and_more.py │ │ ├── 0003_alter_historicalmodelwithcustomattrforeignkey_options_and_more.py │ │ ├── 0007_alter_historicalmodelwithcustomattrforeignkey_options_and_more.py │ │ ├── 0002_historicalmodelwithcustomattrforeignkey_modelwithcustomattrforeignkey.py │ │ ├── 0005_historicalmodelwithcustomattronetoonefield_modelwithcustomattronetoonefield.py │ │ └── 0001_initial.py │ │ └── models.py ├── templatetags │ ├── __init__.py │ ├── simple_history_compat.py │ └── getattributes.py ├── locale │ ├── ar │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── de │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── fr │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── id │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── nb │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pl │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── uk │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ur │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── cs_CZ │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── pt_BR │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ ├── ru_RU │ │ └── LC_MESSAGES │ │ │ ├── django.mo │ │ │ └── django.po │ └── zh_Hans │ │ └── LC_MESSAGES │ │ ├── django.mo │ │ └── django.po ├── templates │ └── simple_history │ │ ├── submit_line.html │ │ ├── object_history.html │ │ ├── object_history_form.html │ │ └── object_history_list.html ├── exceptions.py ├── signals.py ├── middleware.py └── __init__.py ├── requirements ├── test.txt ├── mysql.txt ├── postgres.txt ├── coverage.txt ├── docs.txt ├── lint.txt └── tox.txt ├── doc-requirements.txt ├── .coveragerc ├── .github ├── release.yml ├── dependabot.yml ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── release.yml │ └── test.yml ├── CODE_OF_CONDUCT.md ├── codecov.yml ├── .gitignore ├── .yamllint ├── .editorconfig ├── .readthedocs.yaml ├── Makefile ├── .codeclimate.yml ├── LICENSE.txt ├── PULL_REQUEST_TEMPLATE.md ├── tox.ini ├── .pre-commit-config.yaml ├── README.rst ├── CONTRIBUTING.rst ├── pyproject.toml ├── runtests.py └── AUTHORS.rst /docs/_static/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simple_history/tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simple_history/management/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/test.txt: -------------------------------------------------------------------------------- 1 | -r ./coverage.txt 2 | -------------------------------------------------------------------------------- /simple_history/registry_tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simple_history/registry_tests/models.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simple_history/templatetags/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simple_history/tests/external/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/mysql.txt: -------------------------------------------------------------------------------- 1 | mysqlclient==2.2.7 2 | -------------------------------------------------------------------------------- /simple_history/management/commands/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /simple_history/tests/custom_user/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/postgres.txt: -------------------------------------------------------------------------------- 1 | psycopg[binary]==3.3.2 2 | -------------------------------------------------------------------------------- /simple_history/tests/generated_file_checks/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/coverage.txt: -------------------------------------------------------------------------------- 1 | coverage==7.13.0 2 | toml==0.10.2 3 | -------------------------------------------------------------------------------- /simple_history/registry_tests/migration_test_app/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /doc-requirements.txt: -------------------------------------------------------------------------------- 1 | Sphinx==1.2.3 2 | sphinx-autobuild==0.3.0 3 | -------------------------------------------------------------------------------- /requirements/docs.txt: -------------------------------------------------------------------------------- 1 | Sphinx==8.2.3 2 | sphinx-rtd-theme==3.0.2 3 | -------------------------------------------------------------------------------- /simple_history/registry_tests/migration_test_app/migrations/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements/lint.txt: -------------------------------------------------------------------------------- 1 | black==25.12.0 2 | flake8==7.3.0 3 | isort==7.0.0 4 | -------------------------------------------------------------------------------- /requirements/tox.txt: -------------------------------------------------------------------------------- 1 | -r ./coverage.txt 2 | tox==4.32.0 3 | tox-gh-actions==3.5.0 4 | -------------------------------------------------------------------------------- /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | include = simple_history/* 3 | omit = simple_history/tests/* 4 | branch = 1 5 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | changelog: 3 | exclude: 4 | authors: 5 | - dependabot 6 | - pre-commit-ci 7 | -------------------------------------------------------------------------------- /docs/screens/2_revert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/docs/screens/2_revert.png -------------------------------------------------------------------------------- /docs/screens/1_poll_history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/docs/screens/1_poll_history.png -------------------------------------------------------------------------------- /docs/screens/3_poll_reverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/docs/screens/3_poll_reverted.png -------------------------------------------------------------------------------- /docs/screens/10_revert_disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/docs/screens/10_revert_disabled.png -------------------------------------------------------------------------------- /docs/screens/5_history_list_display.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/docs/screens/5_history_list_display.png -------------------------------------------------------------------------------- /simple_history/tests/custom_user/models.py: -------------------------------------------------------------------------------- 1 | from django.contrib.auth.models import AbstractUser 2 | 3 | 4 | class CustomUser(AbstractUser): 5 | pass 6 | -------------------------------------------------------------------------------- /docs/screens/4_history_after_poll_reverted.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/docs/screens/4_history_after_poll_reverted.png -------------------------------------------------------------------------------- /simple_history/locale/ar/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/ar/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/de/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/de/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/fr/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/fr/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/id/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/id/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/nb/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/nb/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/pl/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/pl/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/uk/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/uk/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/ur/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/ur/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/tests/tests/__init__.py: -------------------------------------------------------------------------------- 1 | from .test_admin import * 2 | from .test_commands import * 3 | from .test_manager import * 4 | from .test_models import * 5 | -------------------------------------------------------------------------------- /simple_history/locale/cs_CZ/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/cs_CZ/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/pt_BR/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/pt_BR/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/ru_RU/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/ru_RU/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/locale/zh_Hans/LC_MESSAGES/django.mo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/django-commons/django-simple-history/HEAD/simple_history/locale/zh_Hans/LC_MESSAGES/django.mo -------------------------------------------------------------------------------- /simple_history/templatetags/simple_history_compat.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | from django.template.defaulttags import url 3 | 4 | register = template.Library() 5 | 6 | register.tag(url) 7 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | The `django-simple-history` project utilizes the [Django Commons Code of Conduct](https://github.com/django-commons/membership/blob/main/CODE_OF_CONDUCT.md). 4 | -------------------------------------------------------------------------------- /simple_history/tests/custom_user/admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.contrib.auth.admin import UserAdmin 3 | 4 | from .models import CustomUser 5 | 6 | admin.site.register(CustomUser, UserAdmin) 7 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | --- 2 | coverage: 3 | status: 4 | patch: 5 | default: 6 | informational: true 7 | project: 8 | default: 9 | informational: true 10 | 11 | ignore: 12 | - "requirements/*.txt" 13 | -------------------------------------------------------------------------------- /simple_history/tests/other_admin.py: -------------------------------------------------------------------------------- 1 | from django.contrib.admin.sites import AdminSite 2 | 3 | from simple_history.admin import SimpleHistoryAdmin 4 | 5 | from .models import State 6 | 7 | site = AdminSite(name="other_admin") 8 | 9 | site.register(State, SimpleHistoryAdmin) 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg 2 | *.egg-info/ 3 | *.eggs 4 | *.pyc 5 | .coverage 6 | .idea 7 | .tox/ 8 | .venv/ 9 | .python-version 10 | /.project 11 | /.pydevproject 12 | /.ve 13 | build/ 14 | dist/ 15 | docs/_build 16 | htmlcov/ 17 | MANIFEST 18 | test_files/ 19 | venv/ 20 | .DS_Store 21 | env 22 | .vscode 23 | -------------------------------------------------------------------------------- /simple_history/templatetags/getattributes.py: -------------------------------------------------------------------------------- 1 | from django import template 2 | 3 | register = template.Library() 4 | 5 | 6 | def getattribute(value, arg): 7 | """Gets an attribute of an object dynamically from a string name""" 8 | 9 | return getattr(value, arg, None) 10 | 11 | 12 | register.filter("getattribute", getattribute) 13 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | # allow "on" until yamllint stops checking keys for truthy! 2 | # https://github.com/adrienverge/yamllint/issues/158 3 | --- 4 | extends: default 5 | 6 | rules: 7 | comments-indentation: disable 8 | braces: disable 9 | line-length: 10 | max: 120 11 | truthy: 12 | level: error 13 | allowed-values: ['true', 'false', 'on'] 14 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: "pip" 5 | directory: "/requirements" 6 | schedule: 7 | interval: "daily" 8 | 9 | - package-ecosystem: "github-actions" 10 | # Workflow files stored in the default location of `.github/workflows` 11 | directory: "/" 12 | schedule: 13 | interval: "weekly" 14 | -------------------------------------------------------------------------------- /simple_history/tests/tests/test_deprecation.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | 3 | 4 | class DeprecationWarningTest(unittest.TestCase): 5 | """Tests that check whether ``DeprecationWarning`` is raised for certain features, 6 | and that compare ``simple_history.__version__`` against the version the features 7 | will be removed in. 8 | 9 | If this class is empty, it normally means that nothing is currently deprecated. 10 | """ 11 | -------------------------------------------------------------------------------- /simple_history/templates/simple_history/submit_line.html: -------------------------------------------------------------------------------- 1 | {% load i18n %} 2 |
8 | {% blocktrans %}Choose a date from the list below to revert to a previous version of this object.{% endblocktrans %}
{% endif %} 9 |{% trans "This object doesn't have a change history." %}
14 | {% endif %} 15 |{% if not revert_disabled %}{% blocktrans %}Press the 'Revert' button below to revert to this version of the object.{% endblocktrans %}{% endif %}{% if change_history %}{% blocktrans %}Press the 'Change History' button below to edit the history.{% endblocktrans %}{% endif %}
34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ## Description 4 | 5 | 6 | ## Related Issue 7 | 8 | 9 | 10 | 11 | 12 | ## Motivation and Context 13 | 14 | 15 | ## How Has This Been Tested? 16 | 17 | 18 | 19 | 20 | ## Screenshots (if appropriate): 21 | 22 | ## Types of changes 23 | 24 | - [ ] Bug fix (non-breaking change which fixes an issue) 25 | - [ ] New feature (non-breaking change which adds functionality) 26 | - [ ] Breaking change (fix or feature that would cause existing functionality to change) 27 | 28 | ## Checklist: 29 | 30 | 31 | - [ ] I have run the `pre-commit run` command to format and lint. 32 | - [ ] My change requires a change to the documentation. 33 | - [ ] I have updated the documentation accordingly. 34 | - [ ] I have read the **CONTRIBUTING** document. 35 | - [ ] I have added tests to cover my changes. 36 | - [ ] I have added my name and/or github handle to `AUTHORS.rst` 37 | - [ ] I have added my change to `CHANGES.rst` 38 | - [ ] All new and existing tests passed. 39 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = 3 | py3{9-13}-dj42-{sqlite3,postgres,mysql,mariadb}, 4 | py3{10-13}-dj{50-52}-{sqlite3,postgres,mysql,mariadb}, 5 | py3{12-13}-dj{60,main}-{sqlite3,postgres,mysql,mariadb}, 6 | py314-dj{52,60,main}-{sqlite3,postgres,mysql,mariadb}, 7 | docs, 8 | lint 9 | 10 | [gh-actions] 11 | python = 12 | 3.10: py310 13 | 3.11: py311, docs, lint 14 | 3.12: py312 15 | 3.13: py313 16 | 3.14: py314 17 | 18 | [gh-actions:env] 19 | DJANGO = 20 | 4.2: dj42 21 | 5.0: dj50 22 | 5.1: dj51 23 | 5.2: dj52 24 | 6.0: dj60 25 | main: djmain 26 | 27 | [flake8] 28 | ignore = N802,F401,W503 29 | max-complexity = 10 30 | max-line-length = 88 31 | exclude = __init__.py,simple_history/registry_tests/migration_test_app/migrations/* 32 | 33 | [testenv] 34 | deps = 35 | -rrequirements/test.txt 36 | dj42: Django>=4.2,<4.3 37 | dj50: Django>=5.0,<5.1 38 | dj51: Django>=5.1,<5.2 39 | dj52: Django>=5.2,<5.3 40 | dj60: Django>=6.0,<6.1 41 | djmain: https://github.com/django/django/tarball/main 42 | postgres: -rrequirements/postgres.txt 43 | mysql: -rrequirements/mysql.txt 44 | mariadb: -rrequirements/mysql.txt 45 | 46 | commands = 47 | sqlite3: coverage run -a runtests.py {posargs} 48 | postgres: coverage run -a runtests.py --database=postgres {posargs} 49 | mysql: coverage run -a runtests.py --database=mysql {posargs} 50 | mariadb: coverage run -a runtests.py --database=mariadb {posargs} 51 | coverage report 52 | 53 | [testenv:format] 54 | deps = -rrequirements/lint.txt 55 | commands = 56 | isort docs simple_history runtests.py 57 | black docs simple_history runtests.py 58 | flake8 simple_history 59 | 60 | [testenv:lint] 61 | deps = pre-commit 62 | commands = 63 | pre-commit run --all-files 64 | 65 | [testenv:docs] 66 | changedir = docs 67 | deps = -rrequirements/docs.txt 68 | commands = sphinx-build -W -b html -d {envtmpdir}/doctrees . {envtmpdir}/html 69 | -------------------------------------------------------------------------------- /simple_history/registry_tests/migration_test_app/models.py: -------------------------------------------------------------------------------- 1 | from django.db import models 2 | 3 | from simple_history.models import HistoricalRecords 4 | 5 | 6 | class DoYouKnow(models.Model): 7 | pass 8 | 9 | 10 | class WhatIMean(DoYouKnow): 11 | pass 12 | 13 | 14 | class Yar(models.Model): 15 | what = models.ForeignKey(WhatIMean, on_delete=models.CASCADE) 16 | history = HistoricalRecords() 17 | 18 | 19 | class CustomAttrNameForeignKey(models.ForeignKey): 20 | def __init__(self, *args, **kwargs): 21 | self.attr_name = kwargs.pop("attr_name", None) 22 | super().__init__(*args, **kwargs) 23 | 24 | def get_attname(self): 25 | return self.attr_name or super().get_attname() 26 | 27 | def deconstruct(self): 28 | name, path, args, kwargs = super().deconstruct() 29 | if self.attr_name: 30 | kwargs["attr_name"] = self.attr_name 31 | return name, path, args, kwargs 32 | 33 | 34 | class ModelWithCustomAttrForeignKey(models.Model): 35 | what_i_mean = CustomAttrNameForeignKey( 36 | WhatIMean, models.CASCADE, attr_name="custom_attr_name" 37 | ) 38 | history = HistoricalRecords() 39 | 40 | 41 | class CustomAttrNameOneToOneField(models.OneToOneField): 42 | def __init__(self, *args, **kwargs): 43 | self.attr_name = kwargs.pop("attr_name", None) 44 | super().__init__(*args, **kwargs) 45 | 46 | def get_attname(self): 47 | return self.attr_name or super().get_attname() 48 | 49 | def deconstruct(self): 50 | name, path, args, kwargs = super().deconstruct() 51 | if self.attr_name: 52 | kwargs["attr_name"] = self.attr_name 53 | return name, path, args, kwargs 54 | 55 | 56 | class ModelWithCustomAttrOneToOneField(models.Model): 57 | what_i_mean = CustomAttrNameOneToOneField( 58 | WhatIMean, models.CASCADE, attr_name="custom_attr_name" 59 | ) 60 | history = HistoricalRecords(excluded_field_kwargs={"what_i_mean": {"attr_name"}}) 61 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/PyCQA/bandit 4 | rev: 1.8.3 5 | hooks: 6 | - id: bandit 7 | exclude: /.*tests/ 8 | 9 | - repo: https://github.com/psf/black-pre-commit-mirror 10 | rev: 25.1.0 11 | hooks: 12 | - id: black 13 | language_version: python3.10 14 | 15 | - repo: https://github.com/codespell-project/codespell 16 | rev: v2.4.1 17 | hooks: 18 | - id: codespell # See pyproject.toml for args 19 | additional_dependencies: 20 | - tomli 21 | 22 | - repo: https://github.com/pycqa/flake8 23 | rev: 7.2.0 24 | hooks: 25 | - id: flake8 26 | args: 27 | - "--config=tox.ini" 28 | 29 | - repo: https://github.com/PyCQA/isort 30 | rev: 6.0.1 31 | hooks: 32 | - id: isort 33 | 34 | - repo: https://github.com/pre-commit/pre-commit-hooks 35 | rev: v5.0.0 36 | hooks: 37 | - id: requirements-txt-fixer 38 | files: requirements/.*\.txt$ 39 | - id: trailing-whitespace 40 | - id: check-added-large-files 41 | - id: fix-byte-order-marker 42 | - id: check-docstring-first 43 | - id: check-executables-have-shebangs 44 | - id: check-merge-conflict 45 | - id: check-toml 46 | - id: debug-statements 47 | - id: detect-private-key 48 | 49 | - repo: https://github.com/tox-dev/pyproject-fmt 50 | rev: v2.6.0 51 | hooks: 52 | - id: pyproject-fmt 53 | - repo: https://github.com/abravalheri/validate-pyproject 54 | rev: v0.24.1 55 | hooks: 56 | - id: validate-pyproject 57 | 58 | - repo: https://github.com/adrienverge/yamllint 59 | rev: v1.37.1 60 | hooks: 61 | - id: yamllint 62 | args: 63 | - "--strict" 64 | 65 | - repo: https://github.com/asottile/pyupgrade 66 | rev: v3.20.0 67 | hooks: 68 | - id: pyupgrade 69 | args: [--py310-plus] 70 | 71 | - repo: https://github.com/adamchainz/django-upgrade 72 | rev: 1.29.1 73 | hooks: 74 | - id: django-upgrade 75 | -------------------------------------------------------------------------------- /simple_history/tests/urls.py: -------------------------------------------------------------------------------- 1 | from django.contrib import admin 2 | from django.urls import path 3 | 4 | from simple_history.tests.view import ( 5 | BucketDataRegisterRequestUserCreate, 6 | BucketDataRegisterRequestUserDetail, 7 | MockableView, 8 | PollBulkCreateView, 9 | PollBulkCreateWithDefaultUserView, 10 | PollBulkUpdateView, 11 | PollBulkUpdateWithDefaultUserView, 12 | PollCreate, 13 | PollDelete, 14 | PollDetail, 15 | PollList, 16 | PollUpdate, 17 | PollWithHistoricalIPAddressCreate, 18 | ) 19 | 20 | from . import other_admin 21 | 22 | admin.autodiscover() 23 | 24 | urlpatterns = [ 25 | path("admin/", admin.site.urls), 26 | path("other-admin/", other_admin.site.urls), 27 | path( 28 | "bucket_data/add/", 29 | BucketDataRegisterRequestUserCreate.as_view(), 30 | name="bucket_data-add", 31 | ), 32 | path( 33 | "bucket_data/| {% trans 'Object' %} | 10 | {% for column in history_list_display %} 11 |{% trans column %} | 12 | {% endfor %} 13 |{% trans 'Date/time' %} | 14 |{% trans 'Comment' %} | 15 |{% trans 'Changed by' %} | 16 |{% trans 'Change reason' %} | 17 |{% trans 'Changes' %} | 18 |
|---|---|---|---|---|---|---|
| 24 | 25 | {{ record.history_object }} 26 | 27 | | 28 | {% for column in history_list_display %} 29 |{{ record|getattribute:column }} | 30 | {% endfor %} 31 |{{ record.history_date }} | 32 |{{ record.get_history_type_display }} | 33 |34 | {% if record.history_user %} 35 | {% url admin_user_view record.history_user_id as admin_user_url %} 36 | {% if admin_user_url %} 37 | {{ record.history_user }} 38 | {% else %} 39 | {{ record.history_user }} 40 | {% endif %} 41 | {% else %} 42 | {% trans "None" %} 43 | {% endif %} 44 | | 45 |46 | {{ record.history_change_reason }} 47 | | 48 |
49 | {% block history_delta_changes %}
50 | {% if record.history_delta_changes %}
51 |
|
64 |
70 | {% if pagination_required %} 71 | {% for i in page_range %} 72 | {% if i == page_obj.paginator.ELLIPSIS %} 73 | {{ page_obj.paginator.ELLIPSIS }} 74 | {% elif i == page_obj.number %} 75 | {{ i }} 76 | {% else %} 77 | {{ i }} 78 | {% endif %} 79 | {% endfor %} 80 | {% endif %} 81 | {{ page_obj.paginator.count }} {% blocktranslate count counter=page_obj.paginator.count %}entry{% plural %}entries{% endblocktranslate %} 82 |
83 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [build-system] 2 | build-backend = "hatchling.build" 3 | requires = [ 4 | "hatch-fancy-pypi-readme", 5 | "hatch-vcs", 6 | "hatchling", 7 | ] 8 | 9 | [project] 10 | name = "django-simple-history" 11 | description = "Store model history and view/revert changes from admin site." 12 | maintainers = [ 13 | { name = "Trey Hunner" }, 14 | ] 15 | authors = [ 16 | { name = "Corey Bertram", email = "corey@qr7.com" }, 17 | ] 18 | requires-python = ">=3.10" 19 | classifiers = [ 20 | "Development Status :: 5 - Production/Stable", 21 | "Environment :: Web Environment", 22 | "Framework :: Django", 23 | "Framework :: Django :: 4.2", 24 | "Framework :: Django :: 5.0", 25 | "Framework :: Django :: 5.1", 26 | "Framework :: Django :: 5.2", 27 | "Framework :: Django :: 6.0", 28 | "Intended Audience :: Developers", 29 | "License :: OSI Approved :: BSD License", 30 | "Programming Language :: Python", 31 | "Programming Language :: Python :: 3 :: Only", 32 | "Programming Language :: Python :: 3.10", 33 | "Programming Language :: Python :: 3.11", 34 | "Programming Language :: Python :: 3.12", 35 | "Programming Language :: Python :: 3.13", 36 | "Programming Language :: Python :: 3.14", 37 | ] 38 | dynamic = [ 39 | "readme", 40 | "version", 41 | ] 42 | dependencies = [ 43 | "django>=4.2", 44 | ] 45 | urls.Changelog = "https://github.com/django-commons/django-simple-history/blob/master/CHANGES.rst" 46 | urls.Documentation = "https://django-simple-history.readthedocs.io/en/stable/" 47 | urls.Homepage = "https://github.com/django-commons/django-simple-history" 48 | urls.Source = "https://github.com/django-commons/django-simple-history" 49 | urls.Tracker = "https://github.com/django-commons/django-simple-history/issues" 50 | 51 | [tool.hatch.version] 52 | source = "vcs" 53 | fallback-version = "0.0.0" 54 | 55 | [tool.hatch.version.raw-options] 56 | version_scheme = "no-guess-dev" 57 | local_scheme = "node-and-date" 58 | 59 | [tool.hatch.build.targets.wheel] 60 | packages = [ 61 | "simple_history", 62 | ] 63 | exclude = [ 64 | "simple_history/registry_tests", 65 | "simple_history/tests", 66 | ] 67 | 68 | [tool.hatch.metadata.hooks.fancy-pypi-readme] 69 | content-type = "text/x-rst" 70 | # (Preview the generated readme by installing `hatch` and running 71 | # `hatch project metadata readme` - see 72 | # https://github.com/hynek/hatch-fancy-pypi-readme/blob/24.1.0/README.md#cli-interface) 73 | fragments = [ 74 | { path = "README.rst", start-after = ".. Start of PyPI readme\n\n" }, 75 | { text = "\n====\n\nChangelog\n=========\n\n" }, 76 | # Only include the first title after "Unreleased" - as well as the rest of the file 77 | { path = "CHANGES.rst", pattern = "\nUnreleased\n-{4,}\n(?:.*?)\n([^\n]+\n-{4,}\n.*)" }, 78 | ] 79 | 80 | [tool.black] 81 | line-length = 88 82 | target-version = [ 83 | "py310", 84 | ] 85 | 86 | [tool.isort] 87 | profile = "black" 88 | py_version = "310" 89 | 90 | [tool.codespell] 91 | skip = "AUTHORS.rst,*.po" 92 | 93 | [tool.coverage.run] 94 | parallel = true 95 | branch = true 96 | source = [ 97 | "simple_history", 98 | ] 99 | 100 | [tool.coverage.paths] 101 | source = [ 102 | "simple_history", 103 | ".tox/*/site-packages", 104 | ] 105 | 106 | [tool.coverage.report] 107 | show_missing = true 108 | skip_covered = true 109 | omit = [ 110 | "requirements/*", 111 | ] 112 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Publish Python 🐍 distribution 📦 to PyPI 3 | 4 | on: 5 | push: 6 | tags: 7 | # Order matters, the last rule that applies to a tag 8 | # is the one that takes effect: 9 | # https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#example-including-and-excluding-branches-and-tags 10 | - '*' 11 | # There should be no dev tags created, but to be safe, 12 | # let's not publish them. 13 | - '!*.dev*' 14 | 15 | env: 16 | PYPI_URL: https://pypi.org/p/django-simple-history 17 | 18 | jobs: 19 | 20 | build: 21 | name: Build distribution 📦 22 | runs-on: ubuntu-latest 23 | 24 | steps: 25 | - uses: actions/checkout@v6 26 | - name: Set up Python 27 | uses: actions/setup-python@v6 28 | with: 29 | python-version: "3.x" 30 | - name: Install pypa/build 31 | run: 32 | python3 -m pip install build --user 33 | - name: Build a binary wheel and a source tarball 34 | run: python3 -m build 35 | - name: Store the distribution packages 36 | uses: actions/upload-artifact@v5 37 | with: 38 | name: python-package-distributions 39 | path: dist/ 40 | 41 | publish-to-pypi: 42 | name: >- 43 | Publish Python 🐍 distribution 📦 to PyPI 44 | needs: 45 | - build 46 | runs-on: ubuntu-latest 47 | environment: 48 | name: pypi 49 | url: ${{ env.PYPI_URL }} 50 | permissions: 51 | id-token: write # IMPORTANT: mandatory for trusted publishing 52 | steps: 53 | - name: Download all the dists 54 | uses: actions/download-artifact@v6 55 | with: 56 | name: python-package-distributions 57 | path: dist/ 58 | - name: Publish distribution 📦 to PyPI 59 | uses: pypa/gh-action-pypi-publish@release/v1.13 60 | 61 | github-release: 62 | name: >- 63 | Sign the Python 🐍 distribution 📦 with Sigstore 64 | and upload them to GitHub Release 65 | needs: 66 | - publish-to-pypi 67 | runs-on: ubuntu-latest 68 | 69 | permissions: 70 | contents: write # IMPORTANT: mandatory for making GitHub Releases 71 | id-token: write # IMPORTANT: mandatory for sigstore 72 | 73 | steps: 74 | - name: Download all the dists 75 | uses: actions/download-artifact@v6 76 | with: 77 | name: python-package-distributions 78 | path: dist/ 79 | - name: Sign the dists with Sigstore 80 | uses: sigstore/gh-action-sigstore-python@v3.2.0 81 | with: 82 | inputs: >- 83 | ./dist/*.tar.gz 84 | ./dist/*.whl 85 | - name: Create GitHub Release 86 | env: 87 | GITHUB_TOKEN: ${{ github.token }} 88 | run: >- 89 | gh release create 90 | '${{ github.ref_name }}' 91 | --repo '${{ github.repository }}' 92 | --notes "" 93 | - name: Upload artifact signatures to GitHub Release 94 | env: 95 | GITHUB_TOKEN: ${{ github.token }} 96 | # Upload to GitHub Release using the `gh` CLI. 97 | # `dist/` contains the built packages, and the 98 | # sigstore-produced signatures and certificates. 99 | run: >- 100 | gh release upload 101 | '${{ github.ref_name }}' dist/** 102 | --repo '${{ github.repository }}' 103 | -------------------------------------------------------------------------------- /simple_history/registry_tests/migration_test_app/migrations/0002_historicalmodelwithcustomattrforeignkey_modelwithcustomattrforeignkey.py: -------------------------------------------------------------------------------- 1 | # Generated by Django 2.1 on 2018-10-19 21:53 2 | 3 | import django.db.models.deletion 4 | from django.conf import settings 5 | from django.db import migrations, models 6 | 7 | import simple_history.models 8 | 9 | from .. import models as my_models 10 | 11 | 12 | class Migration(migrations.Migration): 13 | dependencies = [ 14 | migrations.swappable_dependency(settings.AUTH_USER_MODEL), 15 | ("migration_test_app", "0001_initial"), 16 | ] 17 | 18 | operations = [ 19 | migrations.CreateModel( 20 | name="HistoricalModelWithCustomAttrForeignKey", 21 | fields=[ 22 | ( 23 | "id", 24 | models.IntegerField( 25 | auto_created=True, blank=True, db_index=True, verbose_name="ID" 26 | ), 27 | ), 28 | ("history_id", models.AutoField(primary_key=True, serialize=False)), 29 | ("history_change_reason", models.CharField(max_length=100, null=True)), 30 | ("history_date", models.DateTimeField()), 31 | ( 32 | "history_type", 33 | models.CharField( 34 | choices=[("+", "Created"), ("~", "Changed"), ("-", "Deleted")], 35 | max_length=1, 36 | ), 37 | ), 38 | ( 39 | "history_user", 40 | models.ForeignKey( 41 | null=True, 42 | on_delete=django.db.models.deletion.SET_NULL, 43 | related_name="+", 44 | to=settings.AUTH_USER_MODEL, 45 | ), 46 | ), 47 | ( 48 | "what_i_mean", 49 | my_models.CustomAttrNameForeignKey( 50 | attr_name="custom_attr_name", 51 | blank=True, 52 | db_constraint=False, 53 | null=True, 54 | on_delete=django.db.models.deletion.DO_NOTHING, 55 | related_name="+", 56 | to="migration_test_app.WhatIMean", 57 | ), 58 | ), 59 | ], 60 | options={ 61 | "verbose_name": "historical model with custom attr foreign key", 62 | "ordering": ("-history_date", "-history_id"), 63 | "get_latest_by": "history_date", 64 | }, 65 | bases=(simple_history.models.HistoricalChanges, models.Model), 66 | ), 67 | migrations.CreateModel( 68 | name="ModelWithCustomAttrForeignKey", 69 | fields=[ 70 | ( 71 | "id", 72 | models.AutoField( 73 | auto_created=True, 74 | primary_key=True, 75 | serialize=False, 76 | verbose_name="ID", 77 | ), 78 | ), 79 | ( 80 | "what_i_mean", 81 | my_models.CustomAttrNameForeignKey( 82 | attr_name="custom_attr_name", 83 | on_delete=django.db.models.deletion.CASCADE, 84 | to="migration_test_app.WhatIMean", 85 | ), 86 | ), 87 | ], 88 | ), 89 | ] 90 | -------------------------------------------------------------------------------- /simple_history/locale/pl/LC_MESSAGES/django.po: -------------------------------------------------------------------------------- 1 | # Polish translation for django-simple-history 2 | # Copyright (C) 2016 3 | # This file is distributed under the same license as the django-simple-history package. 4 | # Grzegorz Bialy