django-spire 0.25.2__py3-none-any.whl → 0.26.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.
- django_spire/auth/templates/django_spire/auth/element/android_and_chrome_app_install_element.html +33 -0
- django_spire/auth/templates/django_spire/auth/element/ios_app_install_element.html +48 -0
- django_spire/auth/templates/django_spire/auth/page/auth_page.html +2 -1
- django_spire/auth/templates/django_spire/auth/page/login_page.html +2 -0
- django_spire/comment/mixins.py +3 -3
- django_spire/comment/templates/django_spire/comment/card/comment_list_card.html +8 -5
- django_spire/comment/templates/django_spire/comment/form/comment_form.html +3 -3
- django_spire/comment/templates/django_spire/comment/form/content/comment_form_content.html +1 -0
- django_spire/comment/templates/django_spire/comment/item/comment_item_ellipsis.html +12 -8
- django_spire/comment/views.py +8 -8
- django_spire/consts.py +1 -1
- django_spire/contrib/form/utils.py +3 -3
- django_spire/contrib/queryset/filter_tools.py +56 -14
- django_spire/contrib/queryset/mixins.py +24 -3
- django_spire/contrib/service/django_model_service.py +5 -6
- django_spire/core/management/commands/spire_startapp.py +42 -25
- django_spire/core/management/commands/spire_startapp_pkg/exceptions.py +5 -0
- django_spire/core/management/commands/spire_startapp_pkg/maps.py +64 -32
- django_spire/core/management/commands/spire_startapp_pkg/template/app/constants.py.template +1 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/forms.py.template +4 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/models.py.template +2 -1
- django_spire/core/management/commands/spire_startapp_pkg/template/app/querysets.py.template +15 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py.template +1 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py.template +6 -6
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py.template +2 -2
- django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/template_urls.py.template +12 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/app/views/form_views.py.template +10 -11
- django_spire/core/management/commands/spire_startapp_pkg/template/app/views/page_views.py.template +17 -3
- django_spire/core/management/commands/spire_startapp_pkg/template/app/views/template_views.py.template +40 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${form_card_template_name}.html.template +1 -1
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_base_card_template_name}.html.template +16 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_items_card_template_name}.html.template +16 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_table_card_template_name}.html.template +16 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/container/${list_container_template_name}.html.template +1 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/${list_filter_form_template_name}.html.template +30 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/${item_template_name}.html.template +32 -20
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/${list_items_template_name}.html.template +3 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${detail_page_template_name}.html.template +3 -3
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${form_page_template_name}.html.template +2 -2
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${list_page_template_name}.html.template +2 -2
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/table/${table_row_template_name}.html.template +6 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/table/${table_rows_template_name}.html.template +3 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/table/${table_template_name}.html.template +6 -0
- django_spire/core/management/commands/spire_startapp_pkg/user_input.py +82 -9
- django_spire/core/management/commands/spire_startapp_pkg/validator.py +19 -6
- django_spire/core/querysets.py +19 -0
- django_spire/core/templates/django_spire/badge/base_badge.html +2 -3
- django_spire/core/templates/django_spire/base/base.html +1 -0
- django_spire/core/templates/django_spire/button/base_button.html +2 -1
- django_spire/core/templates/django_spire/card/title_card.html +13 -10
- django_spire/core/templates/django_spire/container/container.html +1 -1
- django_spire/core/templates/django_spire/filtering/form/base_session_filter_form.html +1 -1
- django_spire/core/templates/django_spire/form/field/_base_file_field.html +216 -0
- django_spire/core/templates/django_spire/form/field/_multi_checkbox_field.html +52 -0
- django_spire/core/templates/django_spire/form/field/base_field.html +128 -0
- django_spire/core/templates/django_spire/form/field/char_field.html +1 -0
- django_spire/core/templates/django_spire/form/field/color_field.html +1 -0
- django_spire/core/templates/django_spire/form/field/date_field.html +1 -0
- django_spire/core/templates/django_spire/form/field/datetime_field.html +1 -0
- django_spire/core/templates/django_spire/form/field/decimal_field.html +1 -0
- django_spire/core/templates/django_spire/form/field/element/select_checkmark_element.html +4 -0
- django_spire/core/templates/django_spire/form/field/element/select_down_arrow_element.html +5 -0
- django_spire/core/templates/django_spire/form/field/email_field.html +1 -0
- django_spire/core/templates/django_spire/form/field/input_field.html +13 -0
- django_spire/core/templates/django_spire/form/field/item/select_choice_item.html +15 -0
- django_spire/core/templates/django_spire/form/field/item/selected_choice_item.html +5 -0
- django_spire/core/templates/django_spire/form/field/list_field.html +112 -0
- django_spire/core/templates/django_spire/form/field/multi_file_field.html +90 -0
- django_spire/core/templates/django_spire/form/field/multi_select_field.html +155 -0
- django_spire/core/templates/django_spire/form/field/number_field.html +11 -0
- django_spire/core/templates/django_spire/form/field/password_field.html +1 -0
- django_spire/core/templates/django_spire/form/field/radio_field.html +24 -0
- django_spire/core/templates/django_spire/form/field/range_field.html +1 -0
- django_spire/core/templates/django_spire/form/field/search_and_select_field.html +119 -0
- django_spire/core/templates/django_spire/form/field/search_field.html +5 -0
- django_spire/core/templates/django_spire/form/field/select_field.html +78 -0
- django_spire/core/templates/django_spire/form/field/single_checkbox_field.html +27 -0
- django_spire/core/templates/django_spire/form/field/single_file_field.html +90 -0
- django_spire/core/templates/django_spire/form/field/telephone_field.html +1 -0
- django_spire/core/templates/django_spire/form/field/text_field.html +11 -0
- django_spire/core/templates/django_spire/form/field/time_field.html +1 -0
- django_spire/core/templates/django_spire/infinite_scroll/base.html +2 -1
- django_spire/core/templatetags/model_tags.py +34 -0
- django_spire/metric/report/tools.py +0 -2
- {django_spire-0.25.2.dist-info → django_spire-0.26.0.dist-info}/METADATA +1 -1
- {django_spire-0.25.2.dist-info → django_spire-0.26.0.dist-info}/RECORD +89 -45
- django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_intelligence/__init__.py.template +0 -0
- django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_card_template_name}.html.template +0 -18
- {django_spire-0.25.2.dist-info → django_spire-0.26.0.dist-info}/WHEEL +0 -0
- {django_spire-0.25.2.dist-info → django_spire-0.26.0.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.25.2.dist-info → django_spire-0.26.0.dist-info}/top_level.txt +0 -0
|
@@ -2,10 +2,13 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import re
|
|
4
4
|
|
|
5
|
-
from typing import TYPE_CHECKING
|
|
5
|
+
from typing import TYPE_CHECKING, Tuple
|
|
6
6
|
|
|
7
7
|
from django.core.management.base import CommandError
|
|
8
8
|
|
|
9
|
+
from django_spire.core.management.commands.spire_startapp_pkg.exceptions import \
|
|
10
|
+
AppExistsError
|
|
11
|
+
|
|
9
12
|
if TYPE_CHECKING:
|
|
10
13
|
from django_spire.core.management.commands.spire_startapp_pkg.reporter import Reporter
|
|
11
14
|
from django_spire.core.management.commands.spire_startapp_pkg.validator import AppValidator
|
|
@@ -30,6 +33,8 @@ class UserInputCollector:
|
|
|
30
33
|
self.reporter = reporter
|
|
31
34
|
self.validator = validator
|
|
32
35
|
|
|
36
|
+
self.skip_app_creation = False
|
|
37
|
+
|
|
33
38
|
def collect_all_inputs(self) -> dict[str, str]:
|
|
34
39
|
"""
|
|
35
40
|
Collects all required user inputs for app creation.
|
|
@@ -52,13 +57,19 @@ class UserInputCollector:
|
|
|
52
57
|
db_table_name = self._collect_db_table_name(components, app_name) # Changed from app_label to components, app_name
|
|
53
58
|
model_permission_path = self._collect_model_permission_path(app_path, model_name)
|
|
54
59
|
|
|
55
|
-
|
|
60
|
+
if not self.skip_app_creation:
|
|
61
|
+
permission_data = self._collect_permission_inheritance(components)
|
|
62
|
+
else:
|
|
63
|
+
permission_data = {}
|
|
64
|
+
|
|
65
|
+
template_options = self._collect_template_generation_options(components)
|
|
56
66
|
|
|
57
67
|
verbose_name, verbose_name_plural = self._derive_verbose_names(model_name, model_name_plural)
|
|
58
68
|
|
|
59
69
|
return {
|
|
60
70
|
'app_path': app_path,
|
|
61
71
|
'app_name': app_name,
|
|
72
|
+
'skip_app_generation': self.skip_app_creation,
|
|
62
73
|
'model_name': model_name,
|
|
63
74
|
'model_name_plural': model_name_plural,
|
|
64
75
|
'app_label': app_label,
|
|
@@ -67,6 +78,7 @@ class UserInputCollector:
|
|
|
67
78
|
'verbose_name': verbose_name,
|
|
68
79
|
'verbose_name_plural': verbose_name_plural,
|
|
69
80
|
**permission_data,
|
|
81
|
+
**template_options
|
|
70
82
|
}
|
|
71
83
|
|
|
72
84
|
def _collect_app_label(self, components: list[str], app_name: str) -> str:
|
|
@@ -79,8 +91,12 @@ class UserInputCollector:
|
|
|
79
91
|
"""
|
|
80
92
|
|
|
81
93
|
immediate_parent = components[-2] if len(components) > 2 else None
|
|
94
|
+
|
|
82
95
|
default = immediate_parent.lower() + '_' + app_name.lower() if immediate_parent else app_name.lower()
|
|
83
|
-
|
|
96
|
+
if self.skip_app_creation:
|
|
97
|
+
return default
|
|
98
|
+
else:
|
|
99
|
+
return self._collect_input('Enter the app label', default, '3/8')
|
|
84
100
|
|
|
85
101
|
def _collect_app_name(self, components: list[str]) -> str:
|
|
86
102
|
"""
|
|
@@ -91,7 +107,10 @@ class UserInputCollector:
|
|
|
91
107
|
"""
|
|
92
108
|
|
|
93
109
|
default = components[-1]
|
|
94
|
-
|
|
110
|
+
if self.skip_app_creation:
|
|
111
|
+
return default
|
|
112
|
+
else:
|
|
113
|
+
return self._collect_input('Enter the app name', default, '2/8')
|
|
95
114
|
|
|
96
115
|
def _collect_app_path(self) -> str:
|
|
97
116
|
"""
|
|
@@ -110,7 +129,12 @@ class UserInputCollector:
|
|
|
110
129
|
raise CommandError(message)
|
|
111
130
|
|
|
112
131
|
components = app_path.split('.')
|
|
113
|
-
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
self.validator.validate_app_path(components)
|
|
135
|
+
except AppExistsError as exc:
|
|
136
|
+
self.reporter.write('\nSkipping app creation.', self.reporter.style_notice)
|
|
137
|
+
self.skip_app_creation = True
|
|
114
138
|
|
|
115
139
|
return app_path
|
|
116
140
|
|
|
@@ -125,7 +149,10 @@ class UserInputCollector:
|
|
|
125
149
|
|
|
126
150
|
parent_parts = components[1:-1] if len(components) > 1 else []
|
|
127
151
|
default = '_'.join(parent_parts).lower() + '_' + app_name.lower() if parent_parts else app_name.lower()
|
|
128
|
-
|
|
152
|
+
if self.skip_app_creation:
|
|
153
|
+
return default
|
|
154
|
+
else:
|
|
155
|
+
return self._collect_input('Enter the database table name', default, '6/8')
|
|
129
156
|
|
|
130
157
|
def _collect_input(self, prompt: str, default: str, step_number: str) -> str:
|
|
131
158
|
"""
|
|
@@ -150,7 +177,10 @@ class UserInputCollector:
|
|
|
150
177
|
"""
|
|
151
178
|
|
|
152
179
|
default = ''.join(word.title() for word in app_name.split('_'))
|
|
153
|
-
|
|
180
|
+
if self.skip_app_creation:
|
|
181
|
+
return default
|
|
182
|
+
else:
|
|
183
|
+
return self._collect_input('Enter the model name', default, '4/8')
|
|
154
184
|
|
|
155
185
|
def _collect_model_name_plural(self, model_name: str) -> str:
|
|
156
186
|
"""
|
|
@@ -161,7 +191,10 @@ class UserInputCollector:
|
|
|
161
191
|
"""
|
|
162
192
|
|
|
163
193
|
default = model_name + 's'
|
|
164
|
-
|
|
194
|
+
if self.skip_app_creation:
|
|
195
|
+
return default
|
|
196
|
+
else:
|
|
197
|
+
return self._collect_input('Enter the model name plural', default, '5/8')
|
|
165
198
|
|
|
166
199
|
def _collect_model_permission_path(self, app_path: str, model_name: str) -> str:
|
|
167
200
|
"""
|
|
@@ -173,7 +206,11 @@ class UserInputCollector:
|
|
|
173
206
|
"""
|
|
174
207
|
|
|
175
208
|
default = f'{app_path}.models.{model_name}'
|
|
176
|
-
|
|
209
|
+
if self.skip_app_creation:
|
|
210
|
+
return default
|
|
211
|
+
else:
|
|
212
|
+
return self._collect_input('Enter the model permission path', default,
|
|
213
|
+
'7/8')
|
|
177
214
|
|
|
178
215
|
def _collect_permission_inheritance(self, components: list[str]) -> dict[str, str]:
|
|
179
216
|
"""
|
|
@@ -253,3 +290,39 @@ class UserInputCollector:
|
|
|
253
290
|
self.reporter.write('\n[8/8]: Do you want this app to inherit permissions from its parent? (y/n)', self.reporter.style_notice)
|
|
254
291
|
user_input = input('Default is "n": ').strip().lower()
|
|
255
292
|
return user_input == 'y'
|
|
293
|
+
|
|
294
|
+
def _collect_template_generation_options(self, components):
|
|
295
|
+
user_input = {}
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
self.validator.validate_template_path(components)
|
|
299
|
+
except AppExistsError:
|
|
300
|
+
user_input['skip_template_generation'] = True
|
|
301
|
+
return user_input
|
|
302
|
+
|
|
303
|
+
self.reporter.write('\n[Template Creation Wizard]\n\n',
|
|
304
|
+
self.reporter.style_success)
|
|
305
|
+
|
|
306
|
+
user_input[
|
|
307
|
+
'skip_template_generation'] = self._collect_skip_template_generation()
|
|
308
|
+
|
|
309
|
+
if not user_input['skip_template_generation']:
|
|
310
|
+
user_input['list_display_type'] = self._collect_input(
|
|
311
|
+
'How do you want to display items on the list page? (items/table)',
|
|
312
|
+
'items',
|
|
313
|
+
'2/2')
|
|
314
|
+
|
|
315
|
+
return user_input
|
|
316
|
+
|
|
317
|
+
def _collect_skip_template_generation(self):
|
|
318
|
+
"""
|
|
319
|
+
Prompts the user to generate templates.
|
|
320
|
+
|
|
321
|
+
:return: True if user wants to skip template generation, False otherwise.
|
|
322
|
+
"""
|
|
323
|
+
|
|
324
|
+
user_input = self._collect_input(
|
|
325
|
+
'Do you want to generate templates for this app? (y/n)', 'n',
|
|
326
|
+
'1/2')
|
|
327
|
+
|
|
328
|
+
return user_input == 'n'
|
|
@@ -4,6 +4,9 @@ from typing import TYPE_CHECKING
|
|
|
4
4
|
|
|
5
5
|
from django.core.management.base import CommandError
|
|
6
6
|
|
|
7
|
+
from django_spire.core.management.commands.spire_startapp_pkg.exceptions import \
|
|
8
|
+
AppExistsError
|
|
9
|
+
|
|
7
10
|
if TYPE_CHECKING:
|
|
8
11
|
from django_spire.core.management.commands.spire_startapp_pkg.filesystem import FileSystem
|
|
9
12
|
from django_spire.core.management.commands.spire_startapp_pkg.registry import AppRegistry
|
|
@@ -65,14 +68,24 @@ class AppValidator:
|
|
|
65
68
|
destination = self._path_resolver.get_app_destination(components)
|
|
66
69
|
|
|
67
70
|
if self._filesystem.has_content(destination):
|
|
68
|
-
|
|
71
|
+
message = f'The app already exists at {destination}.'
|
|
72
|
+
self._reporter.write(f'\n{message}', self._reporter.style_notice)
|
|
73
|
+
raise AppExistsError(message)
|
|
69
74
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
)
|
|
75
|
+
def validate_template_path(self, components: list[str]) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Validates that an app's template path doesn't already exist.
|
|
74
78
|
|
|
75
|
-
|
|
79
|
+
:param components: List of app path components.
|
|
80
|
+
:raises CommandError: If an app already exists at the destination path.
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
destination = self._path_resolver.get_template_destination(components)
|
|
84
|
+
|
|
85
|
+
if self._filesystem.has_content(destination):
|
|
86
|
+
message = f'App templates already exists at {destination}.'
|
|
87
|
+
self._reporter.write(f'\n{message}', self._reporter.style_notice)
|
|
88
|
+
raise AppExistsError(message)
|
|
76
89
|
|
|
77
90
|
def validate_root_app(self, components: list[str]) -> None:
|
|
78
91
|
"""
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from django.db.models import QuerySet, Q
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class SearchQuerySetMixin:
|
|
5
|
+
def search(self, search_value: str) -> QuerySet:
|
|
6
|
+
words = search_value.split(' ')
|
|
7
|
+
|
|
8
|
+
filtered_query = self
|
|
9
|
+
|
|
10
|
+
char_fields = [
|
|
11
|
+
field.name for field in self.model._meta.fields
|
|
12
|
+
if field.get_internal_type() == 'CharField'
|
|
13
|
+
]
|
|
14
|
+
|
|
15
|
+
for word in words:
|
|
16
|
+
or_conditions = Q()
|
|
17
|
+
for field in char_fields:
|
|
18
|
+
or_conditions |= Q(**{f"{field}__icontains": word})
|
|
19
|
+
filtered_query = filtered_query.filter(or_conditions)
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
path: '{{ DJANGO_SPIRE_THEME_PATH|escapejs }}'
|
|
43
43
|
};
|
|
44
44
|
</script>
|
|
45
|
+
<link rel="manifest" href="{% static 'django_spire/favicons/manifest.json' %}">
|
|
45
46
|
|
|
46
47
|
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
|
|
47
48
|
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.x/dist/echarts.min.js"></script>
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
<a
|
|
2
2
|
class="btn shadow-sm {% block button_class %}{% endblock %} {{ button_class|default:'btn-sm' }}"
|
|
3
3
|
{% block button_attributes %}{% endblock %}
|
|
4
|
+
{{ button_attributes }}
|
|
4
5
|
{% if button_href %}href="{{ button_href }}{{ button_url_params }}"{% endif %}
|
|
5
|
-
{% if x_button_click %}@click="{{ x_button_click }}"{% endif %}
|
|
6
|
+
{% if x_button_click or button_modal_href %}@click="{% if button_modal_href %}dispatch_modal_view('{{ button_modal_href }}'){% else %}{{ x_button_click }}{% endif %}"{% endif %}
|
|
6
7
|
>
|
|
7
8
|
{% if button_icon %}
|
|
8
9
|
<i class="{{ button_icon }}"></i>
|
|
@@ -7,7 +7,8 @@
|
|
|
7
7
|
{% block card_body_style %}display: flex; flex-direction: column; min-height: 0;{% endblock %}
|
|
8
8
|
|
|
9
9
|
{% block card_content %}
|
|
10
|
-
<div
|
|
10
|
+
<{% block card_title_content_container_element_type %}div{% endblock %}
|
|
11
|
+
{% block card_title_content_container_attributes %}{% endblock %}
|
|
11
12
|
class="d-flex flex-column flex-grow-1"
|
|
12
13
|
style="min-height: 0;"
|
|
13
14
|
x-data="{
|
|
@@ -17,17 +18,19 @@
|
|
|
17
18
|
}
|
|
18
19
|
}"
|
|
19
20
|
>
|
|
20
|
-
|
|
21
|
-
<div class="
|
|
22
|
-
<div class="
|
|
23
|
-
|
|
24
|
-
|
|
21
|
+
{% block card_title_header_content %}
|
|
22
|
+
<div class="row justify-content-between align-items-center mb-2 pb-2 border-bottom flex-shrink-0" style="min-height: 38px;">
|
|
23
|
+
<div class="col d-flex align-items-center">
|
|
24
|
+
<div class="card-title text-uppercase mb-0">
|
|
25
|
+
{% block card_title %}
|
|
26
|
+
{% endblock %}
|
|
27
|
+
</div>
|
|
28
|
+
</div>
|
|
29
|
+
<div class="{% block card_button_col_class %}col-auto{% endblock %} d-flex">
|
|
30
|
+
{% block card_button %}{% endblock %}
|
|
25
31
|
</div>
|
|
26
32
|
</div>
|
|
27
|
-
|
|
28
|
-
{% block card_button %}{% endblock %}
|
|
29
|
-
</div>
|
|
30
|
-
</div>
|
|
33
|
+
{% endblock %}
|
|
31
34
|
<div x-show="card_title_dropdown" x-cloak x-collapse class="flex-shrink-0">
|
|
32
35
|
{% block card_dropdown_content %}
|
|
33
36
|
{% endblock %}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
<div class="col-12">
|
|
3
3
|
<div class="row justify-content-between align-items-end px-md-3 py-2">
|
|
4
4
|
<div class="col">
|
|
5
|
-
<h1 class="fs-1 fw-semi-bold">
|
|
5
|
+
<h1 class="{% block container_title_class %}fs-1 fw-semi-bold{% endblock %}">
|
|
6
6
|
{% block container_title %}
|
|
7
7
|
{% endblock %}
|
|
8
8
|
</h1>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<form
|
|
2
2
|
method="get"
|
|
3
3
|
action="{% block session_filter_url %}{% endblock %}"
|
|
4
|
-
class="row border-bottom"
|
|
4
|
+
class="{% block form_class %}row border-bottom{% endblock %}"
|
|
5
5
|
>
|
|
6
6
|
<div class="col-12">
|
|
7
7
|
<input type="text" hidden value="{% block session_filter_key %}{% endblock %}" name="session_filter_key">
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
{% extends 'django_spire/form/field/base_field.html' %}
|
|
2
|
+
|
|
3
|
+
{% block field_content %}
|
|
4
|
+
<input
|
|
5
|
+
type="text"
|
|
6
|
+
x-model="value"
|
|
7
|
+
x-ref="glue_field"
|
|
8
|
+
hidden
|
|
9
|
+
>
|
|
10
|
+
|
|
11
|
+
<div
|
|
12
|
+
class="position-relative"
|
|
13
|
+
x-data="{
|
|
14
|
+
show_dropdown: false,
|
|
15
|
+
files: [],
|
|
16
|
+
objects: {},
|
|
17
|
+
|
|
18
|
+
accept: '{{ accept|default:'' }}',
|
|
19
|
+
compression: {{ compression|default:'true' }},
|
|
20
|
+
compression_ratio: {{ compression_ratio|default:'0.75' }},
|
|
21
|
+
disable_camera: '{{ disable_camera|default:'False' }}' === 'True',
|
|
22
|
+
disable_file: '{{ disable_file|default:'False' }}' === 'True',
|
|
23
|
+
maximum_filesize: {{ maximum_filesize|default:'10' }} * 1024 * 1024,
|
|
24
|
+
units: ['B', 'KB', 'MB', 'GB'],
|
|
25
|
+
|
|
26
|
+
init() {
|
|
27
|
+
if (!this.value) return;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
this.files = Array.isArray(this.value) ? this.value : JSON.parse(this.value);
|
|
31
|
+
} catch(e) {
|
|
32
|
+
this.files = [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
this.update_form_files();
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
async compress(file, quality = 0.75, maximum_width = 1920) {
|
|
39
|
+
try {
|
|
40
|
+
let image = await createImageBitmap(file);
|
|
41
|
+
|
|
42
|
+
let scale = Math.min(
|
|
43
|
+
1,
|
|
44
|
+
maximum_width / Math.max(image.width, image.height)
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
let width = image.width * scale;
|
|
48
|
+
let height = image.height * scale;
|
|
49
|
+
|
|
50
|
+
let canvas = Object.assign(document.createElement('canvas'), { width: width, height: height });
|
|
51
|
+
let ctx = canvas.getContext('2d');
|
|
52
|
+
|
|
53
|
+
ctx.drawImage(image, 0, 0, width, height);
|
|
54
|
+
|
|
55
|
+
let filename = file.name.replace(/\.[^/.]+$/, '') + '.jpg';
|
|
56
|
+
|
|
57
|
+
return new Promise(result => canvas.toBlob(
|
|
58
|
+
blob => result(new File([blob], filename, { type: 'image/jpeg' })),
|
|
59
|
+
'image/jpeg',
|
|
60
|
+
quality
|
|
61
|
+
));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('Compression failed:', error);
|
|
64
|
+
return file;
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
format_file_size(bytes) {
|
|
69
|
+
if (!bytes) return '';
|
|
70
|
+
|
|
71
|
+
let size = bytes;
|
|
72
|
+
let index = 0;
|
|
73
|
+
|
|
74
|
+
while (size >= 1024 && index < this.units.length - 1) {
|
|
75
|
+
size /= 1024;
|
|
76
|
+
index++;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return `${size.toFixed(1)} ${this.units[index]}`;
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
take_photo() {
|
|
83
|
+
this.$refs.camera_input.click();
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
truncate_name(name, max = 30) {
|
|
87
|
+
return name && name.length > max ? name.slice(0, max - 3) + '...' : name;
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
update_form_files() {
|
|
91
|
+
let transfer = new DataTransfer();
|
|
92
|
+
|
|
93
|
+
for (let object in this.objects) {
|
|
94
|
+
transfer.items.add(this.objects[object]);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
this.$refs.upload.files = transfer.files;
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
update_value() {
|
|
101
|
+
this.value = this.files.length > 0 ? JSON.stringify(this.files) : null;
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
{% block x-data %}{% endblock %}
|
|
105
|
+
}"
|
|
106
|
+
>
|
|
107
|
+
<input
|
|
108
|
+
type="file"
|
|
109
|
+
:name="{{ glue_field }}.name"
|
|
110
|
+
x-ref="upload"
|
|
111
|
+
class="d-none"
|
|
112
|
+
:accept="accept"
|
|
113
|
+
{% block upload_attributes %}{% endblock %}
|
|
114
|
+
>
|
|
115
|
+
|
|
116
|
+
<div class="d-flex">
|
|
117
|
+
<button
|
|
118
|
+
class="form-control text-start d-flex justify-content-between flex-grow-1"
|
|
119
|
+
type="button"
|
|
120
|
+
@click="show_dropdown = !show_dropdown"
|
|
121
|
+
:class="disable_file ? 'd-none' : ''"
|
|
122
|
+
>
|
|
123
|
+
<span class="d-flex flex-wrap align-items-center">
|
|
124
|
+
{% block selection_display %}{% endblock %}
|
|
125
|
+
</span>
|
|
126
|
+
|
|
127
|
+
<span class="d-flex align-items-center">
|
|
128
|
+
{% include 'django_glue/form/field/element/select_down_arrow_element.html' %}
|
|
129
|
+
</span>
|
|
130
|
+
</button>
|
|
131
|
+
|
|
132
|
+
<button
|
|
133
|
+
type="button"
|
|
134
|
+
class="btn border ms-2"
|
|
135
|
+
@click="take_photo()"
|
|
136
|
+
:class="disable_camera ? 'd-none' : ''"
|
|
137
|
+
>
|
|
138
|
+
<i class="bi bi-camera"></i>
|
|
139
|
+
</button>
|
|
140
|
+
</div>
|
|
141
|
+
|
|
142
|
+
<input
|
|
143
|
+
type="file"
|
|
144
|
+
x-ref="file_input"
|
|
145
|
+
class="d-none"
|
|
146
|
+
@change="handle_files_change"
|
|
147
|
+
:accept="accept"
|
|
148
|
+
:class="disable_file ? 'd-none' : ''"
|
|
149
|
+
{% block file_input_attributes %}{% endblock %}
|
|
150
|
+
>
|
|
151
|
+
|
|
152
|
+
<input
|
|
153
|
+
type="file"
|
|
154
|
+
x-ref="camera_input"
|
|
155
|
+
class="d-none"
|
|
156
|
+
@change="handle_files_change"
|
|
157
|
+
accept="image/*"
|
|
158
|
+
capture="environment"
|
|
159
|
+
:class="disable_camera ? 'd-none' : ''"
|
|
160
|
+
{% block camera_input_attributes %}{% endblock %}
|
|
161
|
+
>
|
|
162
|
+
|
|
163
|
+
<div
|
|
164
|
+
x-cloak
|
|
165
|
+
x-show="show_dropdown"
|
|
166
|
+
@click.outside="show_dropdown = false"
|
|
167
|
+
class="shadow border rounded-2 mt-2 bg-app-glue-layer-one w-100 p-0 list-group"
|
|
168
|
+
style="max-height: 350px; overflow-y: auto;"
|
|
169
|
+
>
|
|
170
|
+
<template x-for="file in files" :key="file.id">
|
|
171
|
+
<div
|
|
172
|
+
class="py-2 d-flex align-items-center list-group-item px-2 bg-app-glue-layer-one-hover"
|
|
173
|
+
>
|
|
174
|
+
<div class="me-2" style="width: 40px; height: 40px;">
|
|
175
|
+
<template x-if="file.preview">
|
|
176
|
+
<img :src="file.preview" class="img-fluid rounded"
|
|
177
|
+
style="max-width: 40px; max-height: 40px; object-fit: cover;">
|
|
178
|
+
</template>
|
|
179
|
+
|
|
180
|
+
<template x-if="!file.preview">
|
|
181
|
+
<div class="d-flex align-items-center justify-content-center rounded"
|
|
182
|
+
style="width: 40px; height: 40px;">
|
|
183
|
+
<i class="bi bi-file-earmark"></i>
|
|
184
|
+
</div>
|
|
185
|
+
</template>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="flex-grow-1 ms-2">
|
|
189
|
+
<div class="text-truncate" x-text="truncate_name(file.name)"></div>
|
|
190
|
+
<div class="glue-fs--2" x-text="format_file_size(file.size)"></div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div class="d-flex align-items-center">
|
|
194
|
+
<i
|
|
195
|
+
class="bi bi-trash text-danger glue-cursor-pointer mx-2"
|
|
196
|
+
@click="remove_file(file.id)"
|
|
197
|
+
></i>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
</template>
|
|
201
|
+
|
|
202
|
+
<div
|
|
203
|
+
class="m-2 border border-dashed rounded p-3 text-center bg-app-glue-layer-one glue-cursor-pointer"
|
|
204
|
+
@dragover.prevent="$el.classList.add('border-primary')"
|
|
205
|
+
@dragleave.prevent="$el.classList.remove('border-primary')"
|
|
206
|
+
@drop.prevent="$el.classList.remove('border-primary'); handle_files_change({target: {files: $event.dataTransfer.files}})"
|
|
207
|
+
@click="$refs.file_input.click()"
|
|
208
|
+
>
|
|
209
|
+
<div>
|
|
210
|
+
<i class="bi bi-cloud-arrow-up me-2"></i>
|
|
211
|
+
{% block drop_area_text %}{% endblock %}
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
</div>
|
|
215
|
+
</div>
|
|
216
|
+
{% endblock %}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{% extends 'django_spire/form/field/base_field.html' %}
|
|
2
|
+
|
|
3
|
+
{% block field_content %}
|
|
4
|
+
<div
|
|
5
|
+
class="glue-form-group"
|
|
6
|
+
x-ref="glue_field"
|
|
7
|
+
x-data="{
|
|
8
|
+
selected_values: [],
|
|
9
|
+
init_value(value) {
|
|
10
|
+
if (value === null || value === undefined) {
|
|
11
|
+
this.selectedValues = [];
|
|
12
|
+
} else if (Array.isArray(value)) {
|
|
13
|
+
this.selectedValues = [...value];
|
|
14
|
+
} else {
|
|
15
|
+
this.selectedValues = [value];
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
update_value() {
|
|
19
|
+
this.value = this.selectedValues.length > 0 ? this.selectedValues : null;
|
|
20
|
+
},
|
|
21
|
+
toggle_value(choice) {
|
|
22
|
+
if (this.selectedValues.includes(choice)) {
|
|
23
|
+
this.selectedValues = this.selectedValues.filter(v => v !== choice);
|
|
24
|
+
} else {
|
|
25
|
+
this.selectedValues.push(choice);
|
|
26
|
+
}
|
|
27
|
+
this.update_value();
|
|
28
|
+
}
|
|
29
|
+
}"
|
|
30
|
+
x-effect="init_value(value)"
|
|
31
|
+
>
|
|
32
|
+
<template x-for="choice in glue_field.choices" :key="choice[0]">
|
|
33
|
+
<div class="d-flex align-items-center mb-2">
|
|
34
|
+
<input
|
|
35
|
+
type="checkbox"
|
|
36
|
+
class="glue-form-check-input me-2"
|
|
37
|
+
:id="glue_field.id + '_' + choice[0]"
|
|
38
|
+
:name="glue_field.name"
|
|
39
|
+
:value="choice[0]"
|
|
40
|
+
:checked="selectedValues.includes(choice[0])"
|
|
41
|
+
@change="toggle_value(choice[0])"
|
|
42
|
+
style="accent-color: var(--glue-primary);"
|
|
43
|
+
>
|
|
44
|
+
<label
|
|
45
|
+
class="glue-form-check-label glue-text-primary glue-cursor-pointer"
|
|
46
|
+
:for="glue_field.id + '_' + choice[0]"
|
|
47
|
+
x-text="choice[1]"
|
|
48
|
+
></label>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
</div>
|
|
52
|
+
{% endblock %}
|