django-spire 0.25.1__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 (124) hide show
  1. django_spire/ai/chat/router.py +10 -20
  2. django_spire/ai/chat/templates/django_spire/ai/chat/dropdown/ellipsis_dropdown.html +5 -3
  3. django_spire/ai/chat/templates/django_spire/ai/chat/message/default_message.html +6 -2
  4. django_spire/ai/chat/templatetags/spire_ai_chat_tags.py +19 -0
  5. django_spire/ai/chat/tests/test_router/test_spire_chat_router.py +20 -88
  6. django_spire/auth/templates/django_spire/auth/element/android_and_chrome_app_install_element.html +33 -0
  7. django_spire/auth/templates/django_spire/auth/element/ios_app_install_element.html +48 -0
  8. django_spire/auth/templates/django_spire/auth/page/auth_page.html +2 -1
  9. django_spire/auth/templates/django_spire/auth/page/login_page.html +2 -0
  10. django_spire/comment/mixins.py +3 -3
  11. django_spire/comment/templates/django_spire/comment/card/comment_list_card.html +8 -5
  12. django_spire/comment/templates/django_spire/comment/form/comment_form.html +3 -3
  13. django_spire/comment/templates/django_spire/comment/form/content/comment_form_content.html +1 -0
  14. django_spire/comment/templates/django_spire/comment/item/comment_item_ellipsis.html +12 -8
  15. django_spire/comment/views.py +8 -8
  16. django_spire/consts.py +1 -1
  17. django_spire/contrib/form/utils.py +3 -3
  18. django_spire/contrib/progress/session.py +1 -1
  19. django_spire/contrib/queryset/filter_tools.py +56 -14
  20. django_spire/contrib/queryset/mixins.py +24 -3
  21. django_spire/contrib/service/django_model_service.py +5 -6
  22. django_spire/core/management/commands/spire_startapp.py +42 -25
  23. django_spire/core/management/commands/spire_startapp_pkg/exceptions.py +5 -0
  24. django_spire/core/management/commands/spire_startapp_pkg/maps.py +64 -32
  25. django_spire/core/management/commands/spire_startapp_pkg/template/app/constants.py.template +1 -0
  26. django_spire/core/management/commands/spire_startapp_pkg/template/app/forms.py.template +4 -0
  27. django_spire/core/management/commands/spire_startapp_pkg/template/app/models.py.template +2 -1
  28. django_spire/core/management/commands/spire_startapp_pkg/template/app/querysets.py.template +15 -6
  29. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/__init__.py.template +1 -0
  30. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/form_urls.py.template +6 -6
  31. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/page_urls.py.template +2 -2
  32. django_spire/core/management/commands/spire_startapp_pkg/template/app/urls/template_urls.py.template +12 -0
  33. django_spire/core/management/commands/spire_startapp_pkg/template/app/views/form_views.py.template +10 -11
  34. django_spire/core/management/commands/spire_startapp_pkg/template/app/views/page_views.py.template +17 -3
  35. django_spire/core/management/commands/spire_startapp_pkg/template/app/views/template_views.py.template +40 -0
  36. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${form_card_template_name}.html.template +1 -1
  37. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_base_card_template_name}.html.template +16 -0
  38. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_items_card_template_name}.html.template +16 -0
  39. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_table_card_template_name}.html.template +16 -0
  40. django_spire/core/management/commands/spire_startapp_pkg/template/templates/container/${list_container_template_name}.html.template +1 -0
  41. django_spire/core/management/commands/spire_startapp_pkg/template/templates/form/${list_filter_form_template_name}.html.template +30 -0
  42. django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/${item_template_name}.html.template +32 -20
  43. django_spire/core/management/commands/spire_startapp_pkg/template/templates/item/${list_items_template_name}.html.template +3 -0
  44. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${detail_page_template_name}.html.template +3 -3
  45. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${form_page_template_name}.html.template +2 -2
  46. django_spire/core/management/commands/spire_startapp_pkg/template/templates/page/${list_page_template_name}.html.template +2 -2
  47. django_spire/core/management/commands/spire_startapp_pkg/template/templates/table/${table_row_template_name}.html.template +6 -0
  48. django_spire/core/management/commands/spire_startapp_pkg/template/templates/table/${table_rows_template_name}.html.template +3 -0
  49. django_spire/core/management/commands/spire_startapp_pkg/template/templates/table/${table_template_name}.html.template +6 -0
  50. django_spire/core/management/commands/spire_startapp_pkg/user_input.py +82 -9
  51. django_spire/core/management/commands/spire_startapp_pkg/validator.py +19 -6
  52. django_spire/core/middleware.py +2 -3
  53. django_spire/core/querysets.py +19 -0
  54. django_spire/core/static/django_spire/js/theme.js +10 -7
  55. django_spire/core/templates/django_spire/badge/base_badge.html +2 -3
  56. django_spire/core/templates/django_spire/base/base.html +1 -0
  57. django_spire/core/templates/django_spire/button/base_button.html +2 -1
  58. django_spire/core/templates/django_spire/card/title_card.html +13 -10
  59. django_spire/core/templates/django_spire/container/container.html +1 -1
  60. django_spire/core/templates/django_spire/filtering/form/base_session_filter_form.html +1 -1
  61. django_spire/core/templates/django_spire/form/field/_base_file_field.html +216 -0
  62. django_spire/core/templates/django_spire/form/field/_multi_checkbox_field.html +52 -0
  63. django_spire/core/templates/django_spire/form/field/base_field.html +128 -0
  64. django_spire/core/templates/django_spire/form/field/char_field.html +1 -0
  65. django_spire/core/templates/django_spire/form/field/color_field.html +1 -0
  66. django_spire/core/templates/django_spire/form/field/date_field.html +1 -0
  67. django_spire/core/templates/django_spire/form/field/datetime_field.html +1 -0
  68. django_spire/core/templates/django_spire/form/field/decimal_field.html +1 -0
  69. django_spire/core/templates/django_spire/form/field/element/select_checkmark_element.html +4 -0
  70. django_spire/core/templates/django_spire/form/field/element/select_down_arrow_element.html +5 -0
  71. django_spire/core/templates/django_spire/form/field/email_field.html +1 -0
  72. django_spire/core/templates/django_spire/form/field/input_field.html +13 -0
  73. django_spire/core/templates/django_spire/form/field/item/select_choice_item.html +15 -0
  74. django_spire/core/templates/django_spire/form/field/item/selected_choice_item.html +5 -0
  75. django_spire/core/templates/django_spire/form/field/list_field.html +112 -0
  76. django_spire/core/templates/django_spire/form/field/multi_file_field.html +90 -0
  77. django_spire/core/templates/django_spire/form/field/multi_select_field.html +155 -0
  78. django_spire/core/templates/django_spire/form/field/number_field.html +11 -0
  79. django_spire/core/templates/django_spire/form/field/password_field.html +1 -0
  80. django_spire/core/templates/django_spire/form/field/radio_field.html +24 -0
  81. django_spire/core/templates/django_spire/form/field/range_field.html +1 -0
  82. django_spire/core/templates/django_spire/form/field/search_and_select_field.html +119 -0
  83. django_spire/core/templates/django_spire/form/field/search_field.html +5 -0
  84. django_spire/core/templates/django_spire/form/field/select_field.html +78 -0
  85. django_spire/core/templates/django_spire/form/field/single_checkbox_field.html +27 -0
  86. django_spire/core/templates/django_spire/form/field/single_file_field.html +90 -0
  87. django_spire/core/templates/django_spire/form/field/telephone_field.html +1 -0
  88. django_spire/core/templates/django_spire/form/field/text_field.html +11 -0
  89. django_spire/core/templates/django_spire/form/field/time_field.html +1 -0
  90. django_spire/core/templates/django_spire/infinite_scroll/base.html +2 -1
  91. django_spire/core/templatetags/model_tags.py +34 -0
  92. django_spire/knowledge/entry/forms.py +1 -1
  93. django_spire/knowledge/entry/models.py +18 -0
  94. django_spire/knowledge/entry/querysets.py +8 -6
  95. django_spire/knowledge/entry/services/processor_service.py +1 -0
  96. django_spire/knowledge/entry/services/search_index_service.py +61 -0
  97. django_spire/knowledge/entry/services/search_service.py +99 -0
  98. django_spire/knowledge/entry/services/service.py +6 -0
  99. django_spire/knowledge/entry/version/services/processor_service.py +2 -0
  100. django_spire/knowledge/entry/version/tests/factories.py +9 -4
  101. django_spire/knowledge/entry/version/tests/test_services.py +7 -16
  102. django_spire/knowledge/intelligence/bots/knowledge_answer_bot.py +40 -6
  103. django_spire/knowledge/intelligence/bots/knowledge_entries_bot.py +4 -2
  104. django_spire/knowledge/intelligence/bots/search_preprocessing_bot.py +32 -0
  105. django_spire/knowledge/intelligence/intel/entry_intel.py +12 -0
  106. django_spire/knowledge/intelligence/router.py +47 -4
  107. django_spire/knowledge/intelligence/workflows/knowledge_workflow.py +24 -42
  108. django_spire/knowledge/intelligence/workflows/search_preprocessing_workflow.py +78 -0
  109. django_spire/knowledge/management/__init__.py +0 -0
  110. django_spire/knowledge/management/commands/__init__.py +0 -0
  111. django_spire/knowledge/management/commands/rebuild_knowledge_search_index.py +16 -0
  112. django_spire/knowledge/migrations/0010_entry__search_text_entry__search_vector_and_more.py +40 -0
  113. django_spire/knowledge/templates/django_spire/knowledge/message/knowledge_message_intel.html +31 -23
  114. django_spire/metric/report/enums.py +11 -5
  115. django_spire/metric/report/report.py +24 -12
  116. django_spire/metric/report/tools.py +14 -4
  117. django_spire/testing/playwright/fixtures.py +4 -5
  118. {django_spire-0.25.1.dist-info → django_spire-0.26.0.dist-info}/METADATA +1 -1
  119. {django_spire-0.25.1.dist-info → django_spire-0.26.0.dist-info}/RECORD +123 -69
  120. django_spire/core/management/commands/spire_startapp_pkg/template/templates/card/${list_card_template_name}.html.template +0 -18
  121. /django_spire/{core/management/commands/spire_startapp_pkg/template/app/tests/test_intelligence/__init__.py.template → ai/chat/templatetags/__init__.py} +0 -0
  122. {django_spire-0.25.1.dist-info → django_spire-0.26.0.dist-info}/WHEEL +0 -0
  123. {django_spire-0.25.1.dist-info → django_spire-0.26.0.dist-info}/licenses/LICENSE.md +0 -0
  124. {django_spire-0.25.1.dist-info → django_spire-0.26.0.dist-info}/top_level.txt +0 -0
@@ -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 %}
@@ -0,0 +1,128 @@
1
+ {% load django_glue %}
2
+
3
+ <div x-data="{
4
+ value: null
5
+ }"
6
+ {% if glue_model_field %}
7
+ x-modelable="value"
8
+ x-model="{{ glue_model_field }}"
9
+ {% endif %}
10
+
11
+ {% if glue_field %}
12
+ x-modelable="value"
13
+ x-model="{{ glue_field }}.value"
14
+ {% endif %}
15
+ >
16
+ <div
17
+ x-data="{
18
+ glue_field: undefined,
19
+ init() {
20
+ let glue_model_field_name = '{{ glue_model_field }}'
21
+ let glue_field_name = '{{ glue_field }}'
22
+
23
+ if (glue_model_field_name) {
24
+ this.init_glue_model_field(glue_model_field_name)
25
+ } else if (glue_field_name) {
26
+ this.init_glue_field(glue_field_name)
27
+ }
28
+
29
+ if (this.is_valid_glue_field()) {
30
+ this.set_field_attrs()
31
+ this.add_error_event_listener()
32
+ }
33
+
34
+ this.$watch('glue_field', value => {
35
+ if (this.is_valid_glue_field()) {
36
+ this.set_field_attrs()
37
+ }
38
+ })
39
+ },
40
+ init_glue_model_field(glue_model_field_name) {
41
+ let path = glue_model_field_name.split('.')
42
+ this.glue_field = this[`${path[0]}`]['glue_fields'][`${path[1]}`]
43
+ },
44
+ init_glue_field(glue_field_name) {
45
+ this.glue_field = glue_field_name.split('.').reduce((acc, part) => acc && acc[part], this)
46
+ },
47
+ set_field_attrs() {
48
+ let form_field = this.$refs.glue_field
49
+
50
+ // Remove attrs that no longer exist
51
+ let attr_names = this.glue_field._attr_names
52
+ for (const attr_name of this.glue_field._historic_attr_names) {
53
+ if (!attr_names.includes(attr_name)) {
54
+ form_field.removeAttribute(attr_name)
55
+ }
56
+ }
57
+
58
+ // Set attrs on field
59
+ for (let attr of this.glue_field.attrs) {
60
+ form_field.setAttribute(attr.name, attr.value)
61
+ }
62
+ },
63
+ add_error_event_listener() {
64
+ this.$refs.glue_field.addEventListener('invalid', (e) => {
65
+ e.preventDefault();
66
+ this.glue_field.error = e.target.validationMessage
67
+ this.$refs.glue_field.scrollIntoView({ behavior: 'smooth', block: 'center' })
68
+ })
69
+ },
70
+ is_valid_glue_field() {
71
+ return this.glue_field && this.$refs.glue_field
72
+ }
73
+ }"
74
+ >
75
+ {% block field_label %}
76
+ <label
77
+ x-cloak
78
+ x-show="!glue_field._hide_label"
79
+ x-ref="label"
80
+ class="form-label"
81
+ :for="glue_field.id"
82
+ >
83
+ <span x-text="glue_field.label"></span>
84
+ <span x-cloak x-show="glue_field.required" class="text-danger">*</span>
85
+
86
+ <template x-if="glue_field.help_text && glue_field.help_text.trim() !== ''">
87
+ <span
88
+ x-data="{ is_tooltip_visible: false }"
89
+ class="position-relative mx-2"
90
+ >
91
+ <button
92
+ type="button"
93
+ class="btn btn-link p-0 lh-1"
94
+ style="text-decoration: none; color: inherit; outline: none !important; box-shadow: none !important;"
95
+ @mouseenter="is_tooltip_visible = true"
96
+ @mouseleave="is_tooltip_visible = false"
97
+ @click="is_tooltip_visible = !is_tooltip_visible; $event.currentTarget.blur();"
98
+ @focus="$event.currentTarget.blur();"
99
+ >
100
+ <i class="bi bi-question-circle-fill" style="font-size: 0.75rem;"></i>
101
+ </button>
102
+ <div
103
+ x-show="is_tooltip_visible"
104
+ class="fs-sm position-absolute p-2 rounded mt-2"
105
+ style="
106
+ min-width: 200px;
107
+ z-index: 1000;
108
+ top: 100%;
109
+ left: 0;
110
+ background-color: rgba(33, 37, 41, 0.85);
111
+ color: #fff;
112
+ font-weight: 400;
113
+ "
114
+ x-text="glue_field.help_text"
115
+ ></div>
116
+ </span>
117
+ </template>
118
+ </label>
119
+ {% endblock %}
120
+
121
+ {% block field_content %}
122
+ {% endblock %}
123
+
124
+ <div x-show="glue_field.error">
125
+ <span class="text-danger glue-fs--1" x-text="glue_field.error"></span>
126
+ </div>
127
+ </div>
128
+ </div>
@@ -0,0 +1 @@
1
+ {% include 'django_spire/form/field/input_field.html' with input_type='text' %}
@@ -0,0 +1 @@
1
+ {% include 'django_spire/form/field/input_field.html' with input_type='color' %}
@@ -0,0 +1 @@
1
+ {% include 'django_spire/form/field/input_field.html' with input_type='date' %}
@@ -0,0 +1 @@
1
+ {% include 'django_spire/form/field/input_field.html' with input_type='datetime-local' %}
@@ -0,0 +1 @@
1
+ {% include 'django_spire/form/field/input_field.html' with input_type='number' %}
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="15" height="15" class="bi bi-check ms-1" viewBox="0 0 16 16"
2
+ fill="currentColor">
3
+ <path d="M10.97 4.97a.75.75 0 0 1 1.07 1.05l-3.99 4.99a.75.75 0 0 1-1.08.02L4.324 8.384a.75.75 0 1 1 1.06-1.06l2.094 2.093 3.473-4.425a.267.267 0 0 1 .02-.022z"/>
4
+ </svg>
@@ -0,0 +1,5 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" fill="currentColor" class="bi bi-chevron-down"
2
+ viewBox="0 0 16 16">
3
+ <path fill-rule="evenodd"
4
+ d="M1.646 4.646a.5.5 0 0 1 .708 0L8 10.293l5.646-5.647a.5.5 0 0 1 .708.708l-6 6a.5.5 0 0 1-.708 0l-6-6a.5.5 0 0 1 0-.708z"/>
5
+ </svg>
@@ -0,0 +1 @@
1
+ {% include 'django_spire/form/field/input_field.html' with input_type='email' %}
@@ -0,0 +1,13 @@
1
+ {% extends 'django_spire/form/field/base_field.html' %}
2
+
3
+ {% block field_content %}
4
+ <input
5
+ {% if input_type %}type="{{ input_type }}"
6
+ {% else %}type="{% block input_type %}text{% endblock %}"{% endif %}
7
+ {% block input_field_attrs %}{{ input_field_attrs }}{% endblock %}
8
+ class="{% block input_field_class %}{% if input_field_class %}{{ input_field_class }}{% else %}form-control{% endif %}{% endblock %}"
9
+ x-ref="glue_field"
10
+ x-model="value"
11
+ :id="glue_field.id"
12
+ >
13
+ {% endblock %}
@@ -0,0 +1,15 @@
1
+ <div class="d-flex align-items-center justify-content-center" style="width: 35px;">
2
+ {% block choice_checkmark %}
3
+ <template x-if="value === choice[0]">
4
+ {% include 'django_spire/form/field/element/select_checkmark_element.html' %}
5
+ </template>
6
+
7
+ <template x-if="value != choice[0]">
8
+ <span class="ms-3 ps-2"></span>
9
+ </template>
10
+ {% endblock %}
11
+ </div>
12
+
13
+ <div class="d-flex align-items-center">
14
+ <span class="text-truncate" x-text="choice[1]"></span>
15
+ </div>
@@ -0,0 +1,5 @@
1
+ {% extends 'django_spire/form/field/item/select_choice_item.html' %}
2
+
3
+ {% block choice_checkmark %}
4
+ {% include 'django_spire/form/field/element/select_checkmark_element.html' %}
5
+ {% endblock %}
@@ -0,0 +1,112 @@
1
+ {% extends "django_spire/form/field/base_field.html" %}
2
+
3
+ {% block field_content %}
4
+ <div
5
+ x-data="{
6
+ item: null,
7
+ available: [],
8
+ choices: [],
9
+ selected: [],
10
+ queryset: [],
11
+
12
+ async init() {
13
+ this.glue_field.available = await this.glue_field.queryset.all();
14
+ this.glue_field.choices = await this.glue_field.queryset.to_choices();
15
+ this.glue_field.selected = await this.glue_field.selected.all();
16
+
17
+ this.selected = this.glue_field.selected
18
+ ? Object.entries(this.glue_field.selected).map(([id, values]) => ({ id, ...values }))
19
+ : [];
20
+ },
21
+
22
+ async add_item() {
23
+ let item = this.glue_field.value;
24
+ let found = this.glue_field.choices.find(choice => choice[0] == item);
25
+
26
+ if (item && found) {
27
+ let selection = this.glue_field.available[item - 1];
28
+
29
+ this.selected.push({
30
+ id: item,
31
+ ...selection
32
+ });
33
+
34
+ this.glue_field.choices = this.glue_field.choices.filter(choice => choice[0] != item);
35
+
36
+ if (this.glue_field.choices.length > 0) {
37
+ this.glue_field.value = this.glue_field.choices[0][0];
38
+ } else {
39
+ this.glue_field.value = '';
40
+ }
41
+ }
42
+ },
43
+
44
+ async remove_item(index) {
45
+ let removed = this.selected[index]
46
+ this.selected.splice(index, 1)
47
+
48
+ this.glue_field.value = ''
49
+
50
+ let label = this.glue_field.display_label
51
+
52
+ this.glue_field.choices = [
53
+ ...this.glue_field.choices,
54
+ [removed.id, removed[label]]
55
+ ]
56
+
57
+ this.glue_field.value = removed.id
58
+ }
59
+ }"
60
+ x-effect="
61
+ glue_field.hide_label();
62
+ "
63
+ >
64
+ <div class="row g-2 mb-2">
65
+ <div x-ref="component">
66
+ {% block component_field %}
67
+ <div class="col-10">
68
+ {% include "django_spire/form/field/search_and_select_field.html" with glue_field=glue_field %}
69
+ </div>
70
+ {% endblock %}
71
+ </div>
72
+
73
+ {% block add_item_button %}
74
+ <div class="col-2 d-flex align-items-stretch">
75
+ <button
76
+ type="button"
77
+ @click.prevent="add_item()"
78
+ class="glue-btn glue-btn-primary w-100 h-100"
79
+ >
80
+ Add
81
+ </button>
82
+ </div>
83
+ {% endblock %}
84
+ </div>
85
+
86
+ <template x-if="selected.length === 0">
87
+ <div class="mt-4">
88
+ No item(s) selected.
89
+ </div>
90
+ </template>
91
+
92
+ <template x-for="(item, index) in selected" :key="item.id">
93
+ <div class="row align-items-center glue-bg-layer-two-hover glue-list-item-container glue-border-bottom glue-border py-2">
94
+ {% if row %}
95
+ {% include row %}
96
+ {% else %}
97
+ {% block item_row_content %}
98
+ {% endblock %}
99
+
100
+ {% block remove_item_button %}
101
+ {% endblock %}
102
+ {% endif %}
103
+ </div>
104
+ </template>
105
+
106
+ <input
107
+ type="hidden"
108
+ name="{{ glue_field.name }}"
109
+ :value="JSON.stringify(selected)"
110
+ >
111
+ </div>
112
+ {% endblock %}
@@ -0,0 +1,90 @@
1
+ {% extends 'django_spire/form/field/_base_file_field.html' %}
2
+
3
+ {% block x-data %}
4
+ async handle_files_change(event) {
5
+ let files = Array.from(event.target.files || []);
6
+
7
+ for (let file of files) {
8
+ if (file.size > this.maximum_filesize) {
9
+ alert(`The file is too large (${this.format_file_size(file.size)}). Maximum size is
10
+ ${this.format_file_size(this.maximum_filesize)}.`);
11
+ continue;
12
+ }
13
+
14
+ let index = this.files.findIndex(existing => existing.name === file.name);
15
+
16
+ if (index !== -1) {
17
+ URL.revokeObjectURL(this.files[index].data);
18
+ URL.revokeObjectURL(this.files[index].preview);
19
+ this.files.splice(index, 1);
20
+ }
21
+
22
+ let file_id = Date.now() + '-' + Math.random().toString(36).substr(2, 9);
23
+ let processed = file;
24
+
25
+ if (file.type.startsWith('image/') && file.type !== 'image/svg+xml' && this.compression) {
26
+ processed = await this.compress(file);
27
+ }
28
+
29
+ this.objects[file_id] = processed;
30
+
31
+ let data = URL.createObjectURL(processed);
32
+
33
+ this.files.push({
34
+ id: file_id,
35
+ name: processed.name,
36
+ size: processed.size,
37
+ type: processed.type,
38
+ data: data,
39
+ preview: processed.type.startsWith('image/') ? data : null
40
+ });
41
+
42
+ this.show_dropdown = false;
43
+ }
44
+
45
+ this.update_value();
46
+ this.show_dropdown = true;
47
+ this.update_form_files();
48
+
49
+ this.$refs.file_input.value = '';
50
+ this.$refs.camera_input.value = '';
51
+ },
52
+
53
+ remove_file(file_id) {
54
+ let file = this.files.find(file => file.id === file_id);
55
+
56
+ if (file) {
57
+ URL.revokeObjectURL(file.data);
58
+ if (file.preview) URL.revokeObjectURL(file.preview);
59
+ }
60
+
61
+ delete this.objects[file_id];
62
+
63
+ this.files = this.files.filter(file => file.id !== file_id);
64
+
65
+ this.update_value();
66
+ this.update_form_files();
67
+
68
+ if (this.files.length === 0) {
69
+ this.show_dropdown = false;
70
+ }
71
+ }
72
+ {% endblock %}
73
+
74
+ {% block upload_attributes %}multiple{% endblock %}
75
+ {% block file_input_attributes %}multiple{% endblock %}
76
+ {% block camera_input_attributes %}multiple{% endblock %}
77
+
78
+ {% block selection_display %}
79
+ <template x-if="files.length === 0">
80
+ <span>No files selected</span>
81
+ </template>
82
+
83
+ <template x-if="files.length > 0">
84
+ <span x-text="files.length + ' files selected'"></span>
85
+ </template>
86
+ {% endblock %}
87
+
88
+ {% block drop_area_text %}
89
+ <span>Click to add or drag and drop files here</span>
90
+ {% endblock %}