From 308b68e97f2423533c49dc6e46ea0232d17e6a70 Mon Sep 17 00:00:00 2001 From: JensDiemer Date: Mon, 22 Nov 2021 18:38:57 +0100 Subject: [PATCH] Fix #75 protect overwriting a new version with a older one Protect Item, Location and Memo model overwriting a new version with a older one. Add a auto increment "version" number field to these models and check this on every save call... Fix: https://github.com/jedie/PyInventory/issues/75 Use new VersionProtectBaseModel from django-tools: https://github.com/jedie/django-tools/tree/master/django_tools/model_version_protect --- deployment/project.env | 2 +- poetry.lock | 74 +- pyproject.toml | 4 +- src/inventory/__init__.py | 5 +- src/inventory/admin/item.py | 2 +- src/inventory/admin/memo.py | 2 +- .../migrations/0010_version_protect_models.py | 29 + src/inventory/models/item.py | 3 +- src/inventory/models/location.py | 3 +- src/inventory/models/memo.py | 3 +- src/inventory_project/settings/base.py | 3 + .../tests/test_admin_item.py | 151 ++-- ...dmin_item_auto_group_items_1.snapshot.html | 4 +- ...dmin_item_auto_group_items_2.snapshot.html | 4 +- ...l_user_create_minimal_item_1.snapshot.html | 18 +- ...l_user_create_minimal_item_2.snapshot.html | 762 ++++++++++++++++++ .../tests/test_admin_memo.py | 4 + ...l_user_create_minimal_item_1.snapshot.html | 18 +- 18 files changed, 961 insertions(+), 130 deletions(-) create mode 100644 src/inventory/migrations/0010_version_protect_models.py create mode 100644 src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html diff --git a/deployment/project.env b/deployment/project.env index 5da6e6a..04060ff 100644 --- a/deployment/project.env +++ b/deployment/project.env @@ -1,3 +1,3 @@ export PROJECT_NAME=pyinventory export PROJECT_PACKAGE_NAME=pyinventory -export PROJECT_VERSION=0.11.0 +export PROJECT_VERSION=0.12.0.rc1 diff --git a/poetry.lock b/poetry.lock index 194a460..3981e05 100644 --- a/poetry.lock +++ b/poetry.lock @@ -123,7 +123,7 @@ webencodings = "*" [[package]] name = "bx-django-utils" -version = "9" +version = "11" description = "Various Django utility functions" category = "main" optional = false @@ -271,7 +271,7 @@ yaml = ["PyYAML (>=3.10)"] [[package]] name = "cryptography" -version = "35.0.0" +version = "36.0.0" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." category = "main" optional = false @@ -282,7 +282,7 @@ cffi = ">=1.12" [package.extras] docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"] -docstest = ["doc8", "pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] +docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"] pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"] sdist = ["setuptools_rust (>=0.11.4)"] ssh = ["bcrypt (>=3.1.5)"] @@ -374,7 +374,7 @@ django-ipware = ">=3,<5" [[package]] name = "django-ckeditor" -version = "6.1.0" +version = "6.2.0" description = "Django admin CKEditor integration." category = "main" optional = false @@ -488,14 +488,15 @@ devdb = ["psycopg2", "mysqlclient"] [[package]] name = "django-tools" -version = "0.48.3" -description = "miscellaneous tools for django" +version = "0.49.0" +description = "miscellaneous tools for Django based projects" category = "main" optional = false -python-versions = ">=3.6,<4.0.0" +python-versions = ">=3.7,<4.0.0" [package.dependencies] bleach = "*" +bx_py_utils = "*" django = "*" icdiff = "*" pprintpp = "*" @@ -1289,7 +1290,7 @@ jeepney = ">=0.6" [[package]] name = "selenium" -version = "4.0.0" +version = "4.1.0" description = "" category = "dev" optional = false @@ -1620,7 +1621,7 @@ psycopg2-source = ["psycopg2"] [metadata] lock-version = "1.1" python-versions = ">=3.7,<4.0.0" -content-hash = "5eaa826b474da770088d7ca448725afc339023a9ff5493e8496a4f878c844148" +content-hash = "8ecba73cd2070b4092c88aa7219d6dfcbea4133a904b401d80a8b491cf08c187" [metadata.files] asgiref = [ @@ -1669,8 +1670,8 @@ bleach = [ {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"}, ] bx-django-utils = [ - {file = "bx_django_utils-9-py3-none-any.whl", hash = "sha256:3c5e7710abc7084fce567e16c85b598ebd69e5068b426a94935ce7c6e679f4e7"}, - {file = "bx_django_utils-9.tar.gz", hash = "sha256:6acd8e41e18915335aff5aedd71693ff8cc8e99d15cbc37a30c9f25fa4b71eaa"}, + {file = "bx_django_utils-11-py3-none-any.whl", hash = "sha256:b4115fbffb5b788e3ed6c13f5aa9d87991da35f1235de4376ca3e780d03546e9"}, + {file = "bx_django_utils-11.tar.gz", hash = "sha256:a7549c6fd6b9a745e8949c6dfede825433d532c907e86140b7b8fb19cb63945c"}, ] bx-py-utils = [ {file = "bx_py_utils-50-py3-none-any.whl", hash = "sha256:3713ee75904c061eb934d2ab7a842760a8e0fb99db3b69ff5bd6841aa9ef04ae"}, @@ -1810,26 +1811,27 @@ coveralls = [ {file = "coveralls-3.3.1.tar.gz", hash = "sha256:b32a8bb5d2df585207c119d6c01567b81fba690c9c10a753bfe27a335bfc43ea"}, ] cryptography = [ - {file = "cryptography-35.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:d57e0cdc1b44b6cdf8af1d01807db06886f10177469312fbde8f44ccbb284bc9"}, - {file = "cryptography-35.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:ced40344e811d6abba00295ced98c01aecf0c2de39481792d87af4fa58b7b4d6"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:54b2605e5475944e2213258e0ab8696f4f357a31371e538ef21e8d61c843c28d"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7b7ceeff114c31f285528ba8b390d3e9cfa2da17b56f11d366769a807f17cbaa"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2d69645f535f4b2c722cfb07a8eab916265545b3475fdb34e0be2f4ee8b0b15e"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a2d0e0acc20ede0f06ef7aa58546eee96d2592c00f450c9acb89c5879b61992"}, - {file = "cryptography-35.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:07bb7fbfb5de0980590ddfc7f13081520def06dc9ed214000ad4372fb4e3c7f6"}, - {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7eba2cebca600a7806b893cb1d541a6e910afa87e97acf2021a22b32da1df52d"}, - {file = "cryptography-35.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:18d90f4711bf63e2fb21e8c8e51ed8189438e6b35a6d996201ebd98a26abbbe6"}, - {file = "cryptography-35.0.0-cp36-abi3-win32.whl", hash = "sha256:c10c797ac89c746e488d2ee92bd4abd593615694ee17b2500578b63cad6b93a8"}, - {file = "cryptography-35.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:7075b304cd567694dc692ffc9747f3e9cb393cc4aa4fb7b9f3abd6f5c4e43588"}, - {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a688ebcd08250eab5bb5bca318cc05a8c66de5e4171a65ca51db6bd753ff8953"}, - {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d99915d6ab265c22873f1b4d6ea5ef462ef797b4140be4c9d8b179915e0985c6"}, - {file = "cryptography-35.0.0-pp36-pypy36_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:928185a6d1ccdb816e883f56ebe92e975a262d31cc536429041921f8cb5a62fd"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-macosx_10_10_x86_64.whl", hash = "sha256:ebeddd119f526bcf323a89f853afb12e225902a24d29b55fe18dd6fcb2838a76"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:22a38e96118a4ce3b97509443feace1d1011d0571fae81fc3ad35f25ba3ea999"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eb80e8a1f91e4b7ef8b33041591e6d89b2b8e122d787e87eeb2b08da71bb16ad"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:abb5a361d2585bb95012a19ed9b2c8f412c5d723a9836418fab7aaa0243e67d2"}, - {file = "cryptography-35.0.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1ed82abf16df40a60942a8c211251ae72858b25b7421ce2497c2eb7a1cee817c"}, - {file = "cryptography-35.0.0.tar.gz", hash = "sha256:9933f28f70d0517686bd7de36166dda42094eac49415459d9bdf5e7df3e0086d"}, + {file = "cryptography-36.0.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:9511416e85e449fe1de73f7f99b21b3aa04fba4c4d335d30c486ba3756e3a2a6"}, + {file = "cryptography-36.0.0-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:97199a13b772e74cdcdb03760c32109c808aff7cd49c29e9cf4b7754bb725d1d"}, + {file = "cryptography-36.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:494106e9cd945c2cadfce5374fa44c94cfadf01d4566a3b13bb487d2e6c7959e"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6fbbbb8aab4053fa018984bb0e95a16faeb051dd8cca15add2a27e267ba02b58"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:684993ff6f67000a56454b41bdc7e015429732d65a52d06385b6e9de6181c71e"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c702855cd3174666ef0d2d13dcc879090aa9c6c38f5578896407a7028f75b9f"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d91bc9f535599bed58f6d2e21a2724cb0c3895bf41c6403fe881391d29096f1d"}, + {file = "cryptography-36.0.0-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:b17d83b3d1610e571fedac21b2eb36b816654d6f7496004d6a0d32f99d1d8120"}, + {file = "cryptography-36.0.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:8982c19bb90a4fa2aad3d635c6d71814e38b643649b4000a8419f8691f20ac44"}, + {file = "cryptography-36.0.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:24469d9d33217ffd0ce4582dfcf2a76671af115663a95328f63c99ec7ece61a4"}, + {file = "cryptography-36.0.0-cp36-abi3-win32.whl", hash = "sha256:f6a5a85beb33e57998dc605b9dbe7deaa806385fdf5c4810fb849fcd04640c81"}, + {file = "cryptography-36.0.0-cp36-abi3-win_amd64.whl", hash = "sha256:2deab5ec05d83ddcf9b0916319674d3dae88b0e7ee18f8962642d3cde0496568"}, + {file = "cryptography-36.0.0-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2049f8b87f449fc6190350de443ee0c1dd631f2ce4fa99efad2984de81031681"}, + {file = "cryptography-36.0.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a776bae1629c8d7198396fd93ec0265f8dd2341c553dc32b976168aaf0e6a636"}, + {file = "cryptography-36.0.0-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:aa94d617a4cd4cdf4af9b5af65100c036bce22280ebb15d8b5262e8273ebc6ba"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:5c49c9e8fb26a567a2b3fa0343c89f5d325447956cc2fc7231c943b29a973712"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef216d13ac8d24d9cd851776662f75f8d29c9f2d05cdcc2d34a18d32463a9b0b"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:231c4a69b11f6af79c1495a0e5a85909686ea8db946935224b7825cfb53827ed"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:f92556f94e476c1b616e6daec5f7ddded2c082efa7cee7f31c7aeda615906ed8"}, + {file = "cryptography-36.0.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d73e3a96c38173e0aa5646c31bf8473bc3564837977dd480f5cbeacf1d7ef3a3"}, + {file = "cryptography-36.0.0.tar.gz", hash = "sha256:52f769ecb4ef39865719aedc67b4b7eae167bafa48dbc2a26dd36fa56460507f"}, ] defusedxml = [ {file = "defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61"}, @@ -1864,8 +1866,8 @@ django-axes = [ {file = "django_axes-5.27.0-py3-none-any.whl", hash = "sha256:e6ac2266626d4e52a44939bdb861817766f127efe110c8e8ce2a776270954926"}, ] django-ckeditor = [ - {file = "django-ckeditor-6.1.0.tar.gz", hash = "sha256:f0d108f67a81a04e26d8de11255fe314f51026eaf8eb0534a807512ae3c21620"}, - {file = "django_ckeditor-6.1.0-py2.py3-none-any.whl", hash = "sha256:346b26b9d60dc8a88524d0eaaf406f4e91a4b3c22d208ae87aa032bf500b251c"}, + {file = "django-ckeditor-6.2.0.tar.gz", hash = "sha256:df64dc9e62790ef824f609605d31be847bdbce1cc7aa94e49bd5ca60d7aa79bb"}, + {file = "django_ckeditor-6.2.0-py2.py3-none-any.whl", hash = "sha256:9f66420907e41f5b4e698fa5671a00a86995776735f2c4696174aed4640fcbd8"}, ] django-dbbackup = [ {file = "django-dbbackup-3.3.0.tar.gz", hash = "sha256:bb109735cae98b64ad084e5b461b7aca2d7b39992f10c9ed9435e3ebb6fb76c8"}, @@ -1903,8 +1905,8 @@ django-tagulous = [ {file = "django_tagulous-1.3.0-py3-none-any.whl", hash = "sha256:2da8fd03d466fea6769e44df774c69d214b95eec2b6e9b7c6073309bc8781c72"}, ] django-tools = [ - {file = "django-tools-0.48.3.tar.gz", hash = "sha256:08ed2ae606067f49c2c3949055227a826c8b880e5816114925eca386cf1823af"}, - {file = "django_tools-0.48.3-py3-none-any.whl", hash = "sha256:40444fa16b703b7c6960a800ba76aad42472c9aa70040d549a4d91dbb47a5ddb"}, + {file = "django-tools-0.49.0.tar.gz", hash = "sha256:9da6d5610576a34219be3dea9c7c2207669e5c916d89987011843ded772ece0a"}, + {file = "django_tools-0.49.0-py3-none-any.whl", hash = "sha256:a27c32cff98cd82dde23ab2f05fb9e06a32aaeb8357e96f1ec02a634b6619170"}, ] docker = [ {file = "docker-5.0.3-py2.py3-none-any.whl", hash = "sha256:7a79bb439e3df59d0a72621775d600bc8bc8b422d285824cb37103eab91d1ce0"}, @@ -2391,7 +2393,7 @@ secretstorage = [ {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"}, ] selenium = [ - {file = "selenium-4.0.0-py3-none-any.whl", hash = "sha256:c942b166a21ce9c9065ad249b54059e926d39f9000167b5ca0fa4950d2ef9a82"}, + {file = "selenium-4.1.0-py3-none-any.whl", hash = "sha256:27e7b64df961d609f3d57237caa0df123abbbe22d038f2ec9e332fb90ec1a939"}, ] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, diff --git a/pyproject.toml b/pyproject.toml index 25946c9..f91c23e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "PyInventory" -version = "0.11.0" +version = "0.12.0.rc1" description = "Web based management to catalog things including state and location etc. using Python/Django." authors = ["JensDiemer "] homepage = "https://github.com/jedie/PyInventory" @@ -43,7 +43,7 @@ django-processinfo = "*" # https://github.com/jedie/django-processinfo/ django-debug-toolbar = "*" # http://django-debug-toolbar.readthedocs.io/en/stable/changes.html django-import-export = "*" # https://github.com/django-import-export/django-import-export django-dbbackup = "*" # https://github.com/django-dbbackup/django-dbbackup -django-tools = ">=0.48.2" # https://github.com/jedie/django-tools/ +django-tools = ">=0.49.0" # https://github.com/jedie/django-tools/ django-reversion-compare = "*" # https://github.com/jedie/django-reversion-compare/ django-ckeditor = "*" # https://github.com/django-ckeditor/django-ckeditor bx_py_utils = "*" # https://github.com/boxine/bx_py_utils diff --git a/src/inventory/__init__.py b/src/inventory/__init__.py index b6da6bd..32e3bdd 100644 --- a/src/inventory/__init__.py +++ b/src/inventory/__init__.py @@ -4,7 +4,4 @@ :license: GNU GPL v3 or above, see LICENSE for more details. """ -__version__ = "0.11.0" - -# https://docs.djangoproject.com/en/2.0/ref/applications/#configuring-applications-ref -default_app_config = "inventory.apps.InventoryConfig" +__version__ = "0.12.0.rc1" diff --git a/src/inventory/admin/item.py b/src/inventory/admin/item.py index d5790c8..deceee9 100644 --- a/src/inventory/admin/item.py +++ b/src/inventory/admin/item.py @@ -124,7 +124,7 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin): (_('Internals'), { 'classes': ('collapse',), 'fields': ( - 'id', + ('id', 'version'), 'user', ) }), diff --git a/src/inventory/admin/memo.py b/src/inventory/admin/memo.py index 71636a8..2b2ae1e 100644 --- a/src/inventory/admin/memo.py +++ b/src/inventory/admin/memo.py @@ -47,7 +47,7 @@ class MemoModelAdmin(ImportExportMixin, BaseUserAdmin): (_('Internals'), { 'classes': ('collapse',), 'fields': ( - 'id', + ('id', 'version'), 'user', ) }), diff --git a/src/inventory/migrations/0010_version_protect_models.py b/src/inventory/migrations/0010_version_protect_models.py new file mode 100644 index 0000000..cc0ad7c --- /dev/null +++ b/src/inventory/migrations/0010_version_protect_models.py @@ -0,0 +1,29 @@ +# Generated by Django 3.1.13 on 2021-11-22 17:47 + +from django.db import migrations +import django_tools.model_version_protect.models + + +class Migration(migrations.Migration): + + dependencies = [ + ('inventory', '0009_add_memo'), + ] + + operations = [ + migrations.AddField( + model_name='itemmodel', + name='version', + field=django_tools.model_version_protect.models.VersionModelField(default=0, help_text='Internal version number of this entry. Used to protect the overwriting of an older entry.'), + ), + migrations.AddField( + model_name='locationmodel', + name='version', + field=django_tools.model_version_protect.models.VersionModelField(default=0, help_text='Internal version number of this entry. Used to protect the overwriting of an older entry.'), + ), + migrations.AddField( + model_name='memomodel', + name='version', + field=django_tools.model_version_protect.models.VersionModelField(default=0, help_text='Internal version number of this entry. Used to protect the overwriting of an older entry.'), + ), + ] diff --git a/src/inventory/models/item.py b/src/inventory/models/item.py index a5ba69f..14726da 100644 --- a/src/inventory/models/item.py +++ b/src/inventory/models/item.py @@ -7,6 +7,7 @@ from ckeditor_uploader.fields import RichTextUploadingField from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from django_tools.model_version_protect.models import VersionProtectBaseModel from django_tools.serve_media_app.models import user_directory_path from inventory.models.base import BaseItemAttachmentModel, BaseModel @@ -21,7 +22,7 @@ class ItemQuerySet(models.QuerySet): return self.order_by('kind', 'producer', 'name') -class ItemModel(BaseModel): +class ItemModel(BaseModel, VersionProtectBaseModel): """ A Item that can be described and store somewhere ;) """ diff --git a/src/inventory/models/location.py b/src/inventory/models/location.py index ba757c3..e840c09 100644 --- a/src/inventory/models/location.py +++ b/src/inventory/models/location.py @@ -1,11 +1,12 @@ from ckeditor_uploader.fields import RichTextUploadingField from django.db import models from django.utils.translation import gettext_lazy as _ +from django_tools.model_version_protect.models import VersionProtectBaseModel from inventory.models.base import BaseModel -class LocationModel(BaseModel): +class LocationModel(BaseModel, VersionProtectBaseModel): """ A Storage for items. """ diff --git a/src/inventory/models/memo.py b/src/inventory/models/memo.py index 405dbde..e79c4e2 100644 --- a/src/inventory/models/memo.py +++ b/src/inventory/models/memo.py @@ -6,6 +6,7 @@ from ckeditor_uploader.fields import RichTextUploadingField from django.db import models from django.urls import reverse from django.utils.translation import gettext_lazy as _ +from django_tools.model_version_protect.models import VersionProtectBaseModel from django_tools.serve_media_app.models import user_directory_path from inventory.models.base import BaseMemoAttachmentModel, BaseModel @@ -15,7 +16,7 @@ from inventory.models.links import BaseLink logger = logging.getLogger(__name__) -class MemoModel(BaseModel): +class MemoModel(BaseModel, VersionProtectBaseModel): """ A Memo to hold some information independ of items/location """ diff --git a/src/inventory_project/settings/base.py b/src/inventory_project/settings/base.py index 99aa32d..131dc46 100644 --- a/src/inventory_project/settings/base.py +++ b/src/inventory_project/settings/base.py @@ -80,6 +80,9 @@ INSTALLED_APPS = [ # https://github.com/jedie/django-tools/tree/master/django_tools/serve_media_app 'django_tools.serve_media_app.apps.UserMediaFilesConfig', + # https://github.com/jedie/django-tools/tree/master/django_tools/model_version_protect + 'django_tools.model_version_protect.apps.ModelVersionProtectConfig', + 'inventory.apps.InventoryConfig', ] diff --git a/src/inventory_project/tests/test_admin_item.py b/src/inventory_project/tests/test_admin_item.py index b104d8c..bda1ef6 100644 --- a/src/inventory_project/tests/test_admin_item.py +++ b/src/inventory_project/tests/test_admin_item.py @@ -4,6 +4,7 @@ from unittest import mock from bx_django_utils.test_utils.datetime import MockDatetimeGenerator from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin +from bx_py_utils.test_utils.snapshot import assert_html_snapshot from django.contrib.auth.models import User from django.template.defaulttags import CsrfTokenNode, NowNode from django.test import TestCase @@ -17,6 +18,34 @@ from inventory.permissions import get_or_create_normal_user_group from inventory_project.tests.temp_utils import assert_html_response_snapshot +ITEM_FORM_DEFAULTS = { + 'version': 0, # VersionProtectBaseModel field + 'kind': 'kind', + 'name': 'name', + + 'itemimagemodel_set-TOTAL_FORMS': '0', + 'itemimagemodel_set-INITIAL_FORMS': '0', + 'itemimagemodel_set-MIN_NUM_FORMS': '0', + 'itemimagemodel_set-MAX_NUM_FORMS': '1000', + 'itemimagemodel_set-__prefix__-position': '0', + + 'itemfilemodel_set-TOTAL_FORMS': '0', + 'itemfilemodel_set-INITIAL_FORMS': '0', + 'itemfilemodel_set-MIN_NUM_FORMS': '0', + 'itemfilemodel_set-MAX_NUM_FORMS': '1000', + 'itemfilemodel_set-__prefix__-position': '0', + + 'itemlinkmodel_set-TOTAL_FORMS': '0', + 'itemlinkmodel_set-INITIAL_FORMS': '0', + 'itemlinkmodel_set-MIN_NUM_FORMS': '0', + 'itemlinkmodel_set-MAX_NUM_FORMS': '1000', + 'itemlinkmodel_set-__prefix__-position': '0', + + '_save': 'Save', +} +ITEM_FORM_DEFAULTS = tuple(ITEM_FORM_DEFAULTS.items()) + + class AdminAnonymousTests(TestCase): def test_login(self): response = self.client.get('/admin/inventory/itemmodel/add/', HTTP_ACCEPT_LANGUAGE='en') @@ -30,7 +59,8 @@ class AdminTestCase(HtmlAssertionMixin, TestCase): @classmethod def setUpTestData(cls): cls.normaluser = baker.make( - User, username='NormalUser', + User, + id=1, username='NormalUser', is_staff=True, is_active=True, is_superuser=False ) assert cls.normaluser.user_permissions.count() == 0 @@ -38,59 +68,58 @@ class AdminTestCase(HtmlAssertionMixin, TestCase): cls.normaluser.groups.set([group]) def test_normal_user_create_minimal_item(self): - self.client.force_login(self.normaluser) - - with mock.patch.object(NowNode, 'render', return_value='MockedNowNode'), \ + offset = datetime.timedelta(minutes=1) + with mock.patch.object(timezone, 'now', MockDatetimeGenerator(offset=offset)),\ + mock.patch.object(NowNode, 'render', return_value='MockedNowNode'), \ mock.patch.object(CsrfTokenNode, 'render', return_value='MockedCsrfTokenNode'): + self.client.force_login(self.normaluser) + response = self.client.get('/admin/inventory/itemmodel/add/') - assert response.status_code == 200 - self.assert_html_parts(response, parts=( - f'Add Item | PyInventory v{__version__}', - )) - assert_html_response_snapshot(response=response, validate=False) + assert response.status_code == 200 + self.assert_html_parts(response, parts=( + f'Add Item | PyInventory v{__version__}', + )) + assert_html_response_snapshot(response=response, validate=False) - assert ItemModel.objects.count() == 0 + assert ItemModel.objects.count() == 0 - response = self.client.post( - path='/admin/inventory/itemmodel/add/', - data={ - 'kind': 'kind', - 'name': 'name', + post_data = dict(ITEM_FORM_DEFAULTS) + response = self.client.post( + path='/admin/inventory/itemmodel/add/', + data=post_data, + ) + assert response.status_code == 302, response.content.decode('utf-8') # Form error? + self.assertRedirects(response, expected_url='/admin/inventory/itemmodel/') - 'itemimagemodel_set-TOTAL_FORMS': '0', - 'itemimagemodel_set-INITIAL_FORMS': '0', - 'itemimagemodel_set-MIN_NUM_FORMS': '0', - 'itemimagemodel_set-MAX_NUM_FORMS': '1000', - 'itemimagemodel_set-__prefix__-position': '0', + data = list(ItemModel.objects.values_list('kind__name', 'name', 'version')) + assert data == [('kind', 'name', 2)] # FIXME: Save call done two times! - 'itemfilemodel_set-TOTAL_FORMS': '0', - 'itemfilemodel_set-INITIAL_FORMS': '0', - 'itemfilemodel_set-MIN_NUM_FORMS': '0', - 'itemfilemodel_set-MAX_NUM_FORMS': '1000', - 'itemfilemodel_set-__prefix__-position': '0', + item = ItemModel.objects.first() - 'itemlinkmodel_set-TOTAL_FORMS': '0', - 'itemlinkmodel_set-INITIAL_FORMS': '0', - 'itemlinkmodel_set-MIN_NUM_FORMS': '0', - 'itemlinkmodel_set-MAX_NUM_FORMS': '1000', - 'itemlinkmodel_set-__prefix__-position': '0', + self.assert_messages(response, expected_messages=[ + f'The Item “ - name”' + ' was added successfully.' + ]) - '_save': 'Save', - }, - ) - self.assertRedirects(response, expected_url='/admin/inventory/itemmodel/') + assert item.user_id == self.normaluser.pk - data = list(ItemModel.objects.values_list('kind__name', 'name')) - assert data == [('kind', 'name')] + # Test django-tools VersionProtectBaseModel integration: - item = ItemModel.objects.first() - - self.assert_messages(response, expected_messages=[ - f'The Item “ - name”' - ' was added successfully.' - ]) - - assert item.user_id == self.normaluser.pk + assert item.version == 2 # current Version in DB + post_data['version'] = 1 # Try to overwrite with older version + post_data['name'] = 'A new Name!' + response = self.client.post( + path=f'/admin/inventory/itemmodel/{item.pk}/change/', + data=post_data, + ) + self.assert_html_parts(response, parts=( + f'Change Item | PyInventory v{__version__}', + '
  • Version error: Overwrite version 2 with 1 is forbidden!
  • ', + '
    - "name"\n+ "A new Name!"
    ' + )) + html = response.content.decode('utf-8') + html = html.replace(str(item.pk), '') + assert_html_snapshot(got=html, validate=False) def test_new_item_with_image(self): """ @@ -100,34 +129,16 @@ class AdminTestCase(HtmlAssertionMixin, TestCase): img = ImageDummy(width=1, height=1, format='png').in_memory_image_file(filename='test.png') + post_data = dict(ITEM_FORM_DEFAULTS) + post_data.update({ + 'itemimagemodel_set-TOTAL_FORMS': '1', + 'itemimagemodel_set-0-position': '0', + 'itemimagemodel_set-0-image': img, + }) + response = self.client.post( path='/admin/inventory/itemmodel/add/', - data={ - 'kind': 'kind', - 'name': 'name', - - 'itemimagemodel_set-TOTAL_FORMS': '1', - 'itemimagemodel_set-INITIAL_FORMS': '0', - 'itemimagemodel_set-MIN_NUM_FORMS': '0', - 'itemimagemodel_set-MAX_NUM_FORMS': '1000', - 'itemimagemodel_set-0-position': '0', - 'itemimagemodel_set-__prefix__-position': '0', - 'itemimagemodel_set-0-image': img, - - 'itemfilemodel_set-TOTAL_FORMS': '0', - 'itemfilemodel_set-INITIAL_FORMS': '0', - 'itemfilemodel_set-MIN_NUM_FORMS': '0', - 'itemfilemodel_set-MAX_NUM_FORMS': '1000', - 'itemfilemodel_set-__prefix__-position': '0', - - 'itemlinkmodel_set-TOTAL_FORMS': '0', - 'itemlinkmodel_set-INITIAL_FORMS': '0', - 'itemlinkmodel_set-MIN_NUM_FORMS': '0', - 'itemlinkmodel_set-MAX_NUM_FORMS': '1000', - 'itemlinkmodel_set-__prefix__-position': '0', - - '_save': 'Save', - }, + data=post_data, ) self.assertRedirects(response, expected_url='/admin/inventory/itemmodel/') diff --git a/src/inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html b/src/inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html index 72d4c2f..b891466 100644 --- a/src/inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html +++ b/src/inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html @@ -2,7 +2,7 @@ - Select Item to change | PyInventory v0.11.0 + Select Item to change | PyInventory v0.12.0.rc1 @@ -43,7 +43,7 @@ diff --git a/src/inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html b/src/inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html index 86b87da..7090e2a 100644 --- a/src/inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html +++ b/src/inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html @@ -2,7 +2,7 @@ - Select Item to change | PyInventory v0.11.0 + Select Item to change | PyInventory v0.12.0.rc1 @@ -43,7 +43,7 @@ diff --git a/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html b/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html index 11bcb2b..4c83d9c 100644 --- a/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html +++ b/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html @@ -2,7 +2,7 @@ - Add Item | PyInventory v0.11.0 + Add Item | PyInventory v0.12.0.rc1 @@ -84,7 +84,7 @@ @@ -186,8 +186,8 @@

    Internals

    -
    -
    +
    +
    @@ -198,6 +198,16 @@ ID
    +
    + + + 0 +
    + Internal version number of this entry. Used to protect the overwriting of an older entry. +
    +
    diff --git a/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html b/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html new file mode 100644 index 0000000..419d5b6 --- /dev/null +++ b/src/inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html @@ -0,0 +1,762 @@ + + + + + Change Item | PyInventory v0.12.0.rc1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + + + + +
    + + +
    + +
    +

    + Change Item +

    +
    + +
    + MockedCsrfTokenNode +
    +

    + Please correct the errors below. +

    +
      +
    • + Version error: Overwrite version 2 with 1 is forbidden! +
    • +
    +
    +

    + Internals +

    +
    +
    + +
    + + +
    +
    + ID +
    +
    +
    +
      +
    • + Version changed: +
      - 2
      ++ 1
      +
    • +
    + + + 1 +
    + Internal version number of this entry. Used to protect the overwriting of an older entry. +
    +
    +
    +
    +
    + +
    + NormalUser +
    +
    + The user who is the owner of this entry and can manage it (will be set automatically) +
    +
    +
    +
    +
    +

    + Meta +

    +
    +
    + +
    + Jan. 1, 2000, 1:16 a.m. +
    +
    + (will be set automatically) +
    +
    +
    +
    +
    + +
    + Jan. 1, 2000, 1:17 a.m. +
    +
    + (will be set automatically) +
    +
    +
    +
    +
    +

    + Basic +

    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
      +
    • + changes between version 2 and 1: +
      - "name"
      ++ "A new Name!"
      +
    • +
    + + +
    + Name +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    + Unique number from the FCC +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +

    + Lent +

    +
    +
    + + +
    +
    +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +

    + Received +

    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    +

    + Handed over +

    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + +
    + +
    +
    +
    + + + + +
    + + +
    +
    +
    +
    +
    + + +
    +
    +
    + + + diff --git a/src/inventory_project/tests/test_admin_memo.py b/src/inventory_project/tests/test_admin_memo.py index e999a69..ed1b3e7 100644 --- a/src/inventory_project/tests/test_admin_memo.py +++ b/src/inventory_project/tests/test_admin_memo.py @@ -50,6 +50,7 @@ class AdminTestCase(HtmlAssertionMixin, TestCase): response = self.client.post( path='/admin/inventory/memomodel/add/', data={ + 'version': 0, # VersionProtectBaseModel field 'name': 'The Memo Name', 'memo': 'This is a test Memo', @@ -74,6 +75,7 @@ class AdminTestCase(HtmlAssertionMixin, TestCase): '_save': 'Save', }, ) + assert response.status_code == 302, response.content.decode('utf-8') # Form error? self.assertRedirects(response, expected_url='/admin/inventory/memomodel/') data = list(MemoModel.objects.values_list('name', 'memo')) @@ -99,6 +101,7 @@ class AdminTestCase(HtmlAssertionMixin, TestCase): response = self.client.post( path='/admin/inventory/memomodel/add/', data={ + 'version': 0, # VersionProtectBaseModel field 'name': 'The Memo Name', 'memo': 'This is a test Memo', @@ -125,6 +128,7 @@ class AdminTestCase(HtmlAssertionMixin, TestCase): '_save': 'Save', }, ) + assert response.status_code == 302, response.content.decode('utf-8') # Form error? memo = MemoModel.objects.first() or MemoModel() self.assert_messages(response, expected_messages=[ f'The Memo “The Memo Name”' diff --git a/src/inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html b/src/inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html index 3d8aaff..a5d0260 100644 --- a/src/inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html +++ b/src/inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html @@ -2,7 +2,7 @@ - Add Memo | PyInventory v0.11.0 + Add Memo | PyInventory v0.12.0.rc1 @@ -80,7 +80,7 @@ @@ -182,8 +182,8 @@

    Internals

    -
    -
    +
    +
    @@ -194,6 +194,16 @@ ID
    +
    + + + 0 +
    + Internal version number of this entry. Used to protect the overwriting of an older entry. +
    +