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.
- django_spire/auth/group/forms.py +3 -4
- django_spire/auth/group/utils.py +1 -2
- django_spire/auth/group/views/form_views.py +22 -14
- django_spire/auth/group/views/page_views.py +1 -1
- django_spire/auth/mfa/utils.py +1 -1
- django_spire/auth/permissions/consts.py +2 -1
- django_spire/auth/permissions/decorators.py +1 -2
- django_spire/auth/permissions/permissions.py +1 -3
- django_spire/comment/factories.py +1 -1
- django_spire/comment/mixins.py +1 -1
- django_spire/comment/views.py +1 -1
- django_spire/consts.py +1 -1
- django_spire/contrib/breadcrumb/breadcrumbs.py +1 -1
- django_spire/contrib/form/confirmation_forms.py +1 -1
- django_spire/contrib/form/utils.py +26 -16
- django_spire/contrib/generic_views/modal_views.py +1 -1
- django_spire/contrib/generic_views/portal_views.py +18 -55
- django_spire/contrib/ordering/validators.py +4 -8
- django_spire/core/context_processors.py +1 -1
- django_spire/core/management/commands/spire_startapp_pkg/user_input.py +1 -1
- django_spire/core/middleware/maintenance.py +2 -1
- django_spire/core/middleware.py +2 -1
- django_spire/core/redirect/generic_redirect.py +1 -1
- django_spire/core/redirect/safe_redirect.py +2 -1
- django_spire/core/shortcuts.py +4 -2
- django_spire/core/table/__init__.py +0 -0
- django_spire/core/table/enums.py +18 -0
- django_spire/core/templates/django_spire/card/infinite_scroll_card.html +3 -137
- django_spire/core/templates/django_spire/container/infinite_scroll_container.html +64 -0
- django_spire/core/templates/django_spire/infinite_scroll/base.html +348 -0
- django_spire/core/templates/django_spire/infinite_scroll/element/footer.html +11 -0
- django_spire/core/templates/django_spire/infinite_scroll/scroll.html +152 -0
- django_spire/core/templates/django_spire/item/infinite_scroll_item.html +33 -0
- django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_section_element.html +19 -0
- django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_trigger_element.html +15 -0
- django_spire/core/templates/django_spire/lazy_tab/lazy_tab.html +157 -0
- django_spire/core/templates/django_spire/page/infinite_scroll_list_page.html +7 -0
- django_spire/core/templates/django_spire/table/base.html +185 -373
- django_spire/core/templates/django_spire/table/element/footer.html +7 -15
- django_spire/core/templates/django_spire/table/element/header.html +1 -1
- django_spire/core/templates/django_spire/table/element/row.html +15 -7
- django_spire/core/templatetags/spire_core_tags.py +1 -2
- django_spire/file/fields.py +1 -1
- django_spire/file/interfaces.py +1 -1
- django_spire/file/views.py +1 -1
- django_spire/history/activity/utils.py +1 -1
- django_spire/history/mixins.py +0 -4
- django_spire/notification/app/context_data.py +3 -1
- django_spire/notification/app/views/json_views.py +1 -1
- django_spire/notification/app/views/page_views.py +2 -1
- django_spire/notification/app/views/template_views.py +2 -2
- django_spire/profiling/middleware/profiling.py +2 -2
- django_spire/profiling/panel.py +2 -2
- django_spire/testing/__init__.py +0 -0
- django_spire/testing/playwright/__init__.py +64 -0
- django_spire/testing/playwright/components/__init__.py +45 -0
- django_spire/testing/playwright/components/accordion.py +55 -0
- django_spire/testing/playwright/components/attribute_element.py +73 -0
- django_spire/testing/playwright/components/base_session_filter_form.py +57 -0
- django_spire/testing/playwright/components/breadcrumb_element.py +56 -0
- django_spire/testing/playwright/components/card.py +102 -0
- django_spire/testing/playwright/components/dropdown.py +87 -0
- django_spire/testing/playwright/components/infinite_scroll.py +158 -0
- django_spire/testing/playwright/components/lazy_tab.py +92 -0
- django_spire/testing/playwright/components/modal.py +101 -0
- django_spire/testing/playwright/components/navigation.py +119 -0
- django_spire/testing/playwright/components/notification_bell.py +59 -0
- django_spire/testing/playwright/components/theme_selector.py +46 -0
- django_spire/testing/playwright/components/toast.py +72 -0
- django_spire/testing/playwright/fixtures.py +54 -0
- django_spire/testing/playwright/pages/__init__.py +6 -0
- django_spire/testing/playwright/pages/base.py +24 -0
- django_spire/theme/models.py +1 -1
- django_spire/theme/tests/test_context_processor.py +0 -1
- django_spire/theme/views/json_views.py +1 -1
- django_spire/theme/views/page_views.py +1 -1
- {django_spire-0.22.4.dist-info → django_spire-0.23.0.dist-info}/METADATA +4 -1
- {django_spire-0.22.4.dist-info → django_spire-0.23.0.dist-info}/RECORD +81 -51
- {django_spire-0.22.4.dist-info → django_spire-0.23.0.dist-info}/WHEEL +0 -0
- {django_spire-0.22.4.dist-info → django_spire-0.23.0.dist-info}/licenses/LICENSE.md +0 -0
- {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>
|