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.
Files changed (91) hide show
  1. django_spire/auth/templates/django_spire/auth/element/android_and_chrome_app_install_element.html +33 -0
  2. django_spire/auth/templates/django_spire/auth/element/ios_app_install_element.html +48 -0
  3. django_spire/auth/templates/django_spire/auth/page/auth_page.html +2 -1
  4. django_spire/auth/templates/django_spire/auth/page/login_page.html +2 -0
  5. django_spire/comment/mixins.py +3 -3
  6. django_spire/comment/templates/django_spire/comment/card/comment_list_card.html +8 -5
  7. django_spire/comment/templates/django_spire/comment/form/comment_form.html +3 -3
  8. django_spire/comment/templates/django_spire/comment/form/content/comment_form_content.html +1 -0
  9. django_spire/comment/templates/django_spire/comment/item/comment_item_ellipsis.html +12 -8
  10. django_spire/comment/views.py +8 -8
  11. django_spire/consts.py +1 -1
  12. django_spire/contrib/form/utils.py +3 -3
  13. django_spire/contrib/queryset/filter_tools.py +56 -14
  14. django_spire/contrib/queryset/mixins.py +24 -3
  15. django_spire/contrib/service/django_model_service.py +5 -6
  16. django_spire/core/management/commands/spire_startapp.py +42 -25
  17. django_spire/core/management/commands/spire_startapp_pkg/exceptions.py +5 -0
  18. django_spire/core/management/commands/spire_startapp_pkg/maps.py +64 -32
  19. django_spire/core/management/commands/spire_startapp_pkg/template/app/constants.py.template +1 -0
  20. django_spire/core/management/commands/spire_startapp_pkg/template/app/forms.py.template +4 -0
  21. django_spire/core/management/commands/spire_startapp_pkg/template/app/models.py.template +2 -1
  22. django_spire/core/management/commands/spire_startapp_pkg/template/app/querysets.py.template +15 -6
  23. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py.template +1 -0
  24. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py.template +6 -6
  25. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py.template +2 -2
  26. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/template_urls.py.template +12 -0
  27. django_spire/core/management/commands/spire_startapp_pkg/template/app/views/form_views.py.template +10 -11
  28. django_spire/core/management/commands/spire_startapp_pkg/template/app/views/page_views.py.template +17 -3
  29. django_spire/core/management/commands/spire_startapp_pkg/template/app/views/template_views.py.template +40 -0
  30. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${form_card_template_name}.html.template +1 -1
  31. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_base_card_template_name}.html.template +16 -0
  32. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_items_card_template_name}.html.template +16 -0
  33. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_table_card_template_name}.html.template +16 -0
  34. django_spire/core/management/commands/spire_startapp_pkg/template/templates/container/${list_container_template_name}.html.template +1 -0
  35. django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/${list_filter_form_template_name}.html.template +30 -0
  36. django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/${item_template_name}.html.template +32 -20
  37. django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/${list_items_template_name}.html.template +3 -0
  38. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${detail_page_template_name}.html.template +3 -3
  39. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${form_page_template_name}.html.template +2 -2
  40. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${list_page_template_name}.html.template +2 -2
  41. django_spire/core/management/commands/spire_startapp_pkg/template/templates/table/${table_row_template_name}.html.template +6 -0
  42. django_spire/core/management/commands/spire_startapp_pkg/template/templates/table/${table_rows_template_name}.html.template +3 -0
  43. django_spire/core/management/commands/spire_startapp_pkg/template/templates/table/${table_template_name}.html.template +6 -0
  44. django_spire/core/management/commands/spire_startapp_pkg/user_input.py +82 -9
  45. django_spire/core/management/commands/spire_startapp_pkg/validator.py +19 -6
  46. django_spire/core/querysets.py +19 -0
  47. django_spire/core/templates/django_spire/badge/base_badge.html +2 -3
  48. django_spire/core/templates/django_spire/base/base.html +1 -0
  49. django_spire/core/templates/django_spire/button/base_button.html +2 -1
  50. django_spire/core/templates/django_spire/card/title_card.html +13 -10
  51. django_spire/core/templates/django_spire/container/container.html +1 -1
  52. django_spire/core/templates/django_spire/filtering/form/base_session_filter_form.html +1 -1
  53. django_spire/core/templates/django_spire/form/field/_base_file_field.html +216 -0
  54. django_spire/core/templates/django_spire/form/field/_multi_checkbox_field.html +52 -0
  55. django_spire/core/templates/django_spire/form/field/base_field.html +128 -0
  56. django_spire/core/templates/django_spire/form/field/char_field.html +1 -0
  57. django_spire/core/templates/django_spire/form/field/color_field.html +1 -0
  58. django_spire/core/templates/django_spire/form/field/date_field.html +1 -0
  59. django_spire/core/templates/django_spire/form/field/datetime_field.html +1 -0
  60. django_spire/core/templates/django_spire/form/field/decimal_field.html +1 -0
  61. django_spire/core/templates/django_spire/form/field/element/select_checkmark_element.html +4 -0
  62. django_spire/core/templates/django_spire/form/field/element/select_down_arrow_element.html +5 -0
  63. django_spire/core/templates/django_spire/form/field/email_field.html +1 -0
  64. django_spire/core/templates/django_spire/form/field/input_field.html +13 -0
  65. django_spire/core/templates/django_spire/form/field/item/select_choice_item.html +15 -0
  66. django_spire/core/templates/django_spire/form/field/item/selected_choice_item.html +5 -0
  67. django_spire/core/templates/django_spire/form/field/list_field.html +112 -0
  68. django_spire/core/templates/django_spire/form/field/multi_file_field.html +90 -0
  69. django_spire/core/templates/django_spire/form/field/multi_select_field.html +155 -0
  70. django_spire/core/templates/django_spire/form/field/number_field.html +11 -0
  71. django_spire/core/templates/django_spire/form/field/password_field.html +1 -0
  72. django_spire/core/templates/django_spire/form/field/radio_field.html +24 -0
  73. django_spire/core/templates/django_spire/form/field/range_field.html +1 -0
  74. django_spire/core/templates/django_spire/form/field/search_and_select_field.html +119 -0
  75. django_spire/core/templates/django_spire/form/field/search_field.html +5 -0
  76. django_spire/core/templates/django_spire/form/field/select_field.html +78 -0
  77. django_spire/core/templates/django_spire/form/field/single_checkbox_field.html +27 -0
  78. django_spire/core/templates/django_spire/form/field/single_file_field.html +90 -0
  79. django_spire/core/templates/django_spire/form/field/telephone_field.html +1 -0
  80. django_spire/core/templates/django_spire/form/field/text_field.html +11 -0
  81. django_spire/core/templates/django_spire/form/field/time_field.html +1 -0
  82. django_spire/core/templates/django_spire/infinite_scroll/base.html +2 -1
  83. django_spire/core/templatetags/model_tags.py +34 -0
  84. django_spire/metric/report/tools.py +0 -2
  85. {django_spire-0.25.2.dist-info → django_spire-0.26.0.dist-info}/METADATA +1 -1
  86. {django_spire-0.25.2.dist-info → django_spire-0.26.0.dist-info}/RECORD +89 -45
  87. django_spire/core/management/commands/spire_startapp_pkg/template/app/tests/test_intelligence/__init__.py.template +0 -0
  88. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_card_template_name}.html.template +0 -18
  89. {django_spire-0.25.2.dist-info → django_spire-0.26.0.dist-info}/WHEEL +0 -0
  90. {django_spire-0.25.2.dist-info → django_spire-0.26.0.dist-info}/licenses/LICENSE.md +0 -0
  91. {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
- permission_data = self._collect_permission_inheritance(components)
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
- return self._collect_input('Enter the app label', default, '3/8')
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
- return self._collect_input('Enter the app name', default, '2/8')
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
- self.validator.validate_app_path(components)
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
- return self._collect_input('Enter the database table name', default, '6/8')
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
- return self._collect_input('Enter the model name', default, '4/8')
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
- return self._collect_input('Enter the model name plural', default, '5/8')
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
- return self._collect_input('Enter the model permission path', default, '7/8')
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
- self._reporter.write('\n', self._reporter.style_notice)
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
- message = (
71
- f'The app already exists at {destination}. '
72
- 'Please remove the existing app or choose a different name.'
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
- raise CommandError(message)
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)
@@ -4,9 +4,8 @@
4
4
  {{ badge_class|default:'fs--1' }}
5
5
  "
6
6
  >
7
- {% if badge_text %}
8
- {{ badge_text }}
9
- {% endif %}
7
+ {% block badge_text %}{% endblock %}
8
+ {{ badge_text }}
10
9
 
11
10
  {% if x_badge_text %}
12
11
  <span x-text="{{ x_badge_text }}"></span>
@@ -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
- <div class="row justify-content-between align-items-center mb-2 pb-2 border-bottom flex-shrink-0" style="min-height: 38px;">
21
- <div class="col d-flex align-items-center">
22
- <div class="card-title text-uppercase mb-0">
23
- {% block card_title %}
24
- {% endblock %}
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
- <div class="col-auto d-flex">
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 %}