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
pull/78/head
JensDiemer 2021-11-22 18:38:57 +01:00
rodzic 42d492ccf4
commit 308b68e97f
18 zmienionych plików z 961 dodań i 130 usunięć

Wyświetl plik

@ -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

74
poetry.lock wygenerowano
Wyświetl plik

@ -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"},

Wyświetl plik

@ -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 <git@jensdiemer.de>"]
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

Wyświetl plik

@ -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"

Wyświetl plik

@ -124,7 +124,7 @@ class ItemModelAdmin(ImportExportMixin, BaseUserAdmin):
(_('Internals'), {
'classes': ('collapse',),
'fields': (
'id',
('id', 'version'),
'user',
)
}),

Wyświetl plik

@ -47,7 +47,7 @@ class MemoModelAdmin(ImportExportMixin, BaseUserAdmin):
(_('Internals'), {
'classes': ('collapse',),
'fields': (
'id',
('id', 'version'),
'user',
)
}),

Wyświetl plik

@ -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.'),
),
]

Wyświetl plik

@ -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 ;)
"""

Wyświetl plik

@ -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.
"""

Wyświetl plik

@ -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
"""

Wyświetl plik

@ -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',
]

Wyświetl plik

@ -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'<title>Add Item | PyInventory v{__version__}</title>',
))
assert_html_response_snapshot(response=response, validate=False)
assert response.status_code == 200
self.assert_html_parts(response, parts=(
f'<title>Add Item | PyInventory v{__version__}</title>',
))
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 “<a href="/admin/inventory/itemmodel/{item.pk}/change/"> - name</a>”'
' 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 “<a href="/admin/inventory/itemmodel/{item.pk}/change/"> - name</a>”'
' 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'<title>Change Item | PyInventory v{__version__}</title>',
'<li>Version error: Overwrite version 2 with 1 is forbidden!</li>',
'<pre>- &quot;name&quot;\n+ &quot;A new Name!&quot;</pre>'
))
html = response.content.decode('utf-8')
html = html.replace(str(item.pk), '<removed-UUID>')
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/')

Wyświetl plik

@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>
Select Item to change | PyInventory v0.11.0
Select Item to change | PyInventory v0.12.0.rc1
</title>
<link href="/static/admin/css/base.css" rel="stylesheet" type="text/css"/>
<link href="/static/admin/css/nav_sidebar.css" rel="stylesheet" type="text/css"/>
@ -43,7 +43,7 @@
<div id="branding">
<h1 id="site-name">
<a href="/admin/">
PyInventory v0.11.0
PyInventory v0.12.0.rc1
</a>
</h1>
</div>

Wyświetl plik

@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>
Select Item to change | PyInventory v0.11.0
Select Item to change | PyInventory v0.12.0.rc1
</title>
<link href="/static/admin/css/base.css" rel="stylesheet" type="text/css"/>
<link href="/static/admin/css/nav_sidebar.css" rel="stylesheet" type="text/css"/>
@ -43,7 +43,7 @@
<div id="branding">
<h1 id="site-name">
<a href="/admin/">
PyInventory v0.11.0
PyInventory v0.12.0.rc1
</a>
</h1>
</div>

Wyświetl plik

@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>
Add Item | PyInventory v0.11.0
Add Item | PyInventory v0.12.0.rc1
</title>
<link href="/static/admin/css/base.css" rel="stylesheet" type="text/css"/>
<link href="/static/admin/css/nav_sidebar.css" rel="stylesheet" type="text/css"/>
@ -84,7 +84,7 @@
<div id="branding">
<h1 id="site-name">
<a href="/admin/">
PyInventory v0.11.0
PyInventory v0.12.0.rc1
</a>
</h1>
</div>
@ -186,8 +186,8 @@
<h2>
Internals
</h2>
<div class="form-row field-id">
<div>
<div class="form-row field-id field-version">
<div class="fieldBox field-id">
<label>
ID:
</label>
@ -198,6 +198,16 @@
ID
</div>
</div>
<div class="fieldBox field-version">
<label class="required inline" for="id_version">
Version:
</label>
<input id="id_version" name="version" required="" type="hidden" value="0"/>
0
<div class="help">
Internal version number of this entry. Used to protect the overwriting of an older entry.
</div>
</div>
</div>
<div class="form-row field-user">
<div>

Wyświetl plik

@ -0,0 +1,762 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>
Change Item | PyInventory v0.12.0.rc1
</title>
<link href="/static/admin/css/base.css" rel="stylesheet" type="text/css"/>
<link href="/static/admin/css/nav_sidebar.css" rel="stylesheet" type="text/css"/>
<script defer="" src="/static/admin/js/nav_sidebar.js">
</script>
<link href="/static/admin/css/forms.css" rel="stylesheet" type="text/css"/>
<meta content="notranslate" name="google"/>
<meta content="noindex,nofollow" name="robots">
<link href="/static/inventory.css" rel="stylesheet" type="text/css"/>
<script src="/admin/jsi18n/">
</script>
<link href="/static/adminsortable2/css/sortable.css" media="all" rel="stylesheet" type="text/css"/>
<link href="/static/admin/css/vendor/select2/select2.min.css" media="screen" rel="stylesheet" type="text/css"/>
<link href="/static/admin/css/autocomplete.css" media="screen" rel="stylesheet" type="text/css"/>
<script src="/static/admin/js/vendor/jquery/jquery.min.js">
</script>
<script src="/static/tagulous/tagulous.js">
</script>
<script data-ckeditor-basepath="/static/ckeditor/ckeditor/" id="ckeditor-init-script" src="/static/ckeditor/ckeditor-init.js">
</script>
<script src="/static/admin/js/calendar.js">
</script>
<script src="/static/admin/js/collapse.min.js">
</script>
<script src="/static/admin/js/vendor/select2/select2.full.min.js">
</script>
<script src="/static/tagulous/adaptor/select2-4.js">
</script>
<script src="/static/ckeditor/ckeditor/ckeditor.js">
</script>
<script src="/static/admin/js/admin/DateTimeShortcuts.js">
</script>
<script src="/static/admin/js/vendor/select2/i18n/en.js">
</script>
<script src="/static/admin/js/jquery.init.js">
</script>
<script src="/static/admin/js/core.js">
</script>
<script src="/static/admin/js/autocomplete.js">
</script>
<script src="/static/admin/js/inlines.min.js">
</script>
<script src="/static/adminsortable2/js/plugins/admincompat.js">
</script>
<script src="/static/admin/js/admin/RelatedObjectLookups.js">
</script>
<script src="/static/adminsortable2/js/libs/jquery.ui.core-1.11.4.js">
</script>
<script src="/static/admin/js/actions.min.js">
</script>
<script src="/static/adminsortable2/js/libs/jquery.ui.widget-1.11.4.js">
</script>
<script src="/static/admin/js/urlify.js">
</script>
<script src="/static/adminsortable2/js/libs/jquery.ui.mouse-1.11.4.js">
</script>
<script src="/static/admin/js/prepopulate.min.js">
</script>
<script src="/static/adminsortable2/js/libs/jquery.ui.touch-punch-0.2.3.js">
</script>
<script src="/static/admin/js/vendor/xregexp/xregexp.min.js">
</script>
<script src="/static/adminsortable2/js/libs/jquery.ui.sortable-1.11.4.js">
</script>
<script src="/static/adminsortable2/js/inline-sortable.js">
</script>
<script src="/static/adminsortable2/js/inline-tabular.js">
</script>
<meta content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0" name="viewport"/>
<link href="/static/admin/css/responsive.css" rel="stylesheet" type="text/css"/>
<meta content="NONE,NOARCHIVE" name="robots"/>
</meta>
</head>
<body class="app-inventory model-itemmodel change-form" data-admin-utc-offset="MockedNowNode">
<!-- Container -->
<div id="container">
<!-- Header -->
<div id="header">
<div id="branding">
<h1 id="site-name">
<a href="/admin/">
PyInventory v0.12.0.rc1
</a>
</h1>
</div>
<div id="user-tools">
Welcome,
<strong>
NormalUser
</strong>
.
<a href="/">
View site
</a>
/
<a href="/admin/password_change/">
Change password
</a>
/
<a href="/admin/logout/">
Log out
</a>
</div>
</div>
<!-- END Header -->
<div class="breadcrumbs">
<a href="/admin/">
Home
</a>
<a href="/admin/inventory/">
Inventory
</a>
<a href="/admin/inventory/itemmodel/">
Items
</a>
- A new Name!
</div>
<div class="main shifted" id="main">
<button aria-label="Toggle navigation" class="sticky toggle-nav-sidebar" id="toggle-nav-sidebar">
</button>
<nav class="sticky" id="nav-sidebar">
<div class="app-inventory module current-app">
<table>
<caption>
<a class="section" href="/admin/inventory/" title="Models in the Inventory application">
Inventory
</a>
</caption>
<tr class="model-itemmodel current-model">
<th scope="row">
<a aria-current="page" href="/admin/inventory/itemmodel/">
Items
</a>
</th>
<td>
<a class="addlink" href="/admin/inventory/itemmodel/add/">
Add
</a>
</td>
</tr>
<tr class="model-locationmodel">
<th scope="row">
<a href="/admin/inventory/locationmodel/">
Locations
</a>
</th>
<td>
<a class="addlink" href="/admin/inventory/locationmodel/add/">
Add
</a>
</td>
</tr>
<tr class="model-memomodel">
<th scope="row">
<a href="/admin/inventory/memomodel/">
Memos
</a>
</th>
<td>
<a class="addlink" href="/admin/inventory/memomodel/add/">
Add
</a>
</td>
</tr>
</table>
</div>
</nav>
<div class="content">
<!-- Content -->
<div class="colM" id="content">
<h1>
Change Item
</h1>
<div id="content-main">
<ul class="object-tools">
<li>
<a class="historylink" href="/admin/inventory/itemmodel/<removed-UUID>/history/">
History
</a>
</li>
</ul>
<form enctype="multipart/form-data" id="itemmodel_form" method="post" novalidate="">
MockedCsrfTokenNode
<div>
<p class="errornote">
Please correct the errors below.
</p>
<ul class="errorlist nonfield">
<li>
Version error: Overwrite version 2 with 1 is forbidden!
</li>
</ul>
<fieldset class="module aligned collapse">
<h2>
Internals
</h2>
<div class="form-row field-id field-version">
<div class="fieldBox field-id">
<label>
ID:
</label>
<div class="readonly">
<removed-uuid>
</removed-uuid>
</div>
<div class="help">
ID
</div>
</div>
<div class="fieldBox field-version errors">
<ul class="errorlist">
<li>
Version changed:
<pre>- 2
+ 1</pre>
</li>
</ul>
<label class="required inline" for="id_version">
Version:
</label>
<input id="id_version" name="version" required="" type="hidden" value="1"/>
1
<div class="help">
Internal version number of this entry. Used to protect the overwriting of an older entry.
</div>
</div>
</div>
<div class="form-row field-user">
<div>
<label>
User:
</label>
<div class="readonly">
NormalUser
</div>
<div class="help">
The user who is the owner of this entry and can manage it (will be set automatically)
</div>
</div>
</div>
</fieldset>
<fieldset class="module aligned collapse">
<h2>
Meta
</h2>
<div class="form-row field-create_dt">
<div>
<label>
Create date:
</label>
<div class="readonly">
Jan. 1, 2000, 1:16 a.m.
</div>
<div class="help">
(will be set automatically)
</div>
</div>
</div>
<div class="form-row field-update_dt">
<div>
<label>
Last update:
</label>
<div class="readonly">
Jan. 1, 2000, 1:17 a.m.
</div>
<div class="help">
(will be set automatically)
</div>
</div>
</div>
</fieldset>
<fieldset class="module aligned">
<h2>
Basic
</h2>
<div class="form-row field-kind">
<div>
<label class="required" for="id_kind">
Kind:
</label>
<div class="related-widget-wrapper">
<input autocomplete="off" data-tag-list='["kind"]' data-tag-options='{"case_sensitive": false, "force_lowercase": false, "max_count": 3, "space_delimiter": false, "required": true}' data-tagulous="true" data-theme="admin-autocomplete" id="id_kind" name="kind" required="" type="text" value="kind"/>
</div>
<div class="help">
</div>
</div>
</div>
<div class="form-row field-producer field-name">
<div class="fieldBox field-producer">
<label for="id_producer">
Producer:
</label>
<div class="related-widget-wrapper">
<input autocomplete="off" data-tag-list="[]" data-tag-options='{"case_sensitive": false, "force_lowercase": false, "max_count": 1, "space_delimiter": false, "required": false}' data-tagulous="true" data-theme="admin-autocomplete" id="id_producer" name="producer" type="text"/>
</div>
<div class="help">
</div>
</div>
<div class="fieldBox field-name errors">
<ul class="errorlist">
<li>
changes between version 2 and 1:
<pre>- "name"
+ "A new Name!"</pre>
</li>
</ul>
<label class="required inline" for="id_name">
Name:
</label>
<input class="vTextField" id="id_name" maxlength="255" name="name" required="" type="text" value="A new Name!"/>
<div class="help">
Name
</div>
</div>
</div>
<div class="form-row field-description">
<div>
<label for="id_description">
Description:
</label>
<div class="django-ckeditor-widget" data-field-id="id_description" style="display: inline-block;">
<textarea cols="40" data-config='{"skin": "moono-lisa", "toolbar_Basic": [["Source", "-", "Bold", "Italic"]], "toolbar_Full": [["Styles", "Format", "Bold", "Italic", "Underline", "Strike", "SpellChecker", "Undo", "Redo"], ["Link", "Unlink", "Anchor"], ["Image", "Flash", "Table", "HorizontalRule"], ["TextColor", "BGColor"], ["Smiley", "SpecialChar"], ["Source"]], "toolbar": "PyInventoryToolbarConfig", "height": "25em", "width": "100%", "filebrowserWindowWidth": 940, "filebrowserWindowHeight": 725, "removeButtons": "Language,Cut,Copy,Paste,Undo,Redo,Anchor", "removePlugins": ["a11yhelp", "adobeair", "ajax", "autoembed", "autolink", "bbcode", "bidi", "clipboard", "codesnippet", "codesnippetgeshi", "contextmenu", "copyformatting", "devtools", "dialog", "dialogadvtab", "div", "divarea", "docprops", "embed", "embedbase", "embedsemantic", "enterkey", "exportpdf", "find", "flash", "forms", "htmlwriter", "iframe", "iframedialog", "language", "magicline", "mathjax", "newpage", "notification", "notificationaggregator", "pagebreak", "pastefromgdocs", "pastefromword", "pastetext", "pastetools", "placeholder", "preview", "print", "save", "scayt", "selectall", "sharedspace", "smiley", "sourcedialog", "specialchar", "stylescombo", "stylesheetparser", "tab", "templates", "uicolor", "widget", "wsc", "xml"], "toolbar_PyInventoryToolbarConfig": [{"name": "basicstyles", "items": ["Bold", "Italic", "Underline", "Strike", "-", "RemoveFormat"]}, {"name": "paragraph", "items": ["NumberedList", "BulletedList", "-", "Outdent", "Indent", "-", "Blockquote", "-", "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"]}, {"name": "links", "items": ["Link", "Unlink", "Anchor"]}, {"name": "insert", "items": ["Image", "Table", "HorizontalRule"]}, "/", {"name": "styles", "items": ["Styles", "Format", "Font", "FontSize"]}, {"name": "colors", "items": ["TextColor", "BGColor"]}, {"name": "tools", "items": ["Maximize", "ShowBlocks", "Source"]}, {"name": "about", "items": ["About"]}], "filebrowserUploadUrl": "/ckeditor/upload/", "filebrowserBrowseUrl": "/ckeditor/browse/", "language": "en"}' data-external-plugin-resources="[]" data-id="id_description" data-processed="0" data-type="ckeditortype" id="id_description" name="description" rows="10"></textarea>
</div>
<div class="help">
</div>
</div>
</div>
<div class="form-row field-tags">
<div>
<label for="id_tags">
Tags:
</label>
<div class="related-widget-wrapper">
<input autocomplete="off" data-tag-list="[]" data-tag-options='{"case_sensitive": false, "force_lowercase": false, "max_count": 10, "space_delimiter": false, "required": false}' data-tagulous="true" data-theme="admin-autocomplete" id="id_tags" name="tags" type="text"/>
</div>
<div class="help">
</div>
</div>
</div>
<div class="form-row field-fcc_id">
<div>
<label for="id_fcc_id">
FCC ID:
</label>
<input class="vTextField" id="id_fcc_id" maxlength="20" name="fcc_id" type="text"/>
<div class="help">
Unique number from the FCC
</div>
</div>
</div>
<div class="form-row field-parent">
<div>
<label for="id_parent">
Parent:
</label>
<div class="related-widget-wrapper">
<select id="id_parent" name="parent">
<option selected="" value="">
---------
</option>
<option value="<removed-UUID>">
- name
</option>
</select>
<a class="related-widget-wrapper-link change-related" data-href-template="/admin/inventory/itemmodel/__fk__/change/?_to_field=id&_popup=1" id="change_id_parent" title="Change selected Item">
<img alt="Change" src="/static/admin/img/icon-changelink.svg"/>
</a>
<a class="related-widget-wrapper-link add-related" href="/admin/inventory/itemmodel/add/?_to_field=id&_popup=1" id="add_id_parent" title="Add another Item">
<img alt="Add" src="/static/admin/img/icon-addlink.svg"/>
</a>
<a class="related-widget-wrapper-link delete-related" data-href-template="/admin/inventory/itemmodel/__fk__/delete/?_to_field=id&_popup=1" id="delete_id_parent" title="Delete selected Item">
<img alt="Delete" src="/static/admin/img/icon-deletelink.svg"/>
</a>
</div>
<div class="help">
</div>
</div>
</div>
<div class="form-row field-location">
<div>
<label for="id_location">
Location:
</label>
<div class="related-widget-wrapper">
<select id="id_location" name="location">
<option selected="" value="">
---------
</option>
</select>
<a class="related-widget-wrapper-link change-related" data-href-template="/admin/inventory/locationmodel/__fk__/change/?_to_field=id&_popup=1" id="change_id_location" title="Change selected Location">
<img alt="Change" src="/static/admin/img/icon-changelink.svg"/>
</a>
<a class="related-widget-wrapper-link add-related" href="/admin/inventory/locationmodel/add/?_to_field=id&_popup=1" id="add_id_location" title="Add another Location">
<img alt="Add" src="/static/admin/img/icon-addlink.svg"/>
</a>
<a class="related-widget-wrapper-link delete-related" data-href-template="/admin/inventory/locationmodel/__fk__/delete/?_to_field=id&_popup=1" id="delete_id_location" title="Delete selected Location">
<img alt="Delete" src="/static/admin/img/icon-deletelink.svg"/>
</a>
</div>
<div class="help">
</div>
</div>
</div>
</fieldset>
<fieldset class="module aligned collapse">
<h2>
Lent
</h2>
<div class="form-row field-lent_to">
<div>
<label for="id_lent_to">
Lent to:
</label>
<input class="vTextField" id="id_lent_to" maxlength="64" name="lent_to" type="text"/>
<div class="help">
</div>
</div>
</div>
<div class="form-row field-lent_from_date field-lent_until_date">
<div class="fieldBox field-lent_from_date">
<label for="id_lent_from_date">
Lent from date:
</label>
<input class="vDateField" id="id_lent_from_date" name="lent_from_date" size="10" type="text"/>
<div class="help">
</div>
</div>
<div class="fieldBox field-lent_until_date">
<label class="inline" for="id_lent_until_date">
Lent until date:
</label>
<input class="vDateField" id="id_lent_until_date" name="lent_until_date" size="10" type="text"/>
<div class="help">
</div>
</div>
</div>
</fieldset>
<fieldset class="module aligned collapse">
<h2>
Received
</h2>
<div class="form-row field-received_from field-received_date field-received_price">
<div class="fieldBox field-received_from">
<label for="id_received_from">
Received from:
</label>
<input class="vTextField" id="id_received_from" maxlength="64" name="received_from" type="text"/>
<div class="help">
</div>
</div>
<div class="fieldBox field-received_date">
<label class="inline" for="id_received_date">
Received date:
</label>
<input class="vDateField" id="id_received_date" name="received_date" size="10" type="text"/>
<div class="help">
</div>
</div>
<div class="fieldBox field-received_price">
<label class="inline" for="id_received_price">
Received price:
</label>
<input id="id_received_price" name="received_price" step="0.01" type="number"/>
<div class="help">
</div>
</div>
</div>
</fieldset>
<fieldset class="module aligned collapse">
<h2>
Handed over
</h2>
<div class="form-row field-handed_over_to field-handed_over_date field-handed_over_price">
<div class="fieldBox field-handed_over_to">
<label for="id_handed_over_to">
Handed over to:
</label>
<input class="vTextField" id="id_handed_over_to" maxlength="64" name="handed_over_to" type="text"/>
<div class="help">
</div>
</div>
<div class="fieldBox field-handed_over_date">
<label class="inline" for="id_handed_over_date">
Handed over date:
</label>
<input class="vDateField" id="id_handed_over_date" name="handed_over_date" size="10" type="text"/>
<div class="help">
</div>
</div>
<div class="fieldBox field-handed_over_price">
<label class="inline" for="id_handed_over_price">
Handed over price:
</label>
<input id="id_handed_over_price" name="handed_over_price" step="0.01" type="number"/>
<div class="help">
</div>
</div>
</div>
</fieldset>
<div class="inline-group sortable" id="itemimagemodel_set-group">
<div class="tabular inline-related">
<input id="id_itemimagemodel_set-TOTAL_FORMS" name="itemimagemodel_set-TOTAL_FORMS" type="hidden" value="0"/>
<input id="id_itemimagemodel_set-INITIAL_FORMS" name="itemimagemodel_set-INITIAL_FORMS" type="hidden" value="0"/>
<input id="id_itemimagemodel_set-MIN_NUM_FORMS" name="itemimagemodel_set-MIN_NUM_FORMS" type="hidden" value="0"/>
<input id="id_itemimagemodel_set-MAX_NUM_FORMS" name="itemimagemodel_set-MAX_NUM_FORMS" type="hidden" value="1000"/>
<fieldset class="module">
<h2>
Images
</h2>
<table>
<thead>
<tr>
<th>
Sort
</th>
<th colspan="2">
Preview
</th>
<th class="required">
Image
<img alt="( )" class="help help-tooltip" height="10" src="/static/admin/img/icon-unknown.svg" title=" " width="10">
</img>
</th>
<th>
Name
<img alt="(BaseItemAttachmentModel.name.help_text)" class="help help-tooltip" height="10" src="/static/admin/img/icon-unknown.svg" title="BaseItemAttachmentModel.name.help_text" width="10">
</img>
</th>
<th>
Tags
<img alt="( )" class="help help-tooltip" height="10" src="/static/admin/img/icon-unknown.svg" title=" " width="10">
</img>
</th>
<th>
Delete?
</th>
</tr>
</thead>
<tbody>
<tr class="form-row row1 empty-form" id="itemimagemodel_set-empty">
<td class="drag">
</td>
<td class="original">
<input id="id_itemimagemodel_set-__prefix__-id" name="itemimagemodel_set-__prefix__-id" type="hidden"/>
<input id="id_itemimagemodel_set-__prefix__-item" name="itemimagemodel_set-__prefix__-item" type="hidden" value="<removed-UUID>"/>
<input id="id_itemimagemodel_set-__prefix__-position" name="itemimagemodel_set-__prefix__-position" type="hidden" value="0"/>
</td>
<td class="field-preview">
<p>
-
</p>
</td>
<td class="field-image">
<input accept="image/*" id="id_itemimagemodel_set-__prefix__-image" name="itemimagemodel_set-__prefix__-image" type="file"/>
</td>
<td class="field-name">
<input class="vTextField" id="id_itemimagemodel_set-__prefix__-name" maxlength="255" name="itemimagemodel_set-__prefix__-name" type="text"/>
</td>
<td class="field-tags">
<div class="related-widget-wrapper">
<input autocomplete="off" data-tag-list="[]" data-tag-options='{"case_sensitive": false, "force_lowercase": false, "max_count": 10, "space_delimiter": false, "required": false}' data-tagulous="true" data-theme="admin-autocomplete" id="id_itemimagemodel_set-__prefix__-tags" name="itemimagemodel_set-__prefix__-tags" type="text"/>
</div>
</td>
<td class="delete">
</td>
</tr>
</tbody>
</table>
</fieldset>
</div>
</div>
<script class="inline-tabular-config" type="application/json">
{
"prefix": "itemimagemodel_set",
"addText": "Add another Image",
"deleteText": "Remove"
}
</script>
<div class="default_order_field" default_order_direction="" default_order_field="position">
</div>
<div class="inline-group sortable" id="itemfilemodel_set-group">
<div class="tabular inline-related">
<input id="id_itemfilemodel_set-TOTAL_FORMS" name="itemfilemodel_set-TOTAL_FORMS" type="hidden" value="0"/>
<input id="id_itemfilemodel_set-INITIAL_FORMS" name="itemfilemodel_set-INITIAL_FORMS" type="hidden" value="0"/>
<input id="id_itemfilemodel_set-MIN_NUM_FORMS" name="itemfilemodel_set-MIN_NUM_FORMS" type="hidden" value="0"/>
<input id="id_itemfilemodel_set-MAX_NUM_FORMS" name="itemfilemodel_set-MAX_NUM_FORMS" type="hidden" value="1000"/>
<fieldset class="module">
<h2>
Files
</h2>
<table>
<thead>
<tr>
<th>
Sort
</th>
<th class="required" colspan="2">
File
<img alt="( )" class="help help-tooltip" height="10" src="/static/admin/img/icon-unknown.svg" title=" " width="10">
</img>
</th>
<th>
Name
<img alt="(BaseItemAttachmentModel.name.help_text)" class="help help-tooltip" height="10" src="/static/admin/img/icon-unknown.svg" title="BaseItemAttachmentModel.name.help_text" width="10">
</img>
</th>
<th>
Tags
<img alt="( )" class="help help-tooltip" height="10" src="/static/admin/img/icon-unknown.svg" title=" " width="10">
</img>
</th>
<th>
Delete?
</th>
</tr>
</thead>
<tbody>
<tr class="form-row row1 empty-form" id="itemfilemodel_set-empty">
<td class="drag">
</td>
<td class="original">
<input id="id_itemfilemodel_set-__prefix__-id" name="itemfilemodel_set-__prefix__-id" type="hidden"/>
<input id="id_itemfilemodel_set-__prefix__-item" name="itemfilemodel_set-__prefix__-item" type="hidden" value="<removed-UUID>"/>
<input id="id_itemfilemodel_set-__prefix__-position" name="itemfilemodel_set-__prefix__-position" type="hidden" value="0"/>
</td>
<td class="field-file">
<input id="id_itemfilemodel_set-__prefix__-file" name="itemfilemodel_set-__prefix__-file" type="file"/>
</td>
<td class="field-name">
<input class="vTextField" id="id_itemfilemodel_set-__prefix__-name" maxlength="255" name="itemfilemodel_set-__prefix__-name" type="text"/>
</td>
<td class="field-tags">
<div class="related-widget-wrapper">
<input autocomplete="off" data-tag-list="[]" data-tag-options='{"case_sensitive": false, "force_lowercase": false, "max_count": 10, "space_delimiter": false, "required": false}' data-tagulous="true" data-theme="admin-autocomplete" id="id_itemfilemodel_set-__prefix__-tags" name="itemfilemodel_set-__prefix__-tags" type="text"/>
</div>
</td>
<td class="delete">
</td>
</tr>
</tbody>
</table>
</fieldset>
</div>
</div>
<script class="inline-tabular-config" type="application/json">
{
"prefix": "itemfilemodel_set",
"addText": "Add another File",
"deleteText": "Remove"
}
</script>
<div class="default_order_field" default_order_direction="" default_order_field="position">
</div>
<div class="inline-group sortable" id="itemlinkmodel_set-group">
<div class="tabular inline-related last-related">
<input id="id_itemlinkmodel_set-TOTAL_FORMS" name="itemlinkmodel_set-TOTAL_FORMS" type="hidden" value="0"/>
<input id="id_itemlinkmodel_set-INITIAL_FORMS" name="itemlinkmodel_set-INITIAL_FORMS" type="hidden" value="0"/>
<input id="id_itemlinkmodel_set-MIN_NUM_FORMS" name="itemlinkmodel_set-MIN_NUM_FORMS" type="hidden" value="0"/>
<input id="id_itemlinkmodel_set-MAX_NUM_FORMS" name="itemlinkmodel_set-MAX_NUM_FORMS" type="hidden" value="1000"/>
<fieldset class="module">
<h2>
Links
</h2>
<table>
<thead>
<tr>
<th>
Sort
</th>
<th colspan="2">
Tags
<img alt="( )" class="help help-tooltip" height="10" src="/static/admin/img/icon-unknown.svg" title=" " width="10"/>
</th>
<th>
Name
<img alt="( )" class="help help-tooltip" height="10" src="/static/admin/img/icon-unknown.svg" title=" " width="10"/>
</th>
<th class="required">
URL
<img alt="( )" class="help help-tooltip" height="10" src="/static/admin/img/icon-unknown.svg" title=" " width="10"/>
</th>
<th>
Delete?
</th>
</tr>
</thead>
<tbody>
<tr class="form-row row1 empty-form" id="itemlinkmodel_set-empty">
<td class="drag">
</td>
<td class="original">
<input id="id_itemlinkmodel_set-__prefix__-id" name="itemlinkmodel_set-__prefix__-id" type="hidden"/>
<input id="id_itemlinkmodel_set-__prefix__-item" name="itemlinkmodel_set-__prefix__-item" type="hidden" value="<removed-UUID>"/>
<input id="id_itemlinkmodel_set-__prefix__-position" name="itemlinkmodel_set-__prefix__-position" type="hidden" value="0"/>
</td>
<td class="field-tags">
<div class="related-widget-wrapper">
<input autocomplete="off" data-tag-list="[]" data-tag-options='{"case_sensitive": false, "force_lowercase": false, "max_count": 10, "space_delimiter": false, "required": false}' data-tagulous="true" data-theme="admin-autocomplete" id="id_itemlinkmodel_set-__prefix__-tags" name="itemlinkmodel_set-__prefix__-tags" type="text"/>
</div>
</td>
<td class="field-name">
<input class="vTextField" id="id_itemlinkmodel_set-__prefix__-name" maxlength="255" name="itemlinkmodel_set-__prefix__-name" type="text"/>
</td>
<td class="field-url">
<input class="vURLField" id="id_itemlinkmodel_set-__prefix__-url" maxlength="200" name="itemlinkmodel_set-__prefix__-url" type="url"/>
</td>
<td class="delete">
</td>
</tr>
</tbody>
</table>
</fieldset>
</div>
</div>
<script class="inline-tabular-config" type="application/json">
{
"prefix": "itemlinkmodel_set",
"addText": "Add another Link",
"deleteText": "Remove"
}
</script>
<div class="default_order_field" default_order_direction="" default_order_field="position">
</div>
<div class="submit-row">
<input class="default" name="_save" type="submit" value="Save"/>
<p class="deletelink-box">
<a class="deletelink" href="/admin/inventory/itemmodel/<removed-UUID>/delete/">
Delete
</a>
</p>
<input name="_addanother" type="submit" value="Save and add another"/>
<input name="_continue" type="submit" value="Save and continue editing"/>
</div>
<script async="" id="django-admin-form-add-constants" src="/static/admin/js/change_form.js">
</script>
<script data-prepopulated-fields="[]" id="django-admin-prepopulated-fields-constants" src="/static/admin/js/prepopulate_init.js">
</script>
</div>
</form>
</div>
<br class="clear"/>
</div>
<!-- END Content -->
<div id="footer">
<a href="https://github.com/jedie/PyInventory">
https://github.com/jedie/PyInventory
</a>
</div>
</div>
</div>
</div>
<!-- END Container -->
</body>
</html>

Wyświetl plik

@ -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 “<a href="/admin/inventory/memomodel/{memo.pk}/change/">The Memo Name</a>”'

Wyświetl plik

@ -2,7 +2,7 @@
<html lang="en">
<head>
<title>
Add Memo | PyInventory v0.11.0
Add Memo | PyInventory v0.12.0.rc1
</title>
<link href="/static/admin/css/base.css" rel="stylesheet" type="text/css"/>
<link href="/static/admin/css/nav_sidebar.css" rel="stylesheet" type="text/css"/>
@ -80,7 +80,7 @@
<div id="branding">
<h1 id="site-name">
<a href="/admin/">
PyInventory v0.11.0
PyInventory v0.12.0.rc1
</a>
</h1>
</div>
@ -182,8 +182,8 @@
<h2>
Internals
</h2>
<div class="form-row field-id">
<div>
<div class="form-row field-id field-version">
<div class="fieldBox field-id">
<label>
ID:
</label>
@ -194,6 +194,16 @@
ID
</div>
</div>
<div class="fieldBox field-version">
<label class="required inline" for="id_version">
Version:
</label>
<input id="id_version" name="version" required="" type="hidden" value="0"/>
0
<div class="help">
Internal version number of this entry. Used to protect the overwriting of an older entry.
</div>
</div>
</div>
<div class="form-row field-user">
<div>