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,26 @@
1
+ # Generated by Django 3.2.15 on 2022-09-30 18:30
2
+
3
+ import django.db.models.deletion
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+ dependencies = [
9
+ ('inventory', '0012_parent_tree2'),
10
+ ]
11
+
12
+ operations = [
13
+ migrations.AlterField(
14
+ model_name='itemmodel',
15
+ name='location',
16
+ field=models.ForeignKey(
17
+ blank=True,
18
+ help_text='ItemModel.location.help_text',
19
+ null=True,
20
+ on_delete=django.db.models.deletion.SET_NULL,
21
+ related_name='items',
22
+ to='inventory.locationmodel',
23
+ verbose_name='ItemModel.location.verbose_name',
24
+ ),
25
+ ),
26
+ ]
File without changes
@@ -0,0 +1,3 @@
1
+ from inventory.models.item import ItemFileModel, ItemImageModel, ItemLinkModel, ItemModel # noqa
2
+ from inventory.models.location import LocationModel # noqa
3
+ from inventory.models.memo import MemoFileModel, MemoImageModel, MemoLinkModel, MemoModel # noqa
@@ -0,0 +1,239 @@
1
+ import logging
2
+ import re
3
+ import time
4
+ import unicodedata
5
+ import uuid
6
+
7
+ import tagulous.models
8
+ from bx_django_utils.models.timetracking import TimetrackingBaseModel
9
+ from django.conf import settings
10
+ from django.db import models
11
+ from django.db.models import QuerySet
12
+ from django.utils.translation import gettext_lazy as _
13
+
14
+ from inventory.parent_tree import ValuesListTree
15
+ from inventory.string_utils import ltruncatechars
16
+
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ class BaseModel(TimetrackingBaseModel):
22
+ id = models.UUIDField(
23
+ primary_key=True,
24
+ default=uuid.uuid4,
25
+ editable=False,
26
+ verbose_name=_('BaseModel.id.verbose_name'),
27
+ help_text=_('BaseModel.id.help_text'),
28
+ )
29
+ user = models.ForeignKey( # "Owner" of this entry
30
+ settings.AUTH_USER_MODEL,
31
+ related_name='+',
32
+ on_delete=models.CASCADE,
33
+ editable=False, # Must be set automatically and never changed
34
+ verbose_name=_('BaseModel.user.verbose_name'),
35
+ help_text=_('BaseModel.user.help_text'),
36
+ )
37
+ name = models.CharField(
38
+ max_length=255, verbose_name=_('BaseModel.name.verbose_name'), help_text=_('BaseModel.name.help_text')
39
+ )
40
+ tags = tagulous.models.TagField(
41
+ blank=True,
42
+ case_sensitive=False,
43
+ force_lowercase=False,
44
+ space_delimiter=False,
45
+ max_count=10,
46
+ verbose_name=_('BaseModel.tags.verbose_name'),
47
+ help_text=_('BaseModel.tags.help_text'),
48
+ )
49
+
50
+ def __str__(self):
51
+ return self.name
52
+
53
+ class Meta:
54
+ abstract = True
55
+
56
+
57
+ def nomalize_text(text):
58
+ """
59
+ >>> nomalize_text('Foo Bar 1 §$% äö-üß +')
60
+ 'foobar1aou'
61
+ """
62
+ text = unicodedata.normalize('NFKD', text).encode('ascii', 'ignore').decode('ascii')
63
+ text = text.lower()
64
+ text = re.sub(r'[^\w]', '', text)
65
+ return text
66
+
67
+
68
+ def generate_path_str(path):
69
+ """
70
+ >>> generate_path_str(['Foo', 'B a r', '1 §$% äö-üß +'])
71
+ 'foo 0 bar 0 1aou'
72
+ """
73
+ # The choice of the separator is very important for the correct sorting by the database!
74
+ # Use 0, because this character is used for sorting and is the first character in the charset.
75
+ # The spaces are only visual separators ;)
76
+ return ' 0 '.join(nomalize_text(part) for part in path)
77
+
78
+
79
+ class ParentTreeModelManager(models.Manager):
80
+ def update_tree_info(self) -> None:
81
+ start_time = time.monotonic()
82
+
83
+ values = self.all().values('pk', 'name', 'parent__pk', 'path')
84
+ tree = ValuesListTree(values=values)
85
+ tree_path = tree.get_tree_path()
86
+ logger.debug('Tree path: %r', tree_path)
87
+ update_path_info = tree.get_update_path_info()
88
+
89
+ duration = (time.monotonic() - start_time) * 1000
90
+ logger.info('Get update_path_info: %r in %ims', update_path_info, duration)
91
+
92
+ if not update_path_info:
93
+ logger.info('No tree path changed, ok')
94
+ else:
95
+ start_time = time.monotonic()
96
+
97
+ entries = self.filter(pk__in=update_path_info.keys())
98
+ for entry in entries:
99
+ path = update_path_info[entry.pk]
100
+ entry.path = path
101
+ entry.path_str = generate_path_str(path)
102
+ entry.level = len(path)
103
+
104
+ self.bulk_update(entries, ['path', 'path_str', 'level'])
105
+
106
+ duration = (time.monotonic() - start_time) * 1000
107
+ logger.info('Update %i entries in %ims', len(entries), duration)
108
+
109
+ def related_objects(self, instance: 'BaseParentTreeModel') -> QuerySet:
110
+ """
111
+ Returns a QuerySet with relation section of the tree
112
+ """
113
+ path = instance.path
114
+ if path is None:
115
+ # Not saved -> Can't have related objects ;)
116
+ return self.none()
117
+
118
+ root_entry = path[0]
119
+ qs = self.all()
120
+ qs = qs.filter(path__0=root_entry)
121
+ return qs
122
+
123
+
124
+ class BaseParentTreeModel(BaseModel):
125
+ path = models.JSONField(
126
+ blank=True,
127
+ null=True,
128
+ editable=False,
129
+ )
130
+ path_str = models.TextField(
131
+ blank=True,
132
+ null=True,
133
+ editable=False,
134
+ )
135
+ level = models.PositiveSmallIntegerField(
136
+ blank=True,
137
+ null=True,
138
+ editable=False,
139
+ )
140
+ parent = models.ForeignKey(
141
+ 'self',
142
+ on_delete=models.SET_NULL,
143
+ blank=True,
144
+ null=True,
145
+ verbose_name=_('LocationModel.parent.verbose_name'),
146
+ help_text=_('LocationModel.parent.help_text'),
147
+ )
148
+
149
+ objects = models.Manager()
150
+ tree_objects = ParentTreeModelManager()
151
+
152
+ def save(self, **kwargs):
153
+ if not self.path:
154
+ if self.parent:
155
+ path = self.parent.path
156
+ if path:
157
+ self.path = [*path, self.name]
158
+ else:
159
+ self.path = [self.name]
160
+ self.path_str = generate_path_str(self.path)
161
+ self.level = len(self.path)
162
+ logger.info('Init path with: %r', self.path)
163
+
164
+ self.full_clean()
165
+ super().save(**kwargs)
166
+ self.__class__.tree_objects.update_tree_info()
167
+
168
+ def __str__(self):
169
+ if self.path:
170
+ text = ' › '.join(self.path)
171
+ text = ltruncatechars(text, max_length=settings.TREE_PATH_STR_MAX_LENGTH)
172
+ return text
173
+
174
+ return self.name
175
+
176
+ class Meta:
177
+ abstract = True
178
+
179
+
180
+ class BaseAttachmentModel(BaseModel):
181
+ """
182
+ Base model to store files or images to Items
183
+ """
184
+
185
+ name = models.CharField(
186
+ null=True,
187
+ blank=True,
188
+ max_length=255,
189
+ verbose_name=_('BaseItemAttachmentModel.name.verbose_name'),
190
+ help_text=_('BaseItemAttachmentModel.name.help_text'),
191
+ )
192
+ position = models.PositiveSmallIntegerField(
193
+ # Note: Will be set in admin via adminsortable2
194
+ # The JavaScript which performs the sorting is 1-indexed !
195
+ default=0,
196
+ blank=False,
197
+ null=False,
198
+ )
199
+
200
+ def __str__(self):
201
+ return self.name
202
+
203
+ def full_clean(self, *, parent_instance, **kwargs):
204
+ if self.user_id is None:
205
+ # inherit owner of this link from parent model instance
206
+ self.user_id = parent_instance.user_id
207
+
208
+ return super().full_clean(**kwargs)
209
+
210
+ class Meta:
211
+ abstract = True
212
+
213
+
214
+ class BaseItemAttachmentModel(BaseAttachmentModel):
215
+ """
216
+ Base model to store files or images to Items
217
+ """
218
+
219
+ item = models.ForeignKey('ItemModel', on_delete=models.CASCADE)
220
+
221
+ def full_clean(self, **kwargs):
222
+ return super().full_clean(parent_instance=self.item, **kwargs)
223
+
224
+ class Meta:
225
+ abstract = True
226
+
227
+
228
+ class BaseMemoAttachmentModel(BaseAttachmentModel):
229
+ """
230
+ Base model to store files or images to Memos
231
+ """
232
+
233
+ memo = models.ForeignKey('MemoModel', on_delete=models.CASCADE)
234
+
235
+ def full_clean(self, **kwargs):
236
+ return super().full_clean(parent_instance=self.memo, **kwargs)
237
+
238
+ class Meta:
239
+ abstract = True
@@ -0,0 +1,228 @@
1
+ import logging
2
+ from pathlib import Path
3
+
4
+ import tagulous.models
5
+ from bx_django_utils.filename import clean_filename
6
+ from ckeditor_uploader.fields import RichTextUploadingField
7
+ from django.db import models
8
+ from django.urls import reverse
9
+ from django.utils.translation import gettext_lazy as _
10
+ from django_tools.model_version_protect.models import VersionProtectBaseModel
11
+ from django_tools.serve_media_app.models import user_directory_path
12
+
13
+ from inventory.models.base import BaseItemAttachmentModel, BaseParentTreeModel
14
+ from inventory.models.links import BaseLink
15
+
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class ItemQuerySet(models.QuerySet):
21
+ def sort(self):
22
+ return self.order_by('kind', 'producer', 'name')
23
+
24
+
25
+ class ItemModel(BaseParentTreeModel, VersionProtectBaseModel):
26
+ """
27
+ A Item that can be described and store somewhere ;)
28
+ """
29
+
30
+ objects = ItemQuerySet.as_manager()
31
+
32
+ kind = tagulous.models.TagField(
33
+ case_sensitive=False,
34
+ force_lowercase=False,
35
+ space_delimiter=False,
36
+ max_count=3,
37
+ verbose_name=_('ItemModel.kind.verbose_name'),
38
+ help_text=_('ItemModel.kind.help_text'),
39
+ )
40
+ producer = tagulous.models.TagField(
41
+ blank=True,
42
+ case_sensitive=False,
43
+ force_lowercase=False,
44
+ space_delimiter=False,
45
+ max_count=1,
46
+ verbose_name=_('ItemModel.producer.verbose_name'),
47
+ help_text=_('ItemModel.producer.help_text'),
48
+ )
49
+ description = RichTextUploadingField(
50
+ blank=True,
51
+ null=True,
52
+ config_name='ItemModel.description',
53
+ verbose_name=_('ItemModel.description.verbose_name'),
54
+ help_text=_('ItemModel.description.help_text'),
55
+ )
56
+ fcc_id = models.CharField(
57
+ max_length=20,
58
+ blank=True,
59
+ null=True,
60
+ verbose_name=_('ItemModel.fcc_id.verbose_name'),
61
+ help_text=_('ItemModel.fcc_id.help_text'),
62
+ )
63
+ location = models.ForeignKey(
64
+ 'inventory.LocationModel',
65
+ blank=True,
66
+ null=True,
67
+ on_delete=models.SET_NULL,
68
+ related_name='items',
69
+ verbose_name=_('ItemModel.location.verbose_name'),
70
+ help_text=_('ItemModel.location.help_text'),
71
+ )
72
+
73
+ # ________________________________________________________________________
74
+ # lent
75
+
76
+ lent_to = models.CharField(
77
+ max_length=64,
78
+ blank=True,
79
+ null=True,
80
+ verbose_name=_('ItemModel.lent_to.verbose_name'),
81
+ help_text=_('ItemModel.lent_to.help_text'),
82
+ )
83
+ lent_from_date = models.DateField(
84
+ blank=True,
85
+ null=True,
86
+ verbose_name=_('ItemModel.lent_from_date.verbose_name'),
87
+ help_text=_('ItemModel.lent_from_date.help_text'),
88
+ )
89
+ lent_until_date = models.DateField(
90
+ blank=True,
91
+ null=True,
92
+ verbose_name=_('ItemModel.lent_until_date.verbose_name'),
93
+ help_text=_('ItemModel.lent_until_date.help_text'),
94
+ )
95
+
96
+ # ________________________________________________________________________
97
+ # received
98
+
99
+ received_from = models.CharField(
100
+ max_length=64,
101
+ blank=True,
102
+ null=True,
103
+ verbose_name=_('ItemModel.received_from.verbose_name'),
104
+ help_text=_('ItemModel.received_from.help_text'),
105
+ )
106
+ received_date = models.DateField(
107
+ blank=True,
108
+ null=True,
109
+ verbose_name=_('ItemModel.received_date.verbose_name'),
110
+ help_text=_('ItemModel.received_date.help_text'),
111
+ )
112
+ received_price = models.DecimalField(
113
+ decimal_places=2,
114
+ max_digits=6, # up to 9999 with a resolution of 2 decimal places
115
+ blank=True,
116
+ null=True,
117
+ verbose_name=_('ItemModel.received_price.verbose_name'),
118
+ help_text=_('ItemModel.received_price.help_text'),
119
+ )
120
+
121
+ # ________________________________________________________________________
122
+ # handed over
123
+
124
+ handed_over_to = models.CharField(
125
+ max_length=64,
126
+ blank=True,
127
+ null=True,
128
+ verbose_name=_('ItemModel.handed_over_to.verbose_name'),
129
+ help_text=_('ItemModel.handed_over_to.help_text'),
130
+ )
131
+ handed_over_date = models.DateField(
132
+ blank=True,
133
+ null=True,
134
+ verbose_name=_('ItemModel.handed_over_date.verbose_name'),
135
+ help_text=_('ItemModel.handed_over_date.help_text'),
136
+ )
137
+ handed_over_price = models.DecimalField(
138
+ decimal_places=2,
139
+ max_digits=6, # up to 9999 with a resolution of 2 decimal places
140
+ blank=True,
141
+ null=True,
142
+ verbose_name=_('ItemModel.handed_over_price.verbose_name'),
143
+ help_text=_('ItemModel.handed_over_price.help_text'),
144
+ )
145
+
146
+ def local_admin_link(self):
147
+ url = reverse('admin:inventory_itemmodel_change', args=[self.id])
148
+ return url
149
+
150
+ def verbose_name(self):
151
+ parts = [str(part) for part in (self.kind, self.producer, self.name)]
152
+ return ' - '.join(part for part in parts if part)
153
+
154
+ class Meta:
155
+ ordering = ('path_str',)
156
+ verbose_name = _('ItemModel.verbose_name')
157
+ verbose_name_plural = _('ItemModel.verbose_name_plural')
158
+
159
+
160
+ class ItemLinkModel(BaseLink):
161
+ item = models.ForeignKey(ItemModel, on_delete=models.CASCADE)
162
+
163
+ def full_clean(self, **kwargs):
164
+ if self.user_id is None:
165
+ # inherit owner of this link from item instance
166
+ self.user_id = self.item.user_id
167
+ return super().full_clean(**kwargs)
168
+
169
+ class Meta:
170
+ verbose_name = _('ItemLinkModel.verbose_name')
171
+ verbose_name_plural = _('ItemLinkModel.verbose_name_plural')
172
+ ordering = ('position',)
173
+
174
+
175
+ class ItemImageModel(BaseItemAttachmentModel):
176
+ """
177
+ Store images to Items
178
+ """
179
+
180
+ image = models.ImageField(
181
+ upload_to=user_directory_path,
182
+ verbose_name=_('ItemImageModel.image.verbose_name'),
183
+ help_text=_('ItemImageModel.image.help_text'),
184
+ )
185
+
186
+ def __str__(self):
187
+ return self.name or self.image.name
188
+
189
+ def full_clean(self, **kwargs):
190
+ # Set name by image filename:
191
+ if not self.name:
192
+ filename = Path(self.image.name).name
193
+ self.name = clean_filename(filename)
194
+
195
+ return super().full_clean(**kwargs)
196
+
197
+ class Meta:
198
+ verbose_name = _('ItemImageModel.verbose_name')
199
+ verbose_name_plural = _('ItemImageModel.verbose_name_plural')
200
+ ordering = ('position',)
201
+
202
+
203
+ class ItemFileModel(BaseItemAttachmentModel):
204
+ """
205
+ Store files to Items
206
+ """
207
+
208
+ file = models.FileField(
209
+ upload_to=user_directory_path,
210
+ verbose_name=_('ItemFileModel.file.verbose_name'),
211
+ help_text=_('ItemFileModel.file.help_text'),
212
+ )
213
+
214
+ def __str__(self):
215
+ return self.name or self.file.name
216
+
217
+ def full_clean(self, **kwargs):
218
+ # Set name by filename:
219
+ if not self.name:
220
+ filename = Path(self.file.name).name
221
+ self.name = clean_filename(filename)
222
+
223
+ return super().full_clean(**kwargs)
224
+
225
+ class Meta:
226
+ verbose_name = _('ItemFileModel.verbose_name')
227
+ verbose_name_plural = _('ItemFileModel.verbose_name_plural')
228
+ ordering = ('position',)
@@ -0,0 +1,104 @@
1
+ import datetime
2
+ import logging
3
+ import re
4
+
5
+ import requests
6
+ from django.db import models
7
+ from django.template.defaultfilters import striptags
8
+ from django.utils import timezone
9
+ from django.utils.translation import gettext_lazy as _
10
+
11
+ from inventory.models.base import BaseModel
12
+
13
+
14
+ logger = logging.getLogger(__name__)
15
+
16
+
17
+ class BaseLink(BaseModel):
18
+ name = models.CharField(
19
+ max_length=255,
20
+ blank=True,
21
+ null=True,
22
+ verbose_name=_('BaseLink.name.verbose_name'),
23
+ help_text=_('BaseLink.name.help_text'),
24
+ )
25
+ url = models.URLField(verbose_name=_('Link.url.verbose_name'), help_text=_('Link.url.help_text'))
26
+ last_check = models.DateTimeField(
27
+ blank=True,
28
+ null=True,
29
+ editable=False,
30
+ verbose_name=_('Link.url.verbose_name'),
31
+ help_text=_('Link.url.help_text'),
32
+ )
33
+ status_code = models.PositiveSmallIntegerField(
34
+ blank=True,
35
+ null=True,
36
+ editable=False,
37
+ verbose_name=_('Link.status_code.verbose_name'),
38
+ help_text=_('Link.status_code.help_text'),
39
+ )
40
+ page_title = models.CharField(
41
+ max_length=255,
42
+ blank=True,
43
+ null=True,
44
+ editable=False,
45
+ verbose_name=_('Link.page_title.verbose_name'),
46
+ help_text=_('Link.page_title.help_text'),
47
+ )
48
+
49
+ position = models.PositiveSmallIntegerField(
50
+ # Note: Will be set in admin via adminsortable2
51
+ # The JavaScript which performs the sorting is 1-indexed !
52
+ default=0,
53
+ blank=False,
54
+ null=False,
55
+ )
56
+
57
+ def update_response_info(self):
58
+ if self.name:
59
+ logger.debug('Skip link request: because we have a name: %r', self.name)
60
+ return
61
+
62
+ if self.last_check:
63
+ delta = timezone.now() - self.last_check
64
+ logger.debug('Last check is %s ago.', delta)
65
+ if delta < datetime.timedelta(minutes=1):
66
+ logger.info('Skip request for: %r', self.url)
67
+ return
68
+
69
+ try:
70
+ r = requests.get(url=self.url, allow_redirects=True, timeout=10)
71
+ except Exception as err:
72
+ logger.exception('Error get %s: %s', self.url, err)
73
+ self.status_code = None
74
+ self.page_title = None
75
+ return
76
+
77
+ logger.debug('%r: %r', self.url, r.headers)
78
+
79
+ self.last_check = timezone.now()
80
+ self.status_code = r.status_code
81
+
82
+ if r.status_code == 200:
83
+ titles = re.findall(r'<title>(.+?)</title>', r.text)
84
+ if not titles:
85
+ logger.warning('No title found in %r', self.url)
86
+ else:
87
+ title = titles[0]
88
+ logger.info('Found title: %r', title)
89
+
90
+ self.page_title = striptags(title) # TODO: remove with a better clean method!
91
+ if not self.name:
92
+ logger.debug('set name to: %r', self.page_title)
93
+ self.name = self.page_title
94
+
95
+ def full_clean(self, **kwargs):
96
+ if self.url is not None:
97
+ self.update_response_info()
98
+ return super().full_clean(**kwargs)
99
+
100
+ def __str__(self):
101
+ return self.url
102
+
103
+ class Meta:
104
+ abstract = True
@@ -0,0 +1,24 @@
1
+ from ckeditor_uploader.fields import RichTextUploadingField
2
+ from django.utils.translation import gettext_lazy as _
3
+ from django_tools.model_version_protect.models import VersionProtectBaseModel
4
+
5
+ from inventory.models.base import BaseParentTreeModel
6
+
7
+
8
+ class LocationModel(BaseParentTreeModel, VersionProtectBaseModel):
9
+ """
10
+ A Storage for items.
11
+ """
12
+
13
+ description = RichTextUploadingField(
14
+ blank=True,
15
+ null=True,
16
+ config_name='LocationModel.description',
17
+ verbose_name=_('LocationModel.description.verbose_name'),
18
+ help_text=_('LocationModel.description.help_text'),
19
+ )
20
+
21
+ class Meta:
22
+ ordering = ('path_str',)
23
+ verbose_name = _('LocationModel.verbose_name')
24
+ verbose_name_plural = _('LocationModel.verbose_name_plural')