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.
Files changed (101) hide show
  1. PyInventory-0.19.0.dist-info/AUTHORS +14 -0
  2. PyInventory-0.19.0.dist-info/LICENSE +674 -0
  3. PyInventory-0.19.0.dist-info/METADATA +347 -0
  4. PyInventory-0.19.0.dist-info/RECORD +101 -0
  5. PyInventory-0.19.0.dist-info/WHEEL +5 -0
  6. PyInventory-0.19.0.dist-info/entry_points.txt +2 -0
  7. PyInventory-0.19.0.dist-info/top_level.txt +2 -0
  8. inventory/__init__.py +7 -0
  9. inventory/admin/__init__.py +3 -0
  10. inventory/admin/base.py +104 -0
  11. inventory/admin/item.py +169 -0
  12. inventory/admin/location.py +78 -0
  13. inventory/admin/memo.py +76 -0
  14. inventory/admin/tagulous_fix.py +45 -0
  15. inventory/apps.py +18 -0
  16. inventory/ckeditor_upload.py +15 -0
  17. inventory/context_processors.py +5 -0
  18. inventory/forms.py +36 -0
  19. inventory/locale/ca/LC_MESSAGES/django.mo +0 -0
  20. inventory/locale/ca/LC_MESSAGES/django.po +297 -0
  21. inventory/locale/de/LC_MESSAGES/django.mo +0 -0
  22. inventory/locale/de/LC_MESSAGES/django.po +294 -0
  23. inventory/locale/en/LC_MESSAGES/django.mo +0 -0
  24. inventory/locale/en/LC_MESSAGES/django.po +294 -0
  25. inventory/locale/es/LC_MESSAGES/django.mo +0 -0
  26. inventory/locale/es/LC_MESSAGES/django.po +297 -0
  27. inventory/management/__init__.py +0 -0
  28. inventory/management/commands/__init__.py +0 -0
  29. inventory/management/commands/seed_data.py +135 -0
  30. inventory/management/commands/tree.py +62 -0
  31. inventory/middlewares.py +21 -0
  32. inventory/migrations/0001_initial.py +596 -0
  33. inventory/migrations/0002_auto_20201017_2211.py +87 -0
  34. inventory/migrations/0003_auto_20201024_1830.py +23 -0
  35. inventory/migrations/0004_item_user_images.py +129 -0
  36. inventory/migrations/0005_serve_uploads_by_django_tools.py +77 -0
  37. inventory/migrations/0006_refactor_image_model.py +46 -0
  38. inventory/migrations/0007_add_file_attachment.py +128 -0
  39. inventory/migrations/0008_last_check_datetime.py +23 -0
  40. inventory/migrations/0009_add_memo.py +517 -0
  41. inventory/migrations/0010_version_protect_models.py +37 -0
  42. inventory/migrations/0011_parent_tree1.py +97 -0
  43. inventory/migrations/0012_parent_tree2.py +20 -0
  44. inventory/migrations/0013_alter_itemmodel_location.py +26 -0
  45. inventory/migrations/__init__.py +0 -0
  46. inventory/models/__init__.py +3 -0
  47. inventory/models/base.py +239 -0
  48. inventory/models/item.py +228 -0
  49. inventory/models/links.py +104 -0
  50. inventory/models/location.py +24 -0
  51. inventory/models/memo.py +109 -0
  52. inventory/parent_tree.py +71 -0
  53. inventory/permissions.py +60 -0
  54. inventory/request_dict.py +16 -0
  55. inventory/signals.py +15 -0
  56. inventory/string_utils.py +15 -0
  57. inventory/templates/admin/item/related_items.html +18 -0
  58. inventory/templates/admin/location/items.html +18 -0
  59. inventory/tests/__init__.py +0 -0
  60. inventory/tests/fixtures/__init__.py +0 -0
  61. inventory/tests/fixtures/users.py +11 -0
  62. inventory/tests/test_admin_location.py +34 -0
  63. inventory/tests/test_admin_location_empty_change_list_1.snapshot.html +84 -0
  64. inventory/tests/test_item_images.py +76 -0
  65. inventory/tests/test_link_model.py +72 -0
  66. inventory/tests/test_management_command_seed_data.py +49 -0
  67. inventory/tests/test_management_command_tree.py +27 -0
  68. inventory/tests/test_parent_tree.py +40 -0
  69. inventory/tests/test_parent_tree_model.py +139 -0
  70. inventory_project/__init__.py +12 -0
  71. inventory_project/__main__.py +17 -0
  72. inventory_project/manage.py +41 -0
  73. inventory_project/middlewares.py +23 -0
  74. inventory_project/publish.py +21 -0
  75. inventory_project/settings/__init__.py +0 -0
  76. inventory_project/settings/local.py +74 -0
  77. inventory_project/settings/prod.py +393 -0
  78. inventory_project/settings/tests.py +45 -0
  79. inventory_project/templates/admin/base_site.html +22 -0
  80. inventory_project/templates/admin/login.html +32 -0
  81. inventory_project/tests/__init__.py +0 -0
  82. inventory_project/tests/fixtures.py +40 -0
  83. inventory_project/tests/mocks.py +15 -0
  84. inventory_project/tests/playwright_utils.py +22 -0
  85. inventory_project/tests/test_admin.py +15 -0
  86. inventory_project/tests/test_admin_item.py +240 -0
  87. inventory_project/tests/test_admin_item_auto_group_items_1.snapshot.html +349 -0
  88. inventory_project/tests/test_admin_item_auto_group_items_2.snapshot.html +232 -0
  89. inventory_project/tests/test_admin_item_login_1.snapshot.html +40 -0
  90. inventory_project/tests/test_admin_item_normal_user_create_minimal_item_1.snapshot.html +637 -0
  91. inventory_project/tests/test_admin_item_normal_user_create_minimal_item_2.snapshot.html +930 -0
  92. inventory_project/tests/test_admin_memo.py +153 -0
  93. inventory_project/tests/test_admin_memo_normal_user_create_minimal_item_1.snapshot.html +365 -0
  94. inventory_project/tests/test_command_shell_help_django4.2.3.snapshot.txt +60 -0
  95. inventory_project/tests/test_inventory_commands.py +26 -0
  96. inventory_project/tests/test_migrations.py +22 -0
  97. inventory_project/tests/test_models_item.py +24 -0
  98. inventory_project/tests/test_playwright_admin.py +157 -0
  99. inventory_project/tests/test_project_setup.py +102 -0
  100. inventory_project/urls.py +21 -0
  101. inventory_project/wsgi.py +9 -0
@@ -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',)
@@ -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
@@ -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()