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,169 @@
1
+ import logging
2
+
3
+ import tagulous
4
+ from adminsortable2.admin import SortableAdminMixin, SortableInlineAdminMixin
5
+ from django.conf import settings
6
+ from django.contrib import admin
7
+ from django.template.loader import render_to_string
8
+ from django.urls import reverse
9
+ from django.utils.html import format_html
10
+ from django.utils.translation import gettext_lazy as _
11
+ from import_export.admin import ImportExportMixin
12
+ from import_export.resources import ModelResource
13
+
14
+ from inventory.admin.base import (
15
+ BaseFileModelInline,
16
+ BaseImageModelInline,
17
+ BaseUserAdmin,
18
+ LimitTreeDepthListFilter,
19
+ UserInlineMixin,
20
+ )
21
+ from inventory.admin.tagulous_fix import TagulousModelAdminFix
22
+ from inventory.models import ItemLinkModel, ItemModel
23
+ from inventory.models.item import ItemFileModel, ItemImageModel
24
+ from inventory.string_utils import ltruncatechars
25
+
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class ItemLinkModelInline(UserInlineMixin, SortableInlineAdminMixin, admin.TabularInline):
31
+ model = ItemLinkModel
32
+ extra = 0
33
+
34
+
35
+ class ItemImageModelInline(BaseImageModelInline):
36
+ model = ItemImageModel
37
+
38
+
39
+ class ItemFileModelInline(BaseFileModelInline):
40
+ model = ItemFileModel
41
+
42
+
43
+ class ItemModelResource(ModelResource):
44
+ class Meta:
45
+ model = ItemModel
46
+
47
+
48
+ @admin.register(ItemModel)
49
+ class ItemModelAdmin(TagulousModelAdminFix, ImportExportMixin, SortableAdminMixin, BaseUserAdmin):
50
+ @admin.display(description=_('Related items'))
51
+ def related_items(self, obj):
52
+ if obj.pk is None:
53
+ # Add a new item -> there are no related items ;)
54
+ return '-'
55
+
56
+ related_qs = ItemModel.tree_objects.related_objects(instance=obj)
57
+ context = {
58
+ 'items': related_qs,
59
+ 'opts': self.opts,
60
+ }
61
+ return render_to_string('admin/item/related_items.html', context)
62
+
63
+ @admin.display(ordering='path_str', description=_('ItemModel.verbose_name'))
64
+ def item(self, obj):
65
+ path = obj.path
66
+ if len(path) > 1:
67
+ prefixes = ' › '.join(path[:-1] + [''])
68
+ prefixes = ltruncatechars(prefixes, max_length=settings.TREE_PATH_STR_MAX_LENGTH)
69
+ else:
70
+ prefixes = ''
71
+ item = path[-1]
72
+ url = reverse('admin:inventory_itemmodel_change', args=[obj.pk])
73
+ return format_html(
74
+ '<a href="{}">{}<strong>{}</strong></a>',
75
+ url,
76
+ prefixes,
77
+ item,
78
+ )
79
+
80
+ def get_queryset(self, request):
81
+ qs = super().get_queryset(request)
82
+ qs = qs.prefetch_related(
83
+ 'location',
84
+ 'kind',
85
+ 'producer',
86
+ )
87
+ return qs
88
+
89
+ def get_max_order(self, request, obj=None):
90
+ # Work-a-round for: https://github.com/jrief/django-admin-sortable2/issues/341
91
+ return 0
92
+
93
+ date_hierarchy = 'create_dt'
94
+ list_display = ('producer', 'item', 'kind', 'location', 'received_date', 'update_dt')
95
+ ordering = ('path_str',)
96
+ list_display_links = ()
97
+ list_filter = (LimitTreeDepthListFilter, 'kind', 'location', 'producer', 'tags')
98
+ search_fields = ('name', 'description', 'kind__name', 'tags__name')
99
+ fieldsets = (
100
+ (
101
+ _('Internals'),
102
+ {
103
+ 'classes': ('collapse',),
104
+ 'fields': (
105
+ ('id', 'version'),
106
+ 'user',
107
+ ),
108
+ },
109
+ ),
110
+ (_('Meta'), {'classes': ('collapse',), 'fields': ('create_dt', 'update_dt')}),
111
+ (
112
+ _('Basic'),
113
+ {
114
+ 'fields': (
115
+ 'kind',
116
+ ('producer', 'name'),
117
+ 'description',
118
+ 'tags',
119
+ 'fcc_id',
120
+ 'parent',
121
+ 'location',
122
+ )
123
+ },
124
+ ),
125
+ (_('Related items'), {'classes': ('collapse',), 'fields': ('related_items',)}),
126
+ (
127
+ _('Lent'),
128
+ {
129
+ 'classes': ('collapse',),
130
+ 'fields': (
131
+ 'lent_to',
132
+ (
133
+ 'lent_from_date',
134
+ 'lent_until_date',
135
+ ),
136
+ ),
137
+ },
138
+ ),
139
+ (
140
+ _('Received'),
141
+ {
142
+ 'classes': ('collapse',),
143
+ 'fields': (('received_from', 'received_date', 'received_price'),),
144
+ },
145
+ ),
146
+ (
147
+ _('Handed over'),
148
+ {
149
+ 'classes': ('collapse',),
150
+ 'fields': (('handed_over_to', 'handed_over_date', 'handed_over_price'),),
151
+ },
152
+ ),
153
+ )
154
+ autocomplete_fields = ('parent', 'location')
155
+ readonly_fields = ('id', 'create_dt', 'update_dt', 'user', 'related_items')
156
+ inlines = (ItemImageModelInline, ItemFileModelInline, ItemLinkModelInline)
157
+
158
+ def get_list_display(self, request):
159
+ list_display = list(super().get_list_display(request))
160
+
161
+ # FIXME: SortableAdminMixin.get_list_display() adds this, we didn't need here:
162
+ # See: https://github.com/jrief/django-admin-sortable2/issues/363
163
+ if '_reorder_' in list_display:
164
+ list_display.remove('_reorder_')
165
+
166
+ return list_display
167
+
168
+
169
+ tagulous.admin.enhance(ItemModel, ItemModelAdmin)
@@ -0,0 +1,78 @@
1
+ from django.conf import settings
2
+ from django.contrib import admin
3
+ from django.db.models import Count
4
+ from django.db.models.options import Options
5
+ from django.template.loader import render_to_string
6
+ from django.utils.translation import gettext_lazy as _
7
+ from import_export.admin import ImportExportMixin
8
+ from import_export.resources import ModelResource
9
+
10
+ from inventory.admin.base import BaseUserAdmin, LimitTreeDepthListFilter
11
+ from inventory.models import ItemModel, LocationModel
12
+ from inventory.string_utils import ltruncatechars
13
+
14
+
15
+ class LocationModelResource(ModelResource):
16
+ class Meta:
17
+ model = LocationModel
18
+
19
+
20
+ @admin.register(LocationModel)
21
+ class LocationModelAdmin(ImportExportMixin, BaseUserAdmin):
22
+ @admin.display(ordering='item_count', description=_('ItemModel.verbose_name_plural'))
23
+ def item_count(self, obj):
24
+ return obj.item_count
25
+
26
+ @admin.display(description=_('ItemModel.verbose_name_plural'))
27
+ def items(self, obj):
28
+ item_qs = ItemModel.objects.filter(location=obj)
29
+ opts: Options = ItemModel._meta
30
+ context = {
31
+ 'items': item_qs,
32
+ 'opts': opts,
33
+ }
34
+ return render_to_string('admin/location/items.html', context)
35
+
36
+ @admin.display(ordering='path_str', description=_('LocationModel.verbose_name'))
37
+ def location(self, obj):
38
+ text = ' › '.join(obj.path)
39
+ text = ltruncatechars(text, max_length=settings.TREE_PATH_STR_MAX_LENGTH)
40
+ return text
41
+
42
+ def get_queryset(self, request):
43
+ qs = super().get_queryset(request)
44
+ qs = qs.annotate(item_count=Count('items'))
45
+ return qs
46
+
47
+ list_display = ('location', 'create_dt', 'update_dt', 'item_count')
48
+ fieldsets = (
49
+ (
50
+ _('Internals'),
51
+ {
52
+ 'classes': ('collapse',),
53
+ 'fields': (
54
+ ('id', 'version'),
55
+ 'user',
56
+ ),
57
+ },
58
+ ),
59
+ (_('Meta'), {'classes': ('collapse',), 'fields': ('create_dt', 'update_dt')}),
60
+ (
61
+ _('Basic'),
62
+ {
63
+ 'fields': (
64
+ 'name',
65
+ 'description',
66
+ 'tags',
67
+ 'parent',
68
+ )
69
+ },
70
+ ),
71
+ (_('Items in this Location'), {'fields': ('items',)}),
72
+ )
73
+ autocomplete_fields = ('parent',)
74
+ readonly_fields = ('id', 'create_dt', 'update_dt', 'user', 'item_count', 'items')
75
+ list_display_links = ('location',)
76
+ list_filter = (LimitTreeDepthListFilter,)
77
+ search_fields = ('name', 'description', 'tags__name')
78
+ ordering = ('path_str',)
@@ -0,0 +1,76 @@
1
+ import logging
2
+
3
+ import tagulous
4
+ from adminsortable2.admin import SortableAdminMixin, SortableInlineAdminMixin
5
+ from django.contrib import admin
6
+ from django.utils.translation import gettext_lazy as _
7
+ from import_export.admin import ImportExportMixin
8
+ from import_export.resources import ModelResource
9
+
10
+ from inventory.admin.base import BaseFileModelInline, BaseImageModelInline, BaseUserAdmin, UserInlineMixin
11
+ from inventory.admin.tagulous_fix import TagulousModelAdminFix
12
+ from inventory.models import MemoLinkModel, MemoModel
13
+ from inventory.models.memo import MemoFileModel, MemoImageModel
14
+
15
+
16
+ logger = logging.getLogger(__name__)
17
+
18
+
19
+ class MemoLinkModelInline(UserInlineMixin, SortableInlineAdminMixin, admin.TabularInline):
20
+ model = MemoLinkModel
21
+ extra = 0
22
+
23
+
24
+ class MemoImageModelInline(BaseImageModelInline):
25
+ model = MemoImageModel
26
+
27
+
28
+ class MemoFileModelInline(BaseFileModelInline):
29
+ model = MemoFileModel
30
+
31
+
32
+ class MemoModelResource(ModelResource):
33
+ class Meta:
34
+ model = MemoModel
35
+
36
+
37
+ @admin.register(MemoModel)
38
+ class MemoModelAdmin(TagulousModelAdminFix, ImportExportMixin, SortableAdminMixin, BaseUserAdmin):
39
+ def get_max_order(self, request, obj=None):
40
+ # Work-a-round for: https://github.com/jrief/django-admin-sortable2/issues/341
41
+ return 0
42
+
43
+ date_hierarchy = 'create_dt'
44
+ list_display = ('name', 'update_dt')
45
+ ordering = ('-update_dt',)
46
+ list_display_links = ('name',)
47
+ list_filter = ('tags',)
48
+ search_fields = ('name', 'memo', 'tags__name')
49
+ fieldsets = (
50
+ (
51
+ _('Internals'),
52
+ {
53
+ 'classes': ('collapse',),
54
+ 'fields': (
55
+ ('id', 'version'),
56
+ 'user',
57
+ ),
58
+ },
59
+ ),
60
+ (_('Meta'), {'classes': ('collapse',), 'fields': ('create_dt', 'update_dt')}),
61
+ (
62
+ _('Basic'),
63
+ {
64
+ 'fields': (
65
+ 'name',
66
+ 'memo',
67
+ 'tags',
68
+ )
69
+ },
70
+ ),
71
+ )
72
+ readonly_fields = ('id', 'create_dt', 'update_dt', 'user')
73
+ inlines = (MemoImageModelInline, MemoFileModelInline, MemoLinkModelInline)
74
+
75
+
76
+ tagulous.admin.enhance(MemoModel, MemoModelAdmin)
@@ -0,0 +1,45 @@
1
+ """
2
+ Work-a-round for:
3
+ https://github.com/radiac/django-tagulous/issues/164
4
+ """
5
+ from django import forms
6
+ from django.contrib.admin.widgets import AutocompleteMixin
7
+ from tagulous import settings as tagulous_settings
8
+ from tagulous.forms import AdminTagWidget, BaseTagField
9
+ from tagulous.models import SingleTagField, TagField
10
+
11
+
12
+ class AdminTagWidget2(AdminTagWidget):
13
+ @property
14
+ def media(self):
15
+ # Get the media from the AutocompleteMixin - this will give us Django's
16
+ # vendor jQuery and select2
17
+ class GetMedia(AutocompleteMixin, forms.Select):
18
+ pass
19
+
20
+ dependency_media = GetMedia(None, None).media
21
+ tagulous_media = forms.Media(
22
+ js=tagulous_settings.ADMIN_AUTOCOMPLETE_JS,
23
+ css=tagulous_settings.ADMIN_AUTOCOMPLETE_CSS,
24
+ )
25
+ all_media = dependency_media + tagulous_media
26
+
27
+ return all_media
28
+
29
+
30
+ class BaseTagField2(BaseTagField):
31
+ widget = AdminTagWidget2
32
+
33
+
34
+ class TagulousModelAdminFix:
35
+ def __init__(self, *args, **kwargs):
36
+ super().__init__(*args, **kwargs)
37
+
38
+ self.formfield_overrides[SingleTagField] = {
39
+ 'form_class': BaseTagField2,
40
+ 'widget': AdminTagWidget2,
41
+ }
42
+ self.formfield_overrides[TagField] = {
43
+ 'form_class': BaseTagField2,
44
+ 'widget': AdminTagWidget2,
45
+ }
inventory/apps.py ADDED
@@ -0,0 +1,18 @@
1
+ """
2
+ https://docs.djangoproject.com/en/2.0/ref/applications/#configuring-applications-ref
3
+
4
+ created 14.20.2020 by Jens Diemer <opensource@jensdiemer.de>
5
+ :copyleft: 2020 by the PyInventory team, see AUTHORS for more details.
6
+ :license: GNU GPL v3 or above, see LICENSE for more details.
7
+ """
8
+
9
+
10
+ from django.apps import AppConfig
11
+
12
+
13
+ class InventoryConfig(AppConfig):
14
+ name = "inventory"
15
+ verbose_name = "Inventory"
16
+
17
+ def ready(self):
18
+ import inventory.signals # noqa
@@ -0,0 +1,15 @@
1
+ import logging
2
+
3
+ from bx_django_utils.filename import clean_filename
4
+ from django.utils.crypto import get_random_string
5
+
6
+
7
+ logger = logging.getLogger(__name__)
8
+
9
+
10
+ def get_filename(filename, request):
11
+ random_string = get_random_string()
12
+ filename = clean_filename(filename)
13
+ filename = f'{random_string}/{filename}'
14
+ logger.info(f'Upload filename: {filename!r}')
15
+ return filename
@@ -0,0 +1,5 @@
1
+ from inventory import __version__
2
+
3
+
4
+ def inventory_version_string(request):
5
+ return {'inventory_version_string': f'v{__version__}'}
inventory/forms.py ADDED
@@ -0,0 +1,36 @@
1
+ from django import forms
2
+ from django.core.exceptions import FieldDoesNotExist
3
+
4
+ from inventory.request_dict import get_request_dict
5
+
6
+
7
+ class OnlyUserRelationsModelForm(forms.ModelForm):
8
+ def __init__(self, *args, **kwargs):
9
+ super().__init__(*args, **kwargs)
10
+
11
+ # Filter all related fields that has a "user" attribute for the current user
12
+ # e.g.:
13
+ # The user should only select his own "location" and "items"
14
+
15
+ user = get_request_dict()['user'] # get current user via threading.local()
16
+ for formfield in self.fields.values():
17
+ if not hasattr(formfield, 'queryset'):
18
+ continue
19
+
20
+ queryset = formfield.queryset
21
+ opts = queryset.model._meta
22
+ try:
23
+ opts.get_field('user')
24
+ except FieldDoesNotExist:
25
+ continue
26
+
27
+ formfield.queryset = queryset.filter(user=user)
28
+
29
+ def save(self, commit=True):
30
+ instance = super().save(commit=False)
31
+ if instance.user_id is None:
32
+ user = get_request_dict()['user'] # get current user via threading.local()
33
+ instance.user_id = user.pk
34
+
35
+ instance.save()
36
+ return instance