PyInventory 0.19.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- PyInventory-0.19.0.dist-info/AUTHORS +14 -0
- PyInventory-0.19.0.dist-info/LICENSE +674 -0
- PyInventory-0.19.0.dist-info/METADATA +347 -0
- PyInventory-0.19.0.dist-info/RECORD +101 -0
- PyInventory-0.19.0.dist-info/WHEEL +5 -0
- PyInventory-0.19.0.dist-info/entry_points.txt +2 -0
- PyInventory-0.19.0.dist-info/top_level.txt +2 -0
- inventory/__init__.py +7 -0
- inventory/admin/__init__.py +3 -0
- inventory/admin/base.py +104 -0
- inventory/admin/item.py +169 -0
- inventory/admin/location.py +78 -0
- inventory/admin/memo.py +76 -0
- inventory/admin/tagulous_fix.py +45 -0
- inventory/apps.py +18 -0
- inventory/ckeditor_upload.py +15 -0
- inventory/context_processors.py +5 -0
- inventory/forms.py +36 -0
- inventory/locale/ca/LC_MESSAGES/django.mo +0 -0
- inventory/locale/ca/LC_MESSAGES/django.po +297 -0
- inventory/locale/de/LC_MESSAGES/django.mo +0 -0
- inventory/locale/de/LC_MESSAGES/django.po +294 -0
- inventory/locale/en/LC_MESSAGES/django.mo +0 -0
- inventory/locale/en/LC_MESSAGES/django.po +294 -0
- inventory/locale/es/LC_MESSAGES/django.mo +0 -0
- inventory/locale/es/LC_MESSAGES/django.po +297 -0
- inventory/management/__init__.py +0 -0
- inventory/management/commands/__init__.py +0 -0
- inventory/management/commands/seed_data.py +135 -0
- inventory/management/commands/tree.py +62 -0
- inventory/middlewares.py +21 -0
- inventory/migrations/0001_initial.py +596 -0
- inventory/migrations/0002_auto_20201017_2211.py +87 -0
- inventory/migrations/0003_auto_20201024_1830.py +23 -0
- inventory/migrations/0004_item_user_images.py +129 -0
- inventory/migrations/0005_serve_uploads_by_django_tools.py +77 -0
- inventory/migrations/0006_refactor_image_model.py +46 -0
- inventory/migrations/0007_add_file_attachment.py +128 -0
- inventory/migrations/0008_last_check_datetime.py +23 -0
- inventory/migrations/0009_add_memo.py +517 -0
- inventory/migrations/0010_version_protect_models.py +37 -0
- inventory/migrations/0011_parent_tree1.py +97 -0
- inventory/migrations/0012_parent_tree2.py +20 -0
- inventory/migrations/0013_alter_itemmodel_location.py +26 -0
- inventory/migrations/__init__.py +0 -0
- inventory/models/__init__.py +3 -0
- inventory/models/base.py +239 -0
- inventory/models/item.py +228 -0
- inventory/models/links.py +104 -0
- inventory/models/location.py +24 -0
- inventory/models/memo.py +109 -0
- inventory/parent_tree.py +71 -0
- inventory/permissions.py +60 -0
- inventory/request_dict.py +16 -0
- inventory/signals.py +15 -0
- inventory/string_utils.py +15 -0
- inventory/templates/admin/item/related_items.html +18 -0
- inventory/templates/admin/location/items.html +18 -0
- inventory/tests/__init__.py +0 -0
- inventory/tests/fixtures/__init__.py +0 -0
- inventory/tests/fixtures/users.py +11 -0
- inventory/tests/test_admin_location.py +34 -0
- inventory/tests/test_admin_location_empty_change_list_1.snapshot.html +84 -0
- inventory/tests/test_item_images.py +76 -0
- inventory/tests/test_link_model.py +72 -0
- inventory/tests/test_management_command_seed_data.py +49 -0
- inventory/tests/test_management_command_tree.py +27 -0
- inventory/tests/test_parent_tree.py +40 -0
- inventory/tests/test_parent_tree_model.py +139 -0
- inventory_project/__init__.py +12 -0
- inventory_project/__main__.py +17 -0
- inventory_project/manage.py +41 -0
- inventory_project/middlewares.py +23 -0
- inventory_project/publish.py +21 -0
- inventory_project/settings/__init__.py +0 -0
- inventory_project/settings/local.py +74 -0
- inventory_project/settings/prod.py +393 -0
- inventory_project/settings/tests.py +45 -0
- inventory_project/templates/admin/base_site.html +22 -0
- inventory_project/templates/admin/login.html +32 -0
- inventory_project/tests/__init__.py +0 -0
- inventory_project/tests/fixtures.py +40 -0
- inventory_project/tests/mocks.py +15 -0
- inventory_project/tests/playwright_utils.py +22 -0
- inventory_project/tests/test_admin.py +15 -0
- inventory_project/tests/test_admin_item.py +240 -0
- inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html +349 -0
- inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html +232 -0
- inventory_project/tests/test_admin_item_login_1.snapshot.html +40 -0
- inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html +637 -0
- inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html +930 -0
- inventory_project/tests/test_admin_memo.py +153 -0
- inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html +365 -0
- inventory_project/tests/test_command_shell_help_django4.2.3.snapshot.txt +60 -0
- inventory_project/tests/test_inventory_commands.py +26 -0
- inventory_project/tests/test_migrations.py +22 -0
- inventory_project/tests/test_models_item.py +24 -0
- inventory_project/tests/test_playwright_admin.py +157 -0
- inventory_project/tests/test_project_setup.py +102 -0
- inventory_project/urls.py +21 -0
- inventory_project/wsgi.py +9 -0
inventory/models/memo.py
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from bx_django_utils.filename import clean_filename
|
|
5
|
+
from ckeditor_uploader.fields import RichTextUploadingField
|
|
6
|
+
from django.db import models
|
|
7
|
+
from django.urls import reverse
|
|
8
|
+
from django.utils.translation import gettext_lazy as _
|
|
9
|
+
from django_tools.model_version_protect.models import VersionProtectBaseModel
|
|
10
|
+
from django_tools.serve_media_app.models import user_directory_path
|
|
11
|
+
|
|
12
|
+
from inventory.models.base import BaseMemoAttachmentModel, BaseModel
|
|
13
|
+
from inventory.models.links import BaseLink
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MemoModel(BaseModel, VersionProtectBaseModel):
|
|
20
|
+
"""
|
|
21
|
+
A Memo to hold some information independ of items/location
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
memo = RichTextUploadingField(
|
|
25
|
+
blank=True,
|
|
26
|
+
null=True,
|
|
27
|
+
config_name='MemoModel.description',
|
|
28
|
+
verbose_name=_('MemoModel.description.verbose_name'),
|
|
29
|
+
help_text=_('MemoModel.description.help_text'),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
def local_admin_link(self):
|
|
33
|
+
url = reverse('admin:inventory_memomodel_change', args=[self.id])
|
|
34
|
+
return url
|
|
35
|
+
|
|
36
|
+
class Meta:
|
|
37
|
+
verbose_name = _('MemoModel.verbose_name')
|
|
38
|
+
verbose_name_plural = _('MemoModel.verbose_name_plural')
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class MemoLinkModel(BaseLink):
|
|
42
|
+
memo = models.ForeignKey(MemoModel, on_delete=models.CASCADE)
|
|
43
|
+
|
|
44
|
+
def full_clean(self, **kwargs):
|
|
45
|
+
if self.user_id is None:
|
|
46
|
+
# inherit owner of this link from item instance
|
|
47
|
+
self.user_id = self.memo.user_id
|
|
48
|
+
return super().full_clean(**kwargs)
|
|
49
|
+
|
|
50
|
+
class Meta:
|
|
51
|
+
verbose_name = _('MemoLinkModel.verbose_name')
|
|
52
|
+
verbose_name_plural = _('MemoLinkModel.verbose_name_plural')
|
|
53
|
+
ordering = ('position',)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class MemoImageModel(BaseMemoAttachmentModel):
|
|
57
|
+
"""
|
|
58
|
+
Store images to Memos
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
image = models.ImageField(
|
|
62
|
+
upload_to=user_directory_path,
|
|
63
|
+
verbose_name=_('MemoImageModel.image.verbose_name'),
|
|
64
|
+
help_text=_('MemoImageModel.image.help_text'),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def __str__(self):
|
|
68
|
+
return self.name or self.image.name
|
|
69
|
+
|
|
70
|
+
def full_clean(self, **kwargs):
|
|
71
|
+
# Set name by image filename:
|
|
72
|
+
if not self.name:
|
|
73
|
+
filename = Path(self.image.name).name
|
|
74
|
+
self.name = clean_filename(filename)
|
|
75
|
+
|
|
76
|
+
return super().full_clean(**kwargs)
|
|
77
|
+
|
|
78
|
+
class Meta:
|
|
79
|
+
verbose_name = _('MemoImageModel.verbose_name')
|
|
80
|
+
verbose_name_plural = _('MemoImageModel.verbose_name_plural')
|
|
81
|
+
ordering = ('position',)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class MemoFileModel(BaseMemoAttachmentModel):
|
|
85
|
+
"""
|
|
86
|
+
Store files to Memos
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
file = models.FileField(
|
|
90
|
+
upload_to=user_directory_path,
|
|
91
|
+
verbose_name=_('MemoFileModel.file.verbose_name'),
|
|
92
|
+
help_text=_('MemoFileModel.file.help_text'),
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def __str__(self):
|
|
96
|
+
return self.name or self.file.name
|
|
97
|
+
|
|
98
|
+
def full_clean(self, **kwargs):
|
|
99
|
+
# Set name by filename:
|
|
100
|
+
if not self.name:
|
|
101
|
+
filename = Path(self.file.name).name
|
|
102
|
+
self.name = clean_filename(filename)
|
|
103
|
+
|
|
104
|
+
return super().full_clean(**kwargs)
|
|
105
|
+
|
|
106
|
+
class Meta:
|
|
107
|
+
verbose_name = _('MemoFileModel.verbose_name')
|
|
108
|
+
verbose_name_plural = _('MemoFileModel.verbose_name_plural')
|
|
109
|
+
ordering = ('position',)
|
inventory/parent_tree.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
class TreeNode:
|
|
2
|
+
def __init__(self, pk, name, current_path):
|
|
3
|
+
self.pk = pk
|
|
4
|
+
self.name = name
|
|
5
|
+
self.current_path = current_path
|
|
6
|
+
self.parent_node = None
|
|
7
|
+
self.path = None
|
|
8
|
+
|
|
9
|
+
def _set_parent(self, parent_node):
|
|
10
|
+
self.parent_node = parent_node
|
|
11
|
+
|
|
12
|
+
def _get_path(self):
|
|
13
|
+
if self.parent_node:
|
|
14
|
+
parent_path = self.parent_node._get_path()
|
|
15
|
+
return [*parent_path, self.name]
|
|
16
|
+
else:
|
|
17
|
+
return [self.name]
|
|
18
|
+
|
|
19
|
+
def _set_tree_info(self):
|
|
20
|
+
self.path = self._get_path()
|
|
21
|
+
|
|
22
|
+
@property
|
|
23
|
+
def path_string(self):
|
|
24
|
+
return ' / '.join(self.path)
|
|
25
|
+
|
|
26
|
+
def __str__(self):
|
|
27
|
+
return f'pk:{self.pk} name:"{self.name}" path:"{self.path_string}"'
|
|
28
|
+
|
|
29
|
+
def __repr__(self):
|
|
30
|
+
return f'<TreeNode {self.__str__()}>'
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ValuesListTree:
|
|
34
|
+
def __init__(self, values):
|
|
35
|
+
nodes = {}
|
|
36
|
+
|
|
37
|
+
# init all nodes:
|
|
38
|
+
for entry in values:
|
|
39
|
+
pk = entry['pk']
|
|
40
|
+
name = entry['name']
|
|
41
|
+
current_path = entry['path']
|
|
42
|
+
nodes[pk] = TreeNode(pk=pk, name=name, current_path=current_path)
|
|
43
|
+
|
|
44
|
+
# Set parents:
|
|
45
|
+
for entry in values:
|
|
46
|
+
parent_pk = entry['parent__pk']
|
|
47
|
+
if parent_pk:
|
|
48
|
+
pk = entry['pk']
|
|
49
|
+
node = nodes[pk]
|
|
50
|
+
parent_node = nodes[parent_pk]
|
|
51
|
+
node._set_parent(parent_node=parent_node)
|
|
52
|
+
|
|
53
|
+
# Set tree info:
|
|
54
|
+
nodes = list(nodes.values())
|
|
55
|
+
for node in nodes:
|
|
56
|
+
node._set_tree_info()
|
|
57
|
+
|
|
58
|
+
# Oder by hierarchy:
|
|
59
|
+
nodes.sort(key=lambda x: x.path)
|
|
60
|
+
|
|
61
|
+
self.nodes = nodes
|
|
62
|
+
|
|
63
|
+
def get_tree_path(self):
|
|
64
|
+
return [node.path_string for node in self.nodes]
|
|
65
|
+
|
|
66
|
+
def get_update_path_info(self):
|
|
67
|
+
update_info = {}
|
|
68
|
+
for node in self.nodes:
|
|
69
|
+
if node.current_path != node.path:
|
|
70
|
+
update_info[node.pk] = node.path
|
|
71
|
+
return update_info
|
inventory/permissions.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from django.contrib.auth.models import Group, Permission
|
|
2
|
+
from django.contrib.contenttypes.models import ContentType
|
|
3
|
+
|
|
4
|
+
from inventory.models import (
|
|
5
|
+
ItemFileModel,
|
|
6
|
+
ItemImageModel,
|
|
7
|
+
ItemLinkModel,
|
|
8
|
+
ItemModel,
|
|
9
|
+
LocationModel,
|
|
10
|
+
MemoFileModel,
|
|
11
|
+
MemoImageModel,
|
|
12
|
+
MemoLinkModel,
|
|
13
|
+
MemoModel,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
NORMAL_USER_GROUP_NAME = 'normal user'
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_permissions(*models):
|
|
21
|
+
content_types = []
|
|
22
|
+
for model in models:
|
|
23
|
+
content_types.append(ContentType.objects.get_for_model(model))
|
|
24
|
+
|
|
25
|
+
return Permission.objects.filter(content_type__in=content_types)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_or_create_normal_user_group():
|
|
29
|
+
"""
|
|
30
|
+
Will be called by:
|
|
31
|
+
inventory.signals.post_migrate_callback()
|
|
32
|
+
"""
|
|
33
|
+
return Group.objects.get_or_create(name=NORMAL_USER_GROUP_NAME)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def setup_normal_user_permissions(normal_user_group):
|
|
37
|
+
"""
|
|
38
|
+
Setup PyInventory "normal user" permissions.
|
|
39
|
+
Will be called by:
|
|
40
|
+
inventory.signals.post_migrate_callback()
|
|
41
|
+
"""
|
|
42
|
+
assert normal_user_group.name == NORMAL_USER_GROUP_NAME
|
|
43
|
+
permissions = get_permissions(
|
|
44
|
+
ItemFileModel,
|
|
45
|
+
ItemImageModel,
|
|
46
|
+
ItemLinkModel,
|
|
47
|
+
ItemModel,
|
|
48
|
+
LocationModel,
|
|
49
|
+
MemoFileModel,
|
|
50
|
+
MemoImageModel,
|
|
51
|
+
MemoLinkModel,
|
|
52
|
+
MemoModel,
|
|
53
|
+
)
|
|
54
|
+
existing_permissions = normal_user_group.permissions.all()
|
|
55
|
+
|
|
56
|
+
if set(permissions) != set(existing_permissions):
|
|
57
|
+
normal_user_group.permissions.set(permissions)
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
return False
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import threading
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
__request_dict = threading.local()
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_request_dict():
|
|
8
|
+
try:
|
|
9
|
+
return __request_dict.context
|
|
10
|
+
except AttributeError:
|
|
11
|
+
__request_dict.context = {}
|
|
12
|
+
return __request_dict.context
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def clear_request_dict():
|
|
16
|
+
__request_dict.context = {}
|
inventory/signals.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from django.db.models.signals import post_migrate
|
|
2
|
+
from django.dispatch import receiver
|
|
3
|
+
|
|
4
|
+
from inventory.permissions import get_or_create_normal_user_group, setup_normal_user_permissions
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@receiver(post_migrate)
|
|
8
|
+
def post_migrate_callback(sender, **kwargs):
|
|
9
|
+
normal_user_group, created = get_or_create_normal_user_group()
|
|
10
|
+
if created:
|
|
11
|
+
print(f'User group {normal_user_group} created')
|
|
12
|
+
|
|
13
|
+
updated = setup_normal_user_permissions(normal_user_group)
|
|
14
|
+
if updated:
|
|
15
|
+
print(f'Update permissions for {normal_user_group}')
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
def ltruncatechars(text, max_length, truncate='…'):
|
|
2
|
+
"""
|
|
3
|
+
>>> ltruncatechars('1234567890', max_length=10)
|
|
4
|
+
'1234567890'
|
|
5
|
+
>>> ltruncatechars('1234567890', max_length=5)
|
|
6
|
+
'…7890'
|
|
7
|
+
>>> ltruncatechars('1234567890', max_length=6)
|
|
8
|
+
'…67890'
|
|
9
|
+
>>> ltruncatechars('1234567890', max_length=6, truncate='...')
|
|
10
|
+
'...890'
|
|
11
|
+
"""
|
|
12
|
+
if len(text) > max_length:
|
|
13
|
+
length = max_length - len(truncate)
|
|
14
|
+
text = truncate + text[-length:]
|
|
15
|
+
return text
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{% load i18n admin_urls %}
|
|
2
|
+
|
|
3
|
+
<table>
|
|
4
|
+
<thead>
|
|
5
|
+
<tr>
|
|
6
|
+
<th>{% trans "No." %}</th>
|
|
7
|
+
<th>{% trans "ItemModel.verbose_name_plural" %}</th>
|
|
8
|
+
</tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody>
|
|
11
|
+
{% for obj in items %}
|
|
12
|
+
<tr>
|
|
13
|
+
<td>{{ forloop.counter }}</td>
|
|
14
|
+
<td><a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a></td>
|
|
15
|
+
</tr>
|
|
16
|
+
{% endfor %}
|
|
17
|
+
</tbody>
|
|
18
|
+
</table>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{% load i18n admin_urls %}
|
|
2
|
+
|
|
3
|
+
<table>
|
|
4
|
+
<thead>
|
|
5
|
+
<tr>
|
|
6
|
+
<th>{% trans "No." %}</th>
|
|
7
|
+
<th>{% trans "ItemModel.verbose_name_plural" %}</th>
|
|
8
|
+
</tr>
|
|
9
|
+
</thead>
|
|
10
|
+
<tbody>
|
|
11
|
+
{% for obj in items %}
|
|
12
|
+
<tr>
|
|
13
|
+
<td>{{ forloop.counter }}</td>
|
|
14
|
+
<td><a href="{% url opts|admin_urlname:'change' obj.pk %}">{{ obj }}</a></td>
|
|
15
|
+
</tr>
|
|
16
|
+
{% endfor %}
|
|
17
|
+
</tbody>
|
|
18
|
+
</table>
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django.contrib.auth.models import User
|
|
2
|
+
from model_bakery import baker
|
|
3
|
+
|
|
4
|
+
from inventory.permissions import get_or_create_normal_user_group
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def get_normal_pyinventory_user(**baker_kwargs):
|
|
8
|
+
pyinventory_user_group = get_or_create_normal_user_group()[0]
|
|
9
|
+
pyinventory_user = baker.make(User, is_staff=True, is_active=True, is_superuser=False, **baker_kwargs)
|
|
10
|
+
pyinventory_user.groups.set([pyinventory_user_group])
|
|
11
|
+
return pyinventory_user
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
from unittest import mock
|
|
2
|
+
|
|
3
|
+
from bx_django_utils.test_utils.html_assertion import HtmlAssertionMixin, assert_html_response_snapshot
|
|
4
|
+
from django.template.defaulttags import CsrfTokenNode, NowNode
|
|
5
|
+
from django.test import TestCase, override_settings
|
|
6
|
+
|
|
7
|
+
from inventory_project.tests.fixtures import get_normal_user
|
|
8
|
+
from inventory_project.tests.mocks import MockInventoryVersionString
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@override_settings(SECURE_SSL_REDIRECT=False)
|
|
12
|
+
class AdminTestCase(HtmlAssertionMixin, TestCase):
|
|
13
|
+
@classmethod
|
|
14
|
+
def setUpTestData(cls):
|
|
15
|
+
cls.normaluser = get_normal_user()
|
|
16
|
+
|
|
17
|
+
def test_empty_change_list(self):
|
|
18
|
+
self.client.force_login(self.normaluser)
|
|
19
|
+
with mock.patch.object(NowNode, 'render', return_value='MockedNowNode'), mock.patch.object(
|
|
20
|
+
CsrfTokenNode, 'render', return_value='MockedCsrfTokenNode'
|
|
21
|
+
), MockInventoryVersionString():
|
|
22
|
+
response = self.client.get(
|
|
23
|
+
path='/admin/inventory/locationmodel/',
|
|
24
|
+
)
|
|
25
|
+
assert response.status_code == 200
|
|
26
|
+
self.assert_html_parts(
|
|
27
|
+
response,
|
|
28
|
+
parts=(
|
|
29
|
+
'<title>Select Location to change | PyInventory vMockedVersionString</title>',
|
|
30
|
+
'<a href="/admin/inventory/locationmodel/add/" class="addlink">Add Location</a>',
|
|
31
|
+
'<p class="paginator">0 Locations</p>',
|
|
32
|
+
),
|
|
33
|
+
)
|
|
34
|
+
assert_html_response_snapshot(response=response, validate=False)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
<div class="" id="content">
|
|
2
|
+
<h1>
|
|
3
|
+
Select Location to change
|
|
4
|
+
</h1>
|
|
5
|
+
<div id="content-main">
|
|
6
|
+
<ul class="object-tools">
|
|
7
|
+
<li>
|
|
8
|
+
<a class="import_link" href="/admin/inventory/locationmodel/import/">
|
|
9
|
+
Import
|
|
10
|
+
</a>
|
|
11
|
+
</li>
|
|
12
|
+
<li>
|
|
13
|
+
<a class="export_link" href="/admin/inventory/locationmodel/export/?">
|
|
14
|
+
Export
|
|
15
|
+
</a>
|
|
16
|
+
</li>
|
|
17
|
+
<li>
|
|
18
|
+
<a class="recoverlink" href="/admin/inventory/locationmodel/recover/">
|
|
19
|
+
Recover deleted Locations
|
|
20
|
+
</a>
|
|
21
|
+
</li>
|
|
22
|
+
<li>
|
|
23
|
+
<a class="addlink" href="/admin/inventory/locationmodel/add/">
|
|
24
|
+
Add Location
|
|
25
|
+
</a>
|
|
26
|
+
</li>
|
|
27
|
+
</ul>
|
|
28
|
+
<div class="module filtered" id="changelist">
|
|
29
|
+
<div class="changelist-form-container">
|
|
30
|
+
<div id="toolbar">
|
|
31
|
+
<form id="changelist-search" method="get">
|
|
32
|
+
<div>
|
|
33
|
+
<!-- DIV needed for valid HTML -->
|
|
34
|
+
<label for="searchbar">
|
|
35
|
+
<img alt="Search" src="/static/admin/img/search.svg"/>
|
|
36
|
+
</label>
|
|
37
|
+
<input id="searchbar" name="q" size="40" type="text" value=""/>
|
|
38
|
+
<input type="submit" value="Search"/>
|
|
39
|
+
</div>
|
|
40
|
+
</form>
|
|
41
|
+
</div>
|
|
42
|
+
<form id="changelist-form" method="post" novalidate="">
|
|
43
|
+
MockedCsrfTokenNode
|
|
44
|
+
<p class="paginator">
|
|
45
|
+
0 Locations
|
|
46
|
+
</p>
|
|
47
|
+
</form>
|
|
48
|
+
</div>
|
|
49
|
+
<div id="changelist-filter">
|
|
50
|
+
<h2>
|
|
51
|
+
Filter
|
|
52
|
+
</h2>
|
|
53
|
+
<details data-filter-title="Limit tree depth" open="">
|
|
54
|
+
<summary>
|
|
55
|
+
By Limit tree depth
|
|
56
|
+
</summary>
|
|
57
|
+
<ul>
|
|
58
|
+
<li class="selected">
|
|
59
|
+
<a href="?">
|
|
60
|
+
All
|
|
61
|
+
</a>
|
|
62
|
+
</li>
|
|
63
|
+
<li>
|
|
64
|
+
<a href="?level=1">
|
|
65
|
+
Only root
|
|
66
|
+
</a>
|
|
67
|
+
</li>
|
|
68
|
+
<li>
|
|
69
|
+
<a href="?level=2">
|
|
70
|
+
Root + first sub
|
|
71
|
+
</a>
|
|
72
|
+
</li>
|
|
73
|
+
<li>
|
|
74
|
+
<a href="?level=3">
|
|
75
|
+
Root + first + second sub
|
|
76
|
+
</a>
|
|
77
|
+
</li>
|
|
78
|
+
</ul>
|
|
79
|
+
</details>
|
|
80
|
+
</div>
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
<br class="clear"/>
|
|
84
|
+
</div>
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import tempfile
|
|
2
|
+
from unittest import mock
|
|
3
|
+
|
|
4
|
+
from django.http import FileResponse
|
|
5
|
+
from django.test import TestCase, override_settings
|
|
6
|
+
from django_tools.serve_media_app.models import UserMediaTokenModel
|
|
7
|
+
from model_bakery import baker
|
|
8
|
+
|
|
9
|
+
from inventory.models import ItemImageModel
|
|
10
|
+
from inventory.tests.fixtures.users import get_normal_pyinventory_user
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@override_settings(SECURE_SSL_REDIRECT=True)
|
|
14
|
+
class ItemImagesTestCase(TestCase):
|
|
15
|
+
def test_basics(self):
|
|
16
|
+
with mock.patch('secrets.token_urlsafe', return_value='user1token'):
|
|
17
|
+
pyinventory_user1 = get_normal_pyinventory_user(id=1)
|
|
18
|
+
|
|
19
|
+
with mock.patch('secrets.token_urlsafe', return_value='user2token'):
|
|
20
|
+
pyinventory_user2 = get_normal_pyinventory_user(id=2)
|
|
21
|
+
|
|
22
|
+
token1_instance = UserMediaTokenModel.objects.get(user=pyinventory_user1)
|
|
23
|
+
assert repr(token1_instance) == (f"<UserMediaTokenModel: user:1 token:'user1token' ({token1_instance.pk})>")
|
|
24
|
+
token2_instance = UserMediaTokenModel.objects.get(user=pyinventory_user2)
|
|
25
|
+
assert repr(token2_instance) == (f"<UserMediaTokenModel: user:2 token:'user2token' ({token2_instance.pk})>")
|
|
26
|
+
|
|
27
|
+
with tempfile.TemporaryDirectory() as temp:
|
|
28
|
+
with override_settings(MEDIA_ROOT=temp):
|
|
29
|
+
with mock.patch('secrets.token_urlsafe', return_value='12345678901234567890'):
|
|
30
|
+
image_instance = baker.make(ItemImageModel, user=pyinventory_user1, _create_files=True)
|
|
31
|
+
|
|
32
|
+
assert image_instance.image is not None
|
|
33
|
+
url = image_instance.image.url
|
|
34
|
+
assert url == '/media/user1token/12345678901234567890/mock_img.jpeg'
|
|
35
|
+
|
|
36
|
+
# HTTP -> HTTPS redirect:
|
|
37
|
+
response = self.client.get('/media/user1token/12345678901234567890/mock_img.jpeg', secure=False)
|
|
38
|
+
self.assertRedirects(
|
|
39
|
+
response,
|
|
40
|
+
status_code=301,
|
|
41
|
+
expected_url='https://testserver/media/user1token/12345678901234567890/mock_img.jpeg',
|
|
42
|
+
fetch_redirect_response=False,
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
# Anonymous has no access:
|
|
46
|
+
response = self.client.get(
|
|
47
|
+
'/media/user1token/12345678901234567890/mock_img.jpeg',
|
|
48
|
+
secure=True,
|
|
49
|
+
)
|
|
50
|
+
assert response.status_code == 403
|
|
51
|
+
|
|
52
|
+
# Can't access with wrong user:
|
|
53
|
+
self.client.force_login(pyinventory_user2)
|
|
54
|
+
response = self.client.get(
|
|
55
|
+
'/media/user1token/12345678901234567890/mock_img.jpeg',
|
|
56
|
+
secure=True,
|
|
57
|
+
)
|
|
58
|
+
assert response.status_code == 403
|
|
59
|
+
|
|
60
|
+
# Can access with the right user:
|
|
61
|
+
self.client.force_login(pyinventory_user1)
|
|
62
|
+
response = self.client.get(
|
|
63
|
+
'/media/user1token/12345678901234567890/mock_img.jpeg',
|
|
64
|
+
secure=True,
|
|
65
|
+
)
|
|
66
|
+
assert response.status_code == 200
|
|
67
|
+
assert isinstance(response, FileResponse)
|
|
68
|
+
assert response.getvalue() == image_instance.image.open('rb').read()
|
|
69
|
+
|
|
70
|
+
# Test whats happen, if token was deleted
|
|
71
|
+
UserMediaTokenModel.objects.all().delete()
|
|
72
|
+
response = self.client.get(
|
|
73
|
+
'/media/user1token/12345678901234567890/mock_img.jpeg',
|
|
74
|
+
secure=True,
|
|
75
|
+
)
|
|
76
|
+
assert response.status_code == 400 # SuspiciousOperation -> HttpResponseBadRequest
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import logging
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
import requests_mock
|
|
6
|
+
from bx_django_utils.test_utils.datetime import MockDatetimeGenerator
|
|
7
|
+
from bx_py_utils.test_utils.datetime import parse_dt
|
|
8
|
+
from django.test import TestCase
|
|
9
|
+
from django.utils import timezone
|
|
10
|
+
from model_bakery import baker
|
|
11
|
+
|
|
12
|
+
from inventory.models import ItemLinkModel, ItemModel
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ItemLinkModelTestCase(TestCase):
|
|
16
|
+
def test_set_name_by_request(self):
|
|
17
|
+
with self.assertLogs('django_tools'):
|
|
18
|
+
item = baker.make(ItemModel)
|
|
19
|
+
|
|
20
|
+
link = ItemLinkModel(item=item, url='http://test.tld/foo/bar')
|
|
21
|
+
|
|
22
|
+
offset = datetime.timedelta(seconds=30)
|
|
23
|
+
with patch.object(timezone, 'now', MockDatetimeGenerator(offset)):
|
|
24
|
+
with requests_mock.Mocker() as m:
|
|
25
|
+
m.get('http://test.tld/foo/bar', text='No title')
|
|
26
|
+
|
|
27
|
+
assert link.last_check is None
|
|
28
|
+
with self.assertLogs('inventory.models.links', level=logging.WARNING) as logs:
|
|
29
|
+
link.full_clean()
|
|
30
|
+
assert link.page_title is None
|
|
31
|
+
assert link.name is None
|
|
32
|
+
assert link.last_check == parse_dt('2000-01-01T00:00:30+0000')
|
|
33
|
+
|
|
34
|
+
logs = logs.output
|
|
35
|
+
assert logs == ["WARNING:inventory.models.links:No title found in 'http://test.tld/foo/bar'"]
|
|
36
|
+
|
|
37
|
+
# We should not create request on every admin save call
|
|
38
|
+
|
|
39
|
+
with self.assertLogs('inventory.models.links', level=logging.DEBUG) as logs:
|
|
40
|
+
link.full_clean()
|
|
41
|
+
assert link.page_title is None
|
|
42
|
+
assert link.name is None
|
|
43
|
+
|
|
44
|
+
logs = logs.output
|
|
45
|
+
assert logs == [
|
|
46
|
+
'DEBUG:inventory.models.links:Last check is 0:00:30 ago.',
|
|
47
|
+
"INFO:inventory.models.links:Skip request for: 'http://test.tld/foo/bar'",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
# Next try after 1 Min
|
|
51
|
+
|
|
52
|
+
m.get('http://test.tld/foo/bar', text='<title>A <boom>Title</boom>!</title>')
|
|
53
|
+
with self.assertLogs('inventory.models.links', level=logging.INFO) as logs:
|
|
54
|
+
link.full_clean()
|
|
55
|
+
assert link.page_title == 'A Title!'
|
|
56
|
+
assert link.name == 'A Title!'
|
|
57
|
+
|
|
58
|
+
logs = logs.output
|
|
59
|
+
assert logs == ["INFO:inventory.models.links:Found title: 'A <boom>Title</boom>!'"]
|
|
60
|
+
|
|
61
|
+
# Don't make requests, if we have a link name!
|
|
62
|
+
|
|
63
|
+
with requests_mock.Mocker():
|
|
64
|
+
with self.assertLogs('inventory.models.links', level=logging.DEBUG) as logs:
|
|
65
|
+
link.full_clean()
|
|
66
|
+
assert link.page_title == 'A Title!'
|
|
67
|
+
assert link.name == 'A Title!'
|
|
68
|
+
|
|
69
|
+
logs = logs.output
|
|
70
|
+
assert logs == [
|
|
71
|
+
("DEBUG:inventory.models.links:Skip link request:" " because we have a name: 'A Title!'")
|
|
72
|
+
]
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
import io
|
|
3
|
+
|
|
4
|
+
from django.contrib.auth.models import User
|
|
5
|
+
from django.core import management
|
|
6
|
+
from django.test import TestCase
|
|
7
|
+
|
|
8
|
+
from inventory.management.commands import seed_data
|
|
9
|
+
from inventory.models import ItemModel, LocationModel
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ManagementCommandTestCase(TestCase):
|
|
13
|
+
def test_seed_data_command(self):
|
|
14
|
+
output = io.StringIO()
|
|
15
|
+
|
|
16
|
+
management.call_command(seed_data.Command(), user_count=2, location_count=2, item_count=2, stdout=output)
|
|
17
|
+
assert User.objects.count() == 2
|
|
18
|
+
assert LocationModel.objects.count() == 8
|
|
19
|
+
assert ItemModel.objects.count() == 16
|
|
20
|
+
|
|
21
|
+
output = output.getvalue()
|
|
22
|
+
reference = inspect.cleandoc(
|
|
23
|
+
'''
|
|
24
|
+
Fill database with example data
|
|
25
|
+
____________________________________________________________________________________________________
|
|
26
|
+
Create seed data for user seed-data-user-1
|
|
27
|
+
Room 1 › Cupboard 1 › Drawer 1 | Equipment 001
|
|
28
|
+
Room 1 › Cupboard 1 › Drawer 1 | Equipment 001 › Item 001
|
|
29
|
+
Room 1 › Cupboard 1 › Drawer 1 | Equipment 001 › Item 001 › Part 001
|
|
30
|
+
Room 1 › Cupboard 1 › Drawer 1 | Equipment 001 › Item 001 › Part 002
|
|
31
|
+
Room 1 › Cupboard 1 › Drawer 2 | Equipment 002
|
|
32
|
+
Room 1 › Cupboard 1 › Drawer 2 | Equipment 002 › Item 002
|
|
33
|
+
Room 1 › Cupboard 1 › Drawer 2 | Equipment 002 › Item 002 › Part 003
|
|
34
|
+
Room 1 › Cupboard 1 › Drawer 2 | Equipment 002 › Item 002 › Part 004
|
|
35
|
+
____________________________________________________________________________________________________
|
|
36
|
+
Create seed data for user seed-data-user-2
|
|
37
|
+
Room 1 › Cupboard 1 › Drawer 1 | Equipment 003
|
|
38
|
+
Room 1 › Cupboard 1 › Drawer 1 | Equipment 003 › Item 003
|
|
39
|
+
Room 1 › Cupboard 1 › Drawer 1 | Equipment 003 › Item 003 › Part 005
|
|
40
|
+
Room 1 › Cupboard 1 › Drawer 1 | Equipment 003 › Item 003 › Part 006
|
|
41
|
+
Room 1 › Cupboard 1 › Drawer 2 | Equipment 004
|
|
42
|
+
Room 1 › Cupboard 1 › Drawer 2 | Equipment 004 › Item 004
|
|
43
|
+
Room 1 › Cupboard 1 › Drawer 2 | Equipment 004 › Item 004 › Part 007
|
|
44
|
+
Room 1 › Cupboard 1 › Drawer 2 | Equipment 004 › Item 004 › Part 008
|
|
45
|
+
|
|
46
|
+
Seed data created.
|
|
47
|
+
'''
|
|
48
|
+
)
|
|
49
|
+
assert output.strip() == reference.strip()
|