django-spire 0.22.4__py3-none-any.whl → 0.23.2__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 (84) 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/contrib/queryset/enums.py +2 -3
  20. django_spire/contrib/queryset/mixins.py +17 -6
  21. django_spire/contrib/session/controller.py +0 -1
  22. django_spire/core/context_processors.py +1 -1
  23. django_spire/core/management/commands/spire_startapp_pkg/user_input.py +1 -1
  24. django_spire/core/middleware/maintenance.py +2 -1
  25. django_spire/core/middleware.py +2 -1
  26. django_spire/core/redirect/generic_redirect.py +1 -1
  27. django_spire/core/redirect/safe_redirect.py +2 -1
  28. django_spire/core/shortcuts.py +4 -2
  29. django_spire/core/table/__init__.py +0 -0
  30. django_spire/core/table/enums.py +18 -0
  31. django_spire/core/templates/django_spire/card/infinite_scroll_card.html +3 -137
  32. django_spire/core/templates/django_spire/container/infinite_scroll_container.html +64 -0
  33. django_spire/core/templates/django_spire/infinite_scroll/base.html +348 -0
  34. django_spire/core/templates/django_spire/infinite_scroll/element/footer.html +11 -0
  35. django_spire/core/templates/django_spire/infinite_scroll/scroll.html +142 -0
  36. django_spire/core/templates/django_spire/item/infinite_scroll_item.html +33 -0
  37. django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_section_element.html +19 -0
  38. django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_trigger_element.html +15 -0
  39. django_spire/core/templates/django_spire/lazy_tab/lazy_tab.html +157 -0
  40. django_spire/core/templates/django_spire/page/infinite_scroll_list_page.html +7 -0
  41. django_spire/core/templates/django_spire/table/base.html +185 -373
  42. django_spire/core/templates/django_spire/table/element/footer.html +7 -15
  43. django_spire/core/templates/django_spire/table/element/header.html +1 -1
  44. django_spire/core/templates/django_spire/table/element/row.html +15 -7
  45. django_spire/core/templatetags/spire_core_tags.py +1 -2
  46. django_spire/file/fields.py +1 -1
  47. django_spire/file/interfaces.py +1 -1
  48. django_spire/file/views.py +1 -1
  49. django_spire/history/activity/utils.py +1 -1
  50. django_spire/history/mixins.py +0 -4
  51. django_spire/notification/app/context_data.py +3 -1
  52. django_spire/notification/app/views/json_views.py +1 -1
  53. django_spire/notification/app/views/page_views.py +2 -1
  54. django_spire/notification/app/views/template_views.py +2 -2
  55. django_spire/profiling/middleware/profiling.py +2 -2
  56. django_spire/profiling/panel.py +2 -2
  57. django_spire/testing/__init__.py +0 -0
  58. django_spire/testing/playwright/__init__.py +64 -0
  59. django_spire/testing/playwright/components/__init__.py +45 -0
  60. django_spire/testing/playwright/components/accordion.py +55 -0
  61. django_spire/testing/playwright/components/attribute_element.py +73 -0
  62. django_spire/testing/playwright/components/base_session_filter_form.py +57 -0
  63. django_spire/testing/playwright/components/breadcrumb_element.py +56 -0
  64. django_spire/testing/playwright/components/card.py +102 -0
  65. django_spire/testing/playwright/components/dropdown.py +87 -0
  66. django_spire/testing/playwright/components/infinite_scroll.py +158 -0
  67. django_spire/testing/playwright/components/lazy_tab.py +92 -0
  68. django_spire/testing/playwright/components/modal.py +101 -0
  69. django_spire/testing/playwright/components/navigation.py +119 -0
  70. django_spire/testing/playwright/components/notification_bell.py +59 -0
  71. django_spire/testing/playwright/components/theme_selector.py +46 -0
  72. django_spire/testing/playwright/components/toast.py +72 -0
  73. django_spire/testing/playwright/fixtures.py +54 -0
  74. django_spire/testing/playwright/pages/__init__.py +6 -0
  75. django_spire/testing/playwright/pages/base.py +24 -0
  76. django_spire/theme/models.py +1 -1
  77. django_spire/theme/tests/test_context_processor.py +0 -1
  78. django_spire/theme/views/json_views.py +1 -1
  79. django_spire/theme/views/page_views.py +1 -1
  80. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/METADATA +4 -1
  81. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/RECORD +84 -54
  82. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/WHEEL +0 -0
  83. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/licenses/LICENSE.md +0 -0
  84. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,142 @@
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 separator = this.endpoint.includes('?') ? '&' : '?';
70
+ let url = `${this.endpoint}${separator}${params}`;
71
+ let view = new ViewGlue(url, {});
72
+
73
+ let previous_count = this.loaded_count;
74
+
75
+ await view.render_insert_adjacent(this.$refs.content_container, {}, 'beforeend');
76
+
77
+ let added = this.loaded_count - previous_count;
78
+
79
+ if (added > 0) {
80
+ this.current_page++;
81
+ }
82
+
83
+ if (added < this.batch_size) {
84
+ this.has_next = false;
85
+ if (this.observer) {
86
+ this.observer.disconnect();
87
+ }
88
+ }
89
+
90
+ if (this.loading_timer) {
91
+ clearTimeout(this.loading_timer);
92
+ this.loading_timer = null;
93
+ }
94
+ this.is_loading = false;
95
+ this.show_loading = false;
96
+ },
97
+
98
+ setup_observer() {
99
+ let trigger = this.$refs.infinite_scroll_trigger;
100
+
101
+ if (!trigger) {
102
+ return;
103
+ }
104
+
105
+ this.observer = new IntersectionObserver(
106
+ (entries) => {
107
+ entries.forEach(async entry => {
108
+ if (entry.isIntersecting && this.has_next && !this.is_loading) {
109
+ await this.load_more();
110
+ }
111
+ });
112
+ },
113
+ { root: null, rootMargin: '200px', threshold: 0.01 }
114
+ );
115
+
116
+ this.observer.observe(trigger);
117
+ },
118
+ }"
119
+ data-batch-size="{{ batch_size|default:25 }}"
120
+ data-endpoint="{{ endpoint }}"
121
+ :data-scroll-id="scroll_id"
122
+ @item-mounted.window="handle_item_mounted($event)"
123
+ @total-count-updated.window="handle_total_count_updated($event)"
124
+ >
125
+ <div class="position-relative" style="height: {{ scroll_height|default:'300px' }};">
126
+ <div
127
+ class="h-100"
128
+ style="overflow-x: hidden; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;"
129
+ x-ref="scroll_container"
130
+ >
131
+ <div x-ref="content_container"></div>
132
+
133
+ <div x-cloak x-show="show_loading" class="py-3 text-center">
134
+ <div class="spinner-border text-app-primary" role="status"></div>
135
+ </div>
136
+
137
+ <div style="height: 10px;" x-ref="infinite_scroll_trigger"></div>
138
+ </div>
139
+ </div>
140
+
141
+ {% include 'django_spire/infinite_scroll/element/footer.html' %}
142
+ </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 %}