django-spire 0.22.4__py3-none-any.whl → 0.23.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 (81) hide show
  1. django_spire/auth/group/forms.py +3 -4
  2. django_spire/auth/group/utils.py +1 -2
  3. django_spire/auth/group/views/form_views.py +22 -14
  4. django_spire/auth/group/views/page_views.py +1 -1
  5. django_spire/auth/mfa/utils.py +1 -1
  6. django_spire/auth/permissions/consts.py +2 -1
  7. django_spire/auth/permissions/decorators.py +1 -2
  8. django_spire/auth/permissions/permissions.py +1 -3
  9. django_spire/comment/factories.py +1 -1
  10. django_spire/comment/mixins.py +1 -1
  11. django_spire/comment/views.py +1 -1
  12. django_spire/consts.py +1 -1
  13. django_spire/contrib/breadcrumb/breadcrumbs.py +1 -1
  14. django_spire/contrib/form/confirmation_forms.py +1 -1
  15. django_spire/contrib/form/utils.py +26 -16
  16. django_spire/contrib/generic_views/modal_views.py +1 -1
  17. django_spire/contrib/generic_views/portal_views.py +18 -55
  18. django_spire/contrib/ordering/validators.py +4 -8
  19. django_spire/core/context_processors.py +1 -1
  20. django_spire/core/management/commands/spire_startapp_pkg/user_input.py +1 -1
  21. django_spire/core/middleware/maintenance.py +2 -1
  22. django_spire/core/middleware.py +2 -1
  23. django_spire/core/redirect/generic_redirect.py +1 -1
  24. django_spire/core/redirect/safe_redirect.py +2 -1
  25. django_spire/core/shortcuts.py +4 -2
  26. django_spire/core/table/__init__.py +0 -0
  27. django_spire/core/table/enums.py +18 -0
  28. django_spire/core/templates/django_spire/card/infinite_scroll_card.html +3 -137
  29. django_spire/core/templates/django_spire/container/infinite_scroll_container.html +64 -0
  30. django_spire/core/templates/django_spire/infinite_scroll/base.html +348 -0
  31. django_spire/core/templates/django_spire/infinite_scroll/element/footer.html +11 -0
  32. django_spire/core/templates/django_spire/infinite_scroll/scroll.html +152 -0
  33. django_spire/core/templates/django_spire/item/infinite_scroll_item.html +33 -0
  34. django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_section_element.html +19 -0
  35. django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_trigger_element.html +15 -0
  36. django_spire/core/templates/django_spire/lazy_tab/lazy_tab.html +157 -0
  37. django_spire/core/templates/django_spire/page/infinite_scroll_list_page.html +7 -0
  38. django_spire/core/templates/django_spire/table/base.html +185 -373
  39. django_spire/core/templates/django_spire/table/element/footer.html +7 -15
  40. django_spire/core/templates/django_spire/table/element/header.html +1 -1
  41. django_spire/core/templates/django_spire/table/element/row.html +15 -7
  42. django_spire/core/templatetags/spire_core_tags.py +1 -2
  43. django_spire/file/fields.py +1 -1
  44. django_spire/file/interfaces.py +1 -1
  45. django_spire/file/views.py +1 -1
  46. django_spire/history/activity/utils.py +1 -1
  47. django_spire/history/mixins.py +0 -4
  48. django_spire/notification/app/context_data.py +3 -1
  49. django_spire/notification/app/views/json_views.py +1 -1
  50. django_spire/notification/app/views/page_views.py +2 -1
  51. django_spire/notification/app/views/template_views.py +2 -2
  52. django_spire/profiling/middleware/profiling.py +2 -2
  53. django_spire/profiling/panel.py +2 -2
  54. django_spire/testing/__init__.py +0 -0
  55. django_spire/testing/playwright/__init__.py +64 -0
  56. django_spire/testing/playwright/components/__init__.py +45 -0
  57. django_spire/testing/playwright/components/accordion.py +55 -0
  58. django_spire/testing/playwright/components/attribute_element.py +73 -0
  59. django_spire/testing/playwright/components/base_session_filter_form.py +57 -0
  60. django_spire/testing/playwright/components/breadcrumb_element.py +56 -0
  61. django_spire/testing/playwright/components/card.py +102 -0
  62. django_spire/testing/playwright/components/dropdown.py +87 -0
  63. django_spire/testing/playwright/components/infinite_scroll.py +158 -0
  64. django_spire/testing/playwright/components/lazy_tab.py +92 -0
  65. django_spire/testing/playwright/components/modal.py +101 -0
  66. django_spire/testing/playwright/components/navigation.py +119 -0
  67. django_spire/testing/playwright/components/notification_bell.py +59 -0
  68. django_spire/testing/playwright/components/theme_selector.py +46 -0
  69. django_spire/testing/playwright/components/toast.py +72 -0
  70. django_spire/testing/playwright/fixtures.py +54 -0
  71. django_spire/testing/playwright/pages/__init__.py +6 -0
  72. django_spire/testing/playwright/pages/base.py +24 -0
  73. django_spire/theme/models.py +1 -1
  74. django_spire/theme/tests/test_context_processor.py +0 -1
  75. django_spire/theme/views/json_views.py +1 -1
  76. django_spire/theme/views/page_views.py +1 -1
  77. {django_spire-0.22.4.dist-info → django_spire-0.23.0.dist-info}/METADATA +4 -1
  78. {django_spire-0.22.4.dist-info → django_spire-0.23.0.dist-info}/RECORD +81 -51
  79. {django_spire-0.22.4.dist-info → django_spire-0.23.0.dist-info}/WHEEL +0 -0
  80. {django_spire-0.22.4.dist-info → django_spire-0.23.0.dist-info}/licenses/LICENSE.md +0 -0
  81. {django_spire-0.22.4.dist-info → django_spire-0.23.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,152 @@
1
+ <div
2
+ x-data="{
3
+ batch_size: 25,
4
+ current_page: 0,
5
+ endpoint: '',
6
+ has_next: true,
7
+ scroll_id: null,
8
+ total_count: 0,
9
+
10
+ is_loading: false,
11
+ loaded_count: 0,
12
+ loading_timer: null,
13
+ observer: null,
14
+ show_loading: false,
15
+
16
+ async init() {
17
+ this.scroll_id = $id('scroll');
18
+ this.endpoint = this.$el.dataset.endpoint || '';
19
+ this.batch_size = parseInt(this.$el.dataset.batchSize || '25');
20
+
21
+ await this.$nextTick();
22
+
23
+ if (this.$refs.scroll_container) {
24
+ this.$refs.scroll_container.scrollTop = 0;
25
+ }
26
+
27
+ await this.load_more();
28
+
29
+ if (this.has_next) {
30
+ setTimeout(() => this.setup_observer(), 500);
31
+ }
32
+ },
33
+
34
+ handle_item_mounted(event) {
35
+ if (event.detail.scroll_id && event.detail.scroll_id !== this.scroll_id) {
36
+ return;
37
+ }
38
+ this.loaded_count++;
39
+ },
40
+
41
+ handle_total_count_updated(event) {
42
+ if (event.detail.scroll_id && event.detail.scroll_id !== this.scroll_id) {
43
+ return;
44
+ }
45
+
46
+ this.total_count = event.detail.total_count;
47
+
48
+ if (event.detail.batch_size) {
49
+ this.batch_size = event.detail.batch_size;
50
+ }
51
+ },
52
+
53
+ async load_more() {
54
+ if (!this.endpoint || this.is_loading) {
55
+ return;
56
+ }
57
+
58
+ this.is_loading = true;
59
+
60
+ this.loading_timer = setTimeout(() => {
61
+ this.show_loading = true;
62
+ }, 200);
63
+
64
+ let params = new URLSearchParams({
65
+ page: this.current_page + 1,
66
+ batch_size: this.batch_size
67
+ });
68
+
69
+ let url = `${this.endpoint}?${params}`;
70
+ let view = new ViewGlue(url, {});
71
+
72
+ let previous_count = this.loaded_count;
73
+
74
+ await view.render_insert_adjacent(this.$refs.content_container, {}, 'beforeend');
75
+
76
+ let added = this.loaded_count - previous_count;
77
+
78
+ if (added > 0) {
79
+ this.current_page++;
80
+ }
81
+
82
+ if (added < this.batch_size) {
83
+ this.has_next = false;
84
+ if (this.observer) {
85
+ this.observer.disconnect();
86
+ }
87
+ }
88
+
89
+ if (this.loading_timer) {
90
+ clearTimeout(this.loading_timer);
91
+ this.loading_timer = null;
92
+ }
93
+ this.is_loading = false;
94
+ this.show_loading = false;
95
+ },
96
+
97
+ setup_observer() {
98
+ let trigger = this.$refs.infinite_scroll_trigger;
99
+
100
+ if (!trigger) {
101
+ return;
102
+ }
103
+
104
+ this.observer = new IntersectionObserver(
105
+ (entries) => {
106
+ entries.forEach(async entry => {
107
+ if (entry.isIntersecting && this.has_next && !this.is_loading) {
108
+ await this.load_more();
109
+ }
110
+ });
111
+ },
112
+ { root: null, rootMargin: '200px', threshold: 0.01 }
113
+ );
114
+
115
+ this.observer.observe(trigger);
116
+ },
117
+ }"
118
+ data-batch-size="{{ batch_size|default:25 }}"
119
+ data-endpoint="{{ endpoint }}"
120
+ :data-scroll-id="scroll_id"
121
+ @item-mounted.window="handle_item_mounted($event)"
122
+ @total-count-updated.window="handle_total_count_updated($event)"
123
+ >
124
+ <div class="position-relative" style="height: {{ scroll_height|default:'300px' }};">
125
+ <div
126
+ class="h-100"
127
+ style="overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;"
128
+ x-ref="scroll_container"
129
+ >
130
+ <div x-ref="content_container"></div>
131
+
132
+ <div style="height: 10px;" x-ref="infinite_scroll_trigger"></div>
133
+ </div>
134
+
135
+ <template x-if="show_loading">
136
+ <div
137
+ class="position-absolute d-flex justify-content-center align-items-center"
138
+ style="top: 0; left: 0; right: 0; bottom: 0; background: color-mix(in srgb, var(--app-layer-one) 85%, transparent);"
139
+ >
140
+ <div class="spinner-border text-app-primary" role="status"></div>
141
+ </div>
142
+ </template>
143
+ </div>
144
+
145
+ <div class="row mt-3">
146
+ <div class="col text-start">
147
+ <span class="fs-7 text-app-secondary">
148
+ Showing <span x-text="loaded_count"></span> of <span x-text="total_count"></span> items
149
+ </span>
150
+ </div>
151
+ </div>
152
+ </div>
@@ -0,0 +1,33 @@
1
+ {% if forloop.first %}
2
+ <div
3
+ x-data="{
4
+ init() {
5
+ let scroll_container = this.$el.closest('[data-scroll-id]');
6
+ let scroll_id = scroll_container ? scroll_container.dataset.scrollId : null;
7
+
8
+ this.$dispatch('total-count-updated', {
9
+ batch_size: {{ batch_size|default:25 }},
10
+ scroll_id: scroll_id,
11
+ total_count: {{ total_count|default:0 }}
12
+ });
13
+ }
14
+ }"
15
+ ></div>
16
+ {% endif %}
17
+
18
+ <div
19
+ class="border-bottom mb-3 pb-3 {% block item_class %}{% endblock %}"
20
+ x-data="{
21
+ init() {
22
+ let scroll_container = this.$el.closest('[data-scroll-id]');
23
+ let scroll_id = scroll_container ? scroll_container.dataset.scrollId : null;
24
+
25
+ this.$dispatch('item-mounted', {
26
+ item_element: this.$el,
27
+ scroll_id: scroll_id
28
+ });
29
+ }
30
+ }"
31
+ >
32
+ {% block item_content %}{% endblock %}
33
+ </div>
@@ -0,0 +1,19 @@
1
+ <section
2
+ role="tabpanel"
3
+ x-cloak
4
+ x-init="$dispatch('register-section', { el: $el })"
5
+ x-show="is_section_selected($el)"
6
+ >
7
+ <div x-ref="content">
8
+ {% block lazy_tab_section_loading %}
9
+ <div
10
+ class="align-items-center d-flex justify-content-center py-5"
11
+ x-show="is_section_loading($el)"
12
+ >
13
+ <div class="spinner-border text-app-primary" role="status">
14
+ <span class="visually-hidden">Loading...</span>
15
+ </div>
16
+ </div>
17
+ {% endblock %}
18
+ </div>
19
+ </section>
@@ -0,0 +1,15 @@
1
+ <li>
2
+ <button
3
+ @click="select($el)"
4
+ @focus="select($el)"
5
+ :aria-selected="is_trigger_selected($el)"
6
+ class="bg-transparent h-100 mb-1 position-relative px-3"
7
+ :class="is_trigger_selected($el) ? 'tab-item text-app-default-link-color' : 'text-app-secondary'"
8
+ role="tab"
9
+ style="border: none;"
10
+ type="button"
11
+ x-init="$dispatch('register-trigger', { el: $el, endpoint: '{{ endpoint|default:'' }}' })"
12
+ >
13
+ {{ trigger_title }}
14
+ </button>
15
+ </li>
@@ -0,0 +1,157 @@
1
+ <div
2
+ x-data="{
3
+ loaded_tabs: new Set(),
4
+ loading_tabs: new Set(),
5
+ sections: [],
6
+ selected_tab: 1,
7
+ tab_id: '{{ tab_id|default:'' }}',
8
+ triggers: [],
9
+ use_url_params: {{ tab_id|yesno:'true,false' }},
10
+
11
+ init() {
12
+ if (!this.tab_id) {
13
+ this.tab_id = this.$id('lazy-tab');
14
+ }
15
+
16
+ if (this.use_url_params) {
17
+ let url_tab = this.get_url_param();
18
+
19
+ if (url_tab) {
20
+ this.selected_tab = url_tab;
21
+ }
22
+ }
23
+ },
24
+
25
+ register_trigger(detail) {
26
+ this.triggers.push({
27
+ el: detail.el,
28
+ endpoint: detail.endpoint || null
29
+ });
30
+ },
31
+
32
+ register_section(detail) {
33
+ this.sections.push({
34
+ el: detail.el,
35
+ content_el: detail.el.firstElementChild
36
+ });
37
+
38
+ if (this.triggers.length === this.sections.length && !this.is_loaded(this.selected_tab)) {
39
+ this.$nextTick(() => this.load_tab_content(this.selected_tab));
40
+ }
41
+ },
42
+
43
+ select(el) {
44
+ let tab_number = this.get_trigger_index(el);
45
+ this.selected_tab = tab_number;
46
+
47
+ if (this.use_url_params) {
48
+ this.update_url(tab_number);
49
+ }
50
+
51
+ this.load_tab_content(tab_number);
52
+
53
+ this.$dispatch('tab-changed', {
54
+ tab_id: this.tab_id,
55
+ tab_number: tab_number
56
+ });
57
+ },
58
+
59
+ get_trigger_index(el) {
60
+ let index = this.triggers.findIndex(t => t.el === el);
61
+ return index + 1;
62
+ },
63
+
64
+ get_section_index(el) {
65
+ let index = this.sections.findIndex(s => s.el === el);
66
+ return index + 1;
67
+ },
68
+
69
+ is_trigger_selected(el) {
70
+ return this.selected_tab === this.get_trigger_index(el);
71
+ },
72
+
73
+ is_section_selected(el) {
74
+ return this.selected_tab === this.get_section_index(el);
75
+ },
76
+
77
+ is_loaded(tab_number) {
78
+ return this.loaded_tabs.has(tab_number);
79
+ },
80
+
81
+ is_loading(tab_number) {
82
+ return this.loading_tabs.has(tab_number);
83
+ },
84
+
85
+ is_section_loading(el) {
86
+ return this.loading_tabs.has(this.get_section_index(el));
87
+ },
88
+
89
+ async load_tab_content(tab_number) {
90
+ if (this.is_loaded(tab_number) || this.is_loading(tab_number)) {
91
+ return;
92
+ }
93
+
94
+ let trigger = this.triggers[tab_number - 1];
95
+ let section = this.sections[tab_number - 1];
96
+
97
+ if (!trigger || !trigger.endpoint || !section) {
98
+ this.loaded_tabs.add(tab_number);
99
+ return;
100
+ }
101
+
102
+ this.loading_tabs.add(tab_number);
103
+
104
+ toggle_loading_overlay();
105
+
106
+ let view = new ViewGlue(trigger.endpoint);
107
+ await view.render_inner(section.content_el);
108
+
109
+ this.loading_tabs.delete(tab_number);
110
+ this.loaded_tabs.add(tab_number);
111
+
112
+ toggle_loading_overlay();
113
+ },
114
+
115
+ get_url_param() {
116
+ let url = new URL(window.location);
117
+ let value = url.searchParams.get(this.tab_id);
118
+ let parsed = parseInt(value, 10);
119
+
120
+ if (!isNaN(parsed) && parsed > 0) {
121
+ return parsed;
122
+ }
123
+
124
+ return null;
125
+ },
126
+
127
+ update_url(tab_number) {
128
+ let url = new URL(window.location);
129
+ url.searchParams.set(this.tab_id, tab_number);
130
+ window.history.replaceState({}, '', url);
131
+ }
132
+ }"
133
+ @register-section="register_section($event.detail)"
134
+ @register-trigger="register_trigger($event.detail)"
135
+ >
136
+ <ul
137
+ @keydown.end.prevent.stop="$focus.last()"
138
+ @keydown.home.prevent.stop="$focus.first()"
139
+ @keydown.left.prevent.stop="$focus.wrap().prev()"
140
+ @keydown.page-down.prevent.stop="$focus.last()"
141
+ @keydown.page-up.prevent.stop="$focus.first()"
142
+ @keydown.right.prevent.stop="$focus.wrap().next()"
143
+ class="border-1 border-bottom d-flex list-unstyled"
144
+ role="tablist"
145
+ >
146
+ {% block tab_trigger %}
147
+ {% endblock %}
148
+ </ul>
149
+
150
+ {% block tab_function %}
151
+ {% endblock %}
152
+
153
+ <div>
154
+ {% block tab_section %}
155
+ {% endblock %}
156
+ </div>
157
+ </div>
@@ -0,0 +1,7 @@
1
+ {% extends 'django_spire/page/full_page.html' %}
2
+
3
+ {% block full_page_content %}
4
+ {% block infinite_scroll_container %}
5
+ {% include 'django_spire/container/infinite_scroll_container.html' %}
6
+ {% endblock %}
7
+ {% endblock %}