django-spire 0.24.2__py3-none-any.whl → 0.24.3__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/consts.py +1 -1
- django_spire/contrib/admin/__init__.py +0 -0
- django_spire/contrib/admin/admin.py +140 -0
- django_spire/contrib/choices/__init__.py +0 -0
- django_spire/contrib/choices/choices.py +9 -0
- django_spire/contrib/choices/tests/__init__.py +0 -0
- django_spire/contrib/choices/tests/test_choices.py +62 -0
- django_spire/core/templates/django_spire/dropdown/ellipsis_modal_dropdown.html +15 -3
- django_spire/core/templates/django_spire/element/attribute_element.html +7 -0
- django_spire/core/templates/django_spire/element/copy_to_clipboard_element.html +31 -0
- django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown.html +10 -10
- django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown_content.html +1 -33
- django_spire/notification/app/templates/django_spire/notification/app/item/notification_item.html +24 -23
- django_spire/notification/app/templates/django_spire/notification/app/page/list_page.html +1 -1
- django_spire/notification/app/templates/django_spire/notification/app/scroll/container/dropdown_container.html +54 -0
- django_spire/notification/app/templates/django_spire/notification/app/scroll/item/items.html +5 -0
- django_spire/notification/app/urls/template_urls.py +9 -2
- django_spire/notification/app/views/page_views.py +2 -10
- django_spire/notification/app/views/template_views.py +22 -7
- {django_spire-0.24.2.dist-info → django_spire-0.24.3.dist-info}/METADATA +1 -1
- {django_spire-0.24.2.dist-info → django_spire-0.24.3.dist-info}/RECORD +24 -16
- django_spire/notification/app/templates/django_spire/notification/app/card/list_card.html +0 -11
- {django_spire-0.24.2.dist-info → django_spire-0.24.3.dist-info}/WHEEL +0 -0
- {django_spire-0.24.2.dist-info → django_spire-0.24.3.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.24.2.dist-info → django_spire-0.24.3.dist-info}/top_level.txt +0 -0
django_spire/consts.py
CHANGED
|
File without changes
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
from typing import Type, Tuple
|
|
2
|
+
|
|
3
|
+
from django.contrib import admin
|
|
4
|
+
from django.contrib.contenttypes.fields import GenericRelation
|
|
5
|
+
from django.db import models
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SpireModelAdmin(admin.ModelAdmin):
|
|
9
|
+
model_class: Type[models.Model] = None
|
|
10
|
+
|
|
11
|
+
max_search_fields: int = 5
|
|
12
|
+
max_list_display: int = 10
|
|
13
|
+
|
|
14
|
+
trailing_fields = ('is_active', 'is_deleted')
|
|
15
|
+
|
|
16
|
+
auto_readonly_fields: Tuple[str] = (
|
|
17
|
+
'created_datetime', 'is_active', 'is_deleted',
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
filter_field_types = (
|
|
21
|
+
models.BooleanField,
|
|
22
|
+
models.DateField,
|
|
23
|
+
models.DateTimeField,
|
|
24
|
+
models.ForeignKey,
|
|
25
|
+
models.CharField,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def __init_subclass__(cls, **kwargs):
|
|
29
|
+
super().__init_subclass__(**kwargs)
|
|
30
|
+
|
|
31
|
+
cls.model_fields = cls.model_class._meta.get_fields()
|
|
32
|
+
|
|
33
|
+
if cls.model_class is None:
|
|
34
|
+
raise ValueError(f'{cls.__name__} must define model_class')
|
|
35
|
+
|
|
36
|
+
if cls.model_class is not None:
|
|
37
|
+
cls._configure_if_needed()
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def _configure_if_needed(cls):
|
|
41
|
+
if not hasattr(cls, '_spire_configured'):
|
|
42
|
+
cls._configure_list_display()
|
|
43
|
+
cls._configure_list_filter()
|
|
44
|
+
cls._configure_search_fields()
|
|
45
|
+
cls._configure_readonly_fields()
|
|
46
|
+
cls._configure_ordering()
|
|
47
|
+
cls._configure_list_per_page()
|
|
48
|
+
cls._spire_configured = True
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
def _configure_list_display(cls):
|
|
52
|
+
if cls.list_display != ('__str__',):
|
|
53
|
+
return
|
|
54
|
+
|
|
55
|
+
fields = []
|
|
56
|
+
|
|
57
|
+
for field in cls.model_fields:
|
|
58
|
+
if not isinstance(
|
|
59
|
+
field,
|
|
60
|
+
(models.ManyToManyField, models.ManyToOneRel, GenericRelation),
|
|
61
|
+
):
|
|
62
|
+
if hasattr(field, 'name') and not field.name.startswith('_'):
|
|
63
|
+
if field.name not in cls.trailing_fields:
|
|
64
|
+
fields.append(field.name)
|
|
65
|
+
|
|
66
|
+
for trailing_field in cls.trailing_fields:
|
|
67
|
+
if trailing_field in [field.name for field in cls.model_fields]:
|
|
68
|
+
fields.append(trailing_field)
|
|
69
|
+
|
|
70
|
+
cls.list_display = tuple(fields[:cls.max_list_display])
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def _configure_list_filter(cls):
|
|
74
|
+
if hasattr(cls, 'list_filter') and cls.list_filter:
|
|
75
|
+
return
|
|
76
|
+
|
|
77
|
+
filters = []
|
|
78
|
+
|
|
79
|
+
for field in cls.model_fields:
|
|
80
|
+
if not isinstance(field, (models.ManyToManyField, models.ManyToOneRel)):
|
|
81
|
+
if isinstance(field, models.BooleanField):
|
|
82
|
+
filters.append(field.name)
|
|
83
|
+
|
|
84
|
+
elif isinstance(field, (models.DateField, models.DateTimeField)):
|
|
85
|
+
filters.append(field.name)
|
|
86
|
+
|
|
87
|
+
elif isinstance(field, models.ForeignKey):
|
|
88
|
+
filters.append(field.name)
|
|
89
|
+
|
|
90
|
+
elif (
|
|
91
|
+
isinstance(field, models.CharField)
|
|
92
|
+
and hasattr(field, 'choices')
|
|
93
|
+
and field.choices
|
|
94
|
+
):
|
|
95
|
+
filters.append(field.name)
|
|
96
|
+
|
|
97
|
+
cls.list_filter = tuple(filters)
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def _configure_search_fields(cls):
|
|
101
|
+
if hasattr(cls, 'search_fields') and cls.search_fields:
|
|
102
|
+
return
|
|
103
|
+
|
|
104
|
+
search_fields = []
|
|
105
|
+
|
|
106
|
+
for field in cls.model_fields:
|
|
107
|
+
if isinstance(field, (models.CharField, models.TextField)):
|
|
108
|
+
if not field.name.startswith('_'):
|
|
109
|
+
search_fields.append(field.name)
|
|
110
|
+
|
|
111
|
+
if len(search_fields) >= cls.max_search_fields:
|
|
112
|
+
break
|
|
113
|
+
|
|
114
|
+
cls.search_fields = tuple(search_fields)
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def _configure_readonly_fields(cls):
|
|
118
|
+
if hasattr(cls, 'readonly_fields') and cls.readonly_fields:
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
readonly = []
|
|
122
|
+
|
|
123
|
+
for field in cls.model_fields:
|
|
124
|
+
if hasattr(field, 'name') and field.name in cls.auto_readonly_fields:
|
|
125
|
+
readonly.append(field.name)
|
|
126
|
+
|
|
127
|
+
cls.readonly_fields = tuple(readonly)
|
|
128
|
+
|
|
129
|
+
@classmethod
|
|
130
|
+
def _configure_ordering(cls):
|
|
131
|
+
if hasattr(cls, 'ordering') and cls.ordering:
|
|
132
|
+
return
|
|
133
|
+
|
|
134
|
+
cls.ordering = ('-id',)
|
|
135
|
+
return
|
|
136
|
+
|
|
137
|
+
@classmethod
|
|
138
|
+
def _configure_list_per_page(cls):
|
|
139
|
+
if not hasattr(cls, 'list_per_page') or cls.list_per_page == 100:
|
|
140
|
+
cls.list_per_page = 25
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from django_spire.contrib.choices.choices import SpireTextChoices
|
|
4
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestSpireTextChoices(BaseTestCase):
|
|
8
|
+
def setUp(self):
|
|
9
|
+
class StatusChoices(SpireTextChoices):
|
|
10
|
+
DRAFT = ('dra', 'Draft')
|
|
11
|
+
PUBLISHED = ('pub', 'Published')
|
|
12
|
+
ARCHIVED = ('arc', 'Archived')
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
self.StatusChoices = StatusChoices
|
|
16
|
+
|
|
17
|
+
def test_inherits_from_text_choices(self):
|
|
18
|
+
assert issubclass(self.StatusChoices, SpireTextChoices), self.StatusChoices.__class__
|
|
19
|
+
|
|
20
|
+
def test_choices_property_exists(self):
|
|
21
|
+
assert hasattr(self.StatusChoices, 'choices'), self.StatusChoices.dir()
|
|
22
|
+
assert self.StatusChoices.choices is not None, self.StatusChoices.choices
|
|
23
|
+
|
|
24
|
+
def test_to_glue_choices_returns_string(self):
|
|
25
|
+
result = self.StatusChoices.to_glue_choices()
|
|
26
|
+
assert isinstance(result, str), type(result)
|
|
27
|
+
|
|
28
|
+
def test_to_glue_choices_returns_valid_json(self):
|
|
29
|
+
result = self.StatusChoices.to_glue_choices()
|
|
30
|
+
try:
|
|
31
|
+
parsed = json.loads(result)
|
|
32
|
+
except json.JSONDecodeError:
|
|
33
|
+
assert 'to_glue_choices did not return valid JSON'
|
|
34
|
+
|
|
35
|
+
def test_to_glue_choices_correct_structure(self):
|
|
36
|
+
result = self.StatusChoices.to_glue_choices()
|
|
37
|
+
parsed = json.loads(result)
|
|
38
|
+
|
|
39
|
+
assert isinstance(parsed, list), type(parsed)
|
|
40
|
+
assert len(parsed) == 3
|
|
41
|
+
|
|
42
|
+
assert parsed[0] == ['dra', 'Draft']
|
|
43
|
+
assert parsed[1] == ['pub', 'Published']
|
|
44
|
+
assert parsed[2] == ['arc', 'Archived']
|
|
45
|
+
|
|
46
|
+
def test_empty_choices(self):
|
|
47
|
+
class EmptyChoices(SpireTextChoices):
|
|
48
|
+
pass
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
result = EmptyChoices.to_glue_choices()
|
|
52
|
+
parsed = json.loads(result)
|
|
53
|
+
assert parsed == []
|
|
54
|
+
|
|
55
|
+
def test_single_choice(self):
|
|
56
|
+
class SingleChoice(SpireTextChoices):
|
|
57
|
+
ONLY = 'only', 'Only Option'
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
result = SingleChoice.to_glue_choices()
|
|
61
|
+
parsed = json.loads(result)
|
|
62
|
+
assert parsed == [['only', 'Only Option']]
|
|
@@ -12,14 +12,26 @@
|
|
|
12
12
|
|
|
13
13
|
{% block dropdown_content %}
|
|
14
14
|
{% if view_url %}
|
|
15
|
-
{%
|
|
15
|
+
{% if redirect_view_url %}
|
|
16
|
+
{% include 'django_spire/dropdown/element/dropdown_link_element.html' with view_url=view_url link_text='View' %}
|
|
17
|
+
{% else %}
|
|
18
|
+
{% include 'django_spire/dropdown/element/ellipsis_dropdown_modal_link_element.html' with view_url=view_url link_text='View' %}
|
|
19
|
+
{% endif %}
|
|
16
20
|
{% endif %}
|
|
17
21
|
|
|
18
22
|
{% if edit_url %}
|
|
19
|
-
{%
|
|
23
|
+
{% if redirect_edit_url %}
|
|
24
|
+
{% include 'django_spire/dropdown/element/dropdown_link_element.html' with view_url=edit_url link_text='Edit' %}
|
|
25
|
+
{% else %}
|
|
26
|
+
{% include 'django_spire/dropdown/element/ellipsis_dropdown_modal_link_element.html' with view_url=edit_url link_text='Edit' %}
|
|
27
|
+
{% endif %}
|
|
20
28
|
{% endif %}
|
|
21
29
|
|
|
22
30
|
{% if delete_url %}
|
|
23
|
-
{%
|
|
31
|
+
{% if redirect_delete_url %}
|
|
32
|
+
{% include 'django_spire/dropdown/element/dropdown_link_element.html' with view_url=delete_url link_text='Delete' link_css='text-app-danger' %}
|
|
33
|
+
{% else %}
|
|
34
|
+
{% include 'django_spire/dropdown/element/ellipsis_dropdown_modal_link_element.html' with view_url=delete_url link_text='Delete' link_css='text-app-danger' %}
|
|
35
|
+
{% endif %}
|
|
24
36
|
{% endif %}
|
|
25
37
|
{% endblock %}
|
|
@@ -32,6 +32,13 @@
|
|
|
32
32
|
<span x-text="{{ x_attribute_value_postfix }}"></span>
|
|
33
33
|
{% endif %}
|
|
34
34
|
{% endif %}
|
|
35
|
+
|
|
36
|
+
{% if show_copy_button %}
|
|
37
|
+
{% block copy_button %}
|
|
38
|
+
{% firstof attribute_value or x_attribute_value as value %}
|
|
39
|
+
{% include 'django_spire/element/copy_to_clipboard_element.html' with value=value copy_func=copy_func %}
|
|
40
|
+
{% endblock %}
|
|
41
|
+
{% endif %}
|
|
35
42
|
{% endblock %}
|
|
36
43
|
|
|
37
44
|
{% if attribute_href or x_attribute_href %}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
<span
|
|
2
|
+
x-data="{
|
|
3
|
+
clicked: false,
|
|
4
|
+
copy_value_to_clipboard() {
|
|
5
|
+
let text = '{{ value }}';
|
|
6
|
+
|
|
7
|
+
{% if copy_func %}
|
|
8
|
+
text = {{ copy_func }}(text);
|
|
9
|
+
{% endif %}
|
|
10
|
+
|
|
11
|
+
navigator.clipboard.writeText(text);
|
|
12
|
+
this.clicked = true;
|
|
13
|
+
setTimeout(() => {
|
|
14
|
+
this.clicked = false;
|
|
15
|
+
}, 1000);
|
|
16
|
+
}
|
|
17
|
+
}"
|
|
18
|
+
>
|
|
19
|
+
<i
|
|
20
|
+
@click="copy_value_to_clipboard()"
|
|
21
|
+
class="bi bi-copy text-app-primary cursor-pointer"
|
|
22
|
+
></i>
|
|
23
|
+
<span
|
|
24
|
+
class="text-app-primary"
|
|
25
|
+
x-show="clicked"
|
|
26
|
+
x-transition:enter.duration.500ms
|
|
27
|
+
x-transition:leave.duration.500ms
|
|
28
|
+
>
|
|
29
|
+
Copied!
|
|
30
|
+
</span>
|
|
31
|
+
</span>
|
|
@@ -4,28 +4,28 @@
|
|
|
4
4
|
<div x-data="{
|
|
5
5
|
new_notification: false,
|
|
6
6
|
async init(){
|
|
7
|
-
await this.check_new_notifications()
|
|
7
|
+
await this.check_new_notifications();
|
|
8
8
|
setInterval(await this.check_new_notifications, 30000);
|
|
9
9
|
},
|
|
10
10
|
|
|
11
11
|
async check_new_notifications(){
|
|
12
|
-
let url = '{% url "django_spire:notification:app:json:check_new" %}'
|
|
13
|
-
let response = await django_glue_fetch(url)
|
|
14
|
-
this.new_notification = response.has_new_notifications
|
|
12
|
+
let url = '{% url "django_spire:notification:app:json:check_new" %}';
|
|
13
|
+
let response = await django_glue_fetch(url);
|
|
14
|
+
this.new_notification = response.has_new_notifications;
|
|
15
15
|
},
|
|
16
16
|
|
|
17
17
|
async render_dropdown(){
|
|
18
18
|
let dropdown_content = new ViewGlue(
|
|
19
19
|
url='{% url "django_spire:notification:app:template:notification_dropdown" %}',
|
|
20
20
|
shared_payload={app_notification_list_url: '{{ app_notification_list_url|default:"" }}'}
|
|
21
|
-
)
|
|
22
|
-
await dropdown_content.render_inner($refs.spire_notification_dropdown_content)
|
|
23
|
-
await this.mark_notifications_as_viewed()
|
|
21
|
+
);
|
|
22
|
+
await dropdown_content.render_inner($refs.spire_notification_dropdown_content);
|
|
23
|
+
await this.mark_notifications_as_viewed();
|
|
24
24
|
},
|
|
25
25
|
|
|
26
26
|
async mark_notifications_as_viewed(){
|
|
27
|
-
let response = await django_glue_fetch('{% url "django_spire:notification:app:json:set_viewed" %}')
|
|
28
|
-
this.new_notification = false
|
|
27
|
+
let response = await django_glue_fetch('{% url "django_spire:notification:app:json:set_viewed" %}');
|
|
28
|
+
this.new_notification = false;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
}"
|
|
@@ -44,5 +44,5 @@
|
|
|
44
44
|
{% block dropdown_position %}top-100 end-50{% endblock %}
|
|
45
45
|
|
|
46
46
|
{% block dropdown_content %}
|
|
47
|
-
<div x-ref="spire_notification_dropdown_content"></div>
|
|
47
|
+
<div :style="{ width: window.innerWidth < 768 ? (window.innerWidth / 2).toFixed(0) + 'px' : '350px' }" x-ref="spire_notification_dropdown_content"></div>
|
|
48
48
|
{% endblock %}
|
|
@@ -1,33 +1 @@
|
|
|
1
|
-
|
|
2
|
-
:style="window.innerWidth < 768
|
|
3
|
-
? 'width: 350px; position: fixed; top: 5%; left: 50%; transform: translateX(-50%);'
|
|
4
|
-
: 'width: 350px'"
|
|
5
|
-
class="container border rounded-2 bg-app-layer-two"
|
|
6
|
-
>
|
|
7
|
-
<div class="col-12 w-100">
|
|
8
|
-
<h6 class="mt-2 ms-2 text-center">Notifications</h6>
|
|
9
|
-
<hr class="m-0">
|
|
10
|
-
</div>
|
|
11
|
-
|
|
12
|
-
<div
|
|
13
|
-
style="max-height: 300px !important;"
|
|
14
|
-
class="text-start overflow-y-scroll overflow-x-hidden"
|
|
15
|
-
>
|
|
16
|
-
{% for app_notification in app_notification_list %}
|
|
17
|
-
<div class="col-12">
|
|
18
|
-
{% include app_notification.template %}
|
|
19
|
-
</div>
|
|
20
|
-
{% empty %}
|
|
21
|
-
No Notifications
|
|
22
|
-
{% endfor %}
|
|
23
|
-
</div>
|
|
24
|
-
<div class="col-12">
|
|
25
|
-
<hr class="m-0">
|
|
26
|
-
</div>
|
|
27
|
-
<div class="col-12 fs-7 text-center py-1">
|
|
28
|
-
{% if not app_notification_list_url %}
|
|
29
|
-
{% url "django_spire:notification:app:page:list" as app_notification_list_url %}
|
|
30
|
-
{% endif %}
|
|
31
|
-
<a href='{{ app_notification_list_url }}' class="text-decoration-none text-app-secondary my-2">View All</a>
|
|
32
|
-
</div>
|
|
33
|
-
</div>
|
|
1
|
+
{% include 'django_spire/notification/app/scroll/container/dropdown_container.html' with endpoint=notification_endpoint container_height='300px' batch_size=10 %}
|
django_spire/notification/app/templates/django_spire/notification/app/item/notification_item.html
CHANGED
|
@@ -1,25 +1,26 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
{% extends 'django_spire/item/infinite_scroll_item.html' %}
|
|
2
|
+
|
|
3
|
+
{% block item_content %}
|
|
4
|
+
{% if app_notification.notification.url %}
|
|
5
|
+
<a href="{{ app_notification.notification.url }}"
|
|
6
|
+
{% else %}
|
|
7
|
+
<div
|
|
8
|
+
{% endif %}
|
|
9
|
+
class="row bg-app-layer-two-hover rounded m-0"
|
|
10
|
+
>
|
|
11
|
+
<div class="col-12">
|
|
12
|
+
<span class="fs-7 fw-semibold">{{ app_notification.notification.title }}</span>
|
|
13
|
+
{% if not app_notification.viewed %}
|
|
14
|
+
{% include 'django_spire/badge/primary_badge.html' with badge_text='New' %}
|
|
15
|
+
{% endif %}
|
|
16
|
+
</div>
|
|
17
|
+
<div class="col-12">
|
|
18
|
+
<span class="fs--1">{{ app_notification.notification.body }}</span><br>
|
|
19
|
+
<span class="text-app-secondary text-muted fs--2">{{ app_notification.verbose_time_since_delivered }}</span>
|
|
20
|
+
</div>
|
|
21
|
+
{% if app_notification.notification.url %}
|
|
22
|
+
</a>
|
|
23
|
+
{% else %}
|
|
21
24
|
</div>
|
|
22
|
-
{% if not forloop.last %}
|
|
23
|
-
<hr class="m-0 mt-1">
|
|
24
25
|
{% endif %}
|
|
25
|
-
|
|
26
|
+
{% endblock %}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{% extends 'django_spire/page/full_page.html' %}
|
|
2
2
|
|
|
3
3
|
{% block full_page_content %}
|
|
4
|
-
{% include "django_spire/
|
|
4
|
+
{% include "django_spire/card/infinite_scroll_card.html" with card_title='Notifications' endpoint=notification_endpoint scroll_height='55vh' %}
|
|
5
5
|
{% endblock %}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{% extends 'django_spire/infinite_scroll/base.html' %}
|
|
2
|
+
|
|
3
|
+
{% block scroll_wrapper_class %}bg-app-layer-two rounded-2{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block scroll_container %}
|
|
6
|
+
<div class="row">
|
|
7
|
+
<div class="col-12">
|
|
8
|
+
<div
|
|
9
|
+
:style="{
|
|
10
|
+
maxHeight: '{{ container_height|default:'300px' }}',
|
|
11
|
+
overflowX: 'hidden',
|
|
12
|
+
overflowY: 'auto',
|
|
13
|
+
overscrollBehavior: 'contain',
|
|
14
|
+
WebkitOverflowScrolling: 'touch',
|
|
15
|
+
maxWidth: window.innerWidth < 768 ? (window.innerWidth / 2).toFixed(0) + 'px' : '350px',
|
|
16
|
+
}"
|
|
17
|
+
x-ref="scroll_container"
|
|
18
|
+
:data-scroll-id="scroll_id"
|
|
19
|
+
>
|
|
20
|
+
<div x-ref="content_container">
|
|
21
|
+
{% block scroll_content %}{% endblock %}
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<div style="height: 10px;" x-ref="infinite_scroll_trigger"></div>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<template x-if="show_loading && !is_refreshing">
|
|
28
|
+
<div
|
|
29
|
+
class="position-absolute d-flex justify-content-center align-items-center"
|
|
30
|
+
style="top: 0; left: 0; right: 0; bottom: 0; background: color-mix(in srgb, var(--app-layer-one) 85%, transparent);"
|
|
31
|
+
>
|
|
32
|
+
<div class="spinner-border text-app-primary" role="status"></div>
|
|
33
|
+
</div>
|
|
34
|
+
</template>
|
|
35
|
+
</div>
|
|
36
|
+
</div>
|
|
37
|
+
{% endblock %}
|
|
38
|
+
|
|
39
|
+
{% block scroll_footer %}
|
|
40
|
+
<div class="row text-center border-top border-dark-subtle m-0">
|
|
41
|
+
<div class="col-12">
|
|
42
|
+
<span class="fs-7 text-app-secondary">
|
|
43
|
+
Showing <span x-text="loaded_count"></span> of <span x-text="total_count"></span> {{ footer_label|default:'items' }}
|
|
44
|
+
</span>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div class="col-12 fs-7 text-center py-1">
|
|
48
|
+
{% if not app_notification_list_url %}
|
|
49
|
+
{% url "django_spire:notification:app:page:list" as app_notification_list_url %}
|
|
50
|
+
{% endif %}
|
|
51
|
+
<a href='{{ app_notification_list_url }}' class="text-decoration-none text-app-secondary my-2">View All</a>
|
|
52
|
+
</div>
|
|
53
|
+
</div>
|
|
54
|
+
{% endblock %}
|
|
@@ -8,7 +8,14 @@ from django_spire.notification.app.views import template_views
|
|
|
8
8
|
app_name = 'django_spire_notification'
|
|
9
9
|
|
|
10
10
|
urlpatterns = [
|
|
11
|
-
path(
|
|
11
|
+
path(
|
|
12
|
+
'notficiation/scroll/items',
|
|
13
|
+
template_views.notification_infinite_scroll_view,
|
|
14
|
+
name='scroll_items'
|
|
15
|
+
),
|
|
16
|
+
path(
|
|
17
|
+
'notficiation/dropdown/template/',
|
|
12
18
|
template_views.notification_dropdown_template_view,
|
|
13
|
-
name='notification_dropdown'
|
|
19
|
+
name='notification_dropdown'
|
|
20
|
+
)
|
|
14
21
|
]
|
|
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING
|
|
4
4
|
|
|
5
5
|
from django.contrib.auth.decorators import login_required
|
|
6
|
+
from django.urls import reverse
|
|
6
7
|
|
|
7
8
|
from django_spire.contrib.generic_views import portal_views
|
|
8
9
|
|
|
@@ -15,18 +16,9 @@ if TYPE_CHECKING:
|
|
|
15
16
|
|
|
16
17
|
@login_required()
|
|
17
18
|
def app_notification_list_view(request: WSGIRequest) -> TemplateResponse:
|
|
18
|
-
app_notification_list = (
|
|
19
|
-
AppNotification.objects.active()
|
|
20
|
-
.is_sent()
|
|
21
|
-
.annotate_is_viewed_by_user(request.user)
|
|
22
|
-
.select_related('notification')
|
|
23
|
-
.distinct()
|
|
24
|
-
.ordered_by_priority_and_sent_datetime()
|
|
25
|
-
)
|
|
26
|
-
|
|
27
19
|
return portal_views.list_view(
|
|
28
20
|
request,
|
|
29
|
-
context_data={'
|
|
21
|
+
context_data={'notification_endpoint': reverse('django_spire:notification:app:template:scroll_items')},
|
|
30
22
|
model=AppNotification,
|
|
31
23
|
template='django_spire/notification/app/page/list_page.html'
|
|
32
24
|
)
|
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
|
-
|
|
5
4
|
from typing import TYPE_CHECKING
|
|
6
5
|
|
|
7
6
|
from django.contrib.auth.models import AnonymousUser
|
|
8
|
-
from django.
|
|
7
|
+
from django.urls import reverse
|
|
9
8
|
|
|
9
|
+
from django_spire.contrib.generic_views.portal_views import infinite_scrolling_view
|
|
10
|
+
from django.template.response import TemplateResponse
|
|
10
11
|
from django_spire.notification.app.models import AppNotification
|
|
11
12
|
|
|
13
|
+
|
|
12
14
|
if TYPE_CHECKING:
|
|
13
15
|
from django.core.handlers.wsgi import WSGIRequest
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
def notification_dropdown_template_view(request: WSGIRequest) -> TemplateResponse:
|
|
17
|
+
def notification_infinite_scroll_view(request: WSGIRequest) -> TemplateResponse:
|
|
17
18
|
if isinstance(request.user, AnonymousUser):
|
|
18
|
-
|
|
19
|
+
notifications = []
|
|
19
20
|
|
|
20
21
|
else:
|
|
21
|
-
|
|
22
|
+
notifications = (
|
|
22
23
|
AppNotification.objects.active()
|
|
23
24
|
.is_sent()
|
|
24
25
|
.annotate_is_viewed_by_user(request.user)
|
|
@@ -29,11 +30,25 @@ def notification_dropdown_template_view(request: WSGIRequest) -> TemplateRespons
|
|
|
29
30
|
|
|
30
31
|
body_data = json.loads(request.body.decode('utf-8'))
|
|
31
32
|
|
|
33
|
+
return infinite_scrolling_view(
|
|
34
|
+
request,
|
|
35
|
+
queryset=notifications,
|
|
36
|
+
queryset_name='notifications',
|
|
37
|
+
context_data={
|
|
38
|
+
'app_notification_list_url': body_data.get('app_notification_list_url'),
|
|
39
|
+
},
|
|
40
|
+
template='django_spire/notification/app/scroll/item/items.html',
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def notification_dropdown_template_view(request: WSGIRequest) -> TemplateResponse:
|
|
45
|
+
body_data = json.loads(request.body.decode('utf-8'))
|
|
46
|
+
|
|
32
47
|
return TemplateResponse(
|
|
33
48
|
request,
|
|
34
49
|
context={
|
|
35
|
-
'app_notification_list': app_notification_list,
|
|
36
50
|
'app_notification_list_url': body_data.get('app_notification_list_url'),
|
|
51
|
+
'notification_endpoint': reverse('django_spire:notification:app:template:scroll_items')
|
|
37
52
|
},
|
|
38
53
|
template='django_spire/notification/app/dropdown/notification_dropdown_content.html'
|
|
39
54
|
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-spire
|
|
3
|
-
Version: 0.24.
|
|
3
|
+
Version: 0.24.3
|
|
4
4
|
Summary: A project for Django Spire
|
|
5
5
|
Author-email: Brayden Carlson <braydenc@stratusadv.com>, Nathan Johnson <nathanj@stratusadv.com>
|
|
6
6
|
License: Copyright (c) 2025 Stratus Advanced Technologies and Contributors.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
django_spire/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
2
|
django_spire/conf.py,sha256=3oUB1mtgHRjvbJsxfQWG5uL1KUP9uGig3zdP2dZphe8,942
|
|
3
|
-
django_spire/consts.py,sha256=
|
|
3
|
+
django_spire/consts.py,sha256=pGfQC8jGUV7BYSAN3PnjA-Ur64Gr7KGC5UkkXkKWfOk,171
|
|
4
4
|
django_spire/exceptions.py,sha256=M7buFvm-K4lK09pH5fVcZ-MxsDIzdpEJBF33Xss5bSw,289
|
|
5
5
|
django_spire/settings.py,sha256=Pr98O2Na5Cv9YXs5y8c2CvGYv1szmXED8RJVT5q2-W4,1164
|
|
6
6
|
django_spire/urls.py,sha256=wQx6R-nXx69MeOF-WmDxcEUM5WmUHGplbY5uZ_HnDp8,703
|
|
@@ -339,11 +339,17 @@ django_spire/comment/tests/test_querysets.py,sha256=01nhMO9Wpi_flkSA41d4l5Il8MaS
|
|
|
339
339
|
django_spire/comment/tests/test_utils.py,sha256=x5WQvkXOoult5V4RIUjSp4qK6Q-wEgqVm-BnyL1qWyU,3371
|
|
340
340
|
django_spire/contrib/__init__.py,sha256=Cw4KTzAMKPQucNRy541gYSe9ph_JtA1rRrILaYVsb1w,100
|
|
341
341
|
django_spire/contrib/utils.py,sha256=IuNcM6ft6whrkPuoDkB100JqUYGZomgXMnTZeCoUvSQ,168
|
|
342
|
+
django_spire/contrib/admin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
343
|
+
django_spire/contrib/admin/admin.py,sha256=zb4bnfum7ZpfnWI17BLDQ3LHM4nTTfNv1Sr13Xbb0rs,4281
|
|
342
344
|
django_spire/contrib/breadcrumb/__init__.py,sha256=Cw4KTzAMKPQucNRy541gYSe9ph_JtA1rRrILaYVsb1w,100
|
|
343
345
|
django_spire/contrib/breadcrumb/apps.py,sha256=8rDWD4nAyAdH0QTHHOqXDPv6tVJTLilJeRV0-YBH2Uw,248
|
|
344
346
|
django_spire/contrib/breadcrumb/breadcrumbs.py,sha256=H_Cpcsli9X4GsSQ1xa_ylKWCJxw9uzUj0_RxP61hOGs,2128
|
|
345
347
|
django_spire/contrib/breadcrumb/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
346
348
|
django_spire/contrib/breadcrumb/tests/test_breadcrumbs.py,sha256=gGdmR8m4jH-raAHA7popA1vX1-I4hCCWBNUi7kLdleM,5864
|
|
349
|
+
django_spire/contrib/choices/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
350
|
+
django_spire/contrib/choices/choices.py,sha256=HXdQakkQ5QTaWsYLFNo8efyBsaXZGBIqLPUIJ4IItN0,185
|
|
351
|
+
django_spire/contrib/choices/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
352
|
+
django_spire/contrib/choices/tests/test_choices.py,sha256=fkm4syq7CY5o9wz8XvqAfP5JN_iNmHhWLx85VhfpIdY,2038
|
|
347
353
|
django_spire/contrib/constructor/__init__.py,sha256=lcBPE05Oa4wNmZRGNSscJ9zPxYuPobPmj8O1Dm78h1c,338
|
|
348
354
|
django_spire/contrib/constructor/constructor.py,sha256=QjOBGMI4vFR-4vnGNcYlf9W9NBIa2vB6-Y8BPKQmgGI,2973
|
|
349
355
|
django_spire/contrib/constructor/django_model_constructor.py,sha256=KSaazWoQaaOJ6goKDz_J-OZuPRSLLgpqEBUa4xUPo-k,1145
|
|
@@ -712,12 +718,13 @@ django_spire/core/templates/django_spire/container/form_container.html,sha256=FZ
|
|
|
712
718
|
django_spire/core/templates/django_spire/container/infinite_scroll_container.html,sha256=xaHlMZ4yLKhZCvbbRHUAHeEZOFKJf-s8eGUN_y9FNJE,2558
|
|
713
719
|
django_spire/core/templates/django_spire/dropdown/dropdown.html,sha256=O3gUp2YZm_2g0Qd-odFOnxfBkRc4c4af4zTbyGibSU0,528
|
|
714
720
|
django_spire/core/templates/django_spire/dropdown/ellipsis_dropdown.html,sha256=6DrFtcvfCnegs_gLfDZDkEGYb6ZpJ85clKQWckiTH00,1431
|
|
715
|
-
django_spire/core/templates/django_spire/dropdown/ellipsis_modal_dropdown.html,sha256=
|
|
721
|
+
django_spire/core/templates/django_spire/dropdown/ellipsis_modal_dropdown.html,sha256=0lbHn6vn2ip_Sia0khQ8k3Vp0nsYUIZHa-XbQXnsB9Q,1540
|
|
716
722
|
django_spire/core/templates/django_spire/dropdown/ellipsis_table_dropdown.html,sha256=_Rf9hKmOchDdE00NU5Aq8pvVb4Q4vPxlIWf1NWOUGg8,935
|
|
717
723
|
django_spire/core/templates/django_spire/dropdown/element/dropdown_link_element.html,sha256=KB4KRolkw5zOImRkXcVXxIH_bxsN648gQRnRDijgvB4,1005
|
|
718
724
|
django_spire/core/templates/django_spire/dropdown/element/ellipsis_dropdown_modal_link_element.html,sha256=WC2ruVWh5c_5hYZmrKFYrfJXGW6r5z7KGu8_X-vPvhs,438
|
|
719
|
-
django_spire/core/templates/django_spire/element/attribute_element.html,sha256=
|
|
725
|
+
django_spire/core/templates/django_spire/element/attribute_element.html,sha256=0cftkHl-b4ePrQioCXMwpyxW2RlU6AVVJ4OyhyCAzMM,1778
|
|
720
726
|
django_spire/core/templates/django_spire/element/breadcrumb_element.html,sha256=NUoLKKKIPDIF8G8mIM1dVF5lhbqmRcOAWLA6qw9o2x8,598
|
|
727
|
+
django_spire/core/templates/django_spire/element/copy_to_clipboard_element.html,sha256=5YG6sdjIFu0CvssEs5HSCihvrLc7bTeIl3B_wsnk3AA,722
|
|
721
728
|
django_spire/core/templates/django_spire/element/divider_element.html,sha256=EjpdMPQZmefh0BfUGUQvazFR5eF0RHQ5pqmYD253GMc,302
|
|
722
729
|
django_spire/core/templates/django_spire/element/grabber_element.html,sha256=ZH6abnf8p4jMRSdjWNsbRqNBuM1yMJF_EKNLh7c4pj8,198
|
|
723
730
|
django_spire/core/templates/django_spire/element/no_data_element.html,sha256=-WSsQZVh3T3NROSfQo8U-ZPxt8nxmDggp3Cz_gbeNL4,145
|
|
@@ -1220,12 +1227,13 @@ django_spire/notification/app/processor.py,sha256=qV4nK37vMpoL9CDEgB8diAG1oBQrEt
|
|
|
1220
1227
|
django_spire/notification/app/querysets.py,sha256=gEi26CfQlLkSGHyWsKFvRbxcOmoCAIjcbe_SqCHr0ns,1821
|
|
1221
1228
|
django_spire/notification/app/migrations/0001_initial.py,sha256=ecwU6tJ5qZBKl3uAgpv_L74OUepxYgLvr3ssRYSfxxs,1439
|
|
1222
1229
|
django_spire/notification/app/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1223
|
-
django_spire/notification/app/templates/django_spire/notification/app/
|
|
1224
|
-
django_spire/notification/app/templates/django_spire/notification/app/dropdown/
|
|
1225
|
-
django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown_content.html,sha256=Lcy1laXb9HWrr0TEgCrK8PEoE0-hnCPrIa7OuZrSkKI,1113
|
|
1230
|
+
django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown.html,sha256=zIsIQ-mEgwzIutI-Jp4Z5vIb9426aAGH9ksNJ0MU4Bk,2034
|
|
1231
|
+
django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown_content.html,sha256=iiRG0us7lIS3q_CPM3h7Uyy_VegNH1oUxQM11FpdJoU,162
|
|
1226
1232
|
django_spire/notification/app/templates/django_spire/notification/app/element/notification_bell.html,sha256=fwXOY5yi76jQOx5wZQky2mfAs0DFIOwBDwJwOZCdw-8,440
|
|
1227
|
-
django_spire/notification/app/templates/django_spire/notification/app/item/notification_item.html,sha256=
|
|
1228
|
-
django_spire/notification/app/templates/django_spire/notification/app/page/list_page.html,sha256=
|
|
1233
|
+
django_spire/notification/app/templates/django_spire/notification/app/item/notification_item.html,sha256=KagHR9pTBWPxHFEIv9TLhWxT_jsGQdIaajqPXCFSaLQ,952
|
|
1234
|
+
django_spire/notification/app/templates/django_spire/notification/app/page/list_page.html,sha256=vE8eDZ8UmQWRI3tqYYAW3FJAIHYkAFcPr86B7ujw_qc,244
|
|
1235
|
+
django_spire/notification/app/templates/django_spire/notification/app/scroll/container/dropdown_container.html,sha256=XCMEJ_XjjI_wwIjUHVK8IB24JIGsJmWxM6Kc98_xBco,2207
|
|
1236
|
+
django_spire/notification/app/templates/django_spire/notification/app/scroll/item/items.html,sha256=LsNhh5n_x_IaRuvDIHRbAHqyBmwfzXfsa12ycZO4XJA,145
|
|
1229
1237
|
django_spire/notification/app/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1230
1238
|
django_spire/notification/app/tests/factories.py,sha256=V0NHRQ-cY-HkSN4jegEFoNk6pVdrTJL6YaQ7hwtxIO8,1280
|
|
1231
1239
|
django_spire/notification/app/tests/test_apps.py,sha256=xo94F1iYXoHmkDkGlDMcRpxXoF0U83YWoK2Zm5kHgD4,863
|
|
@@ -1238,11 +1246,11 @@ django_spire/notification/app/tests/test_views/test_page_views.py,sha256=VIcCcwy
|
|
|
1238
1246
|
django_spire/notification/app/urls/__init__.py,sha256=Scogp1Oni-8fJU80cffWj0UoA43tfRaZ43PgjofrW2Y,406
|
|
1239
1247
|
django_spire/notification/app/urls/json_urls.py,sha256=r-ly6mxXcByH4x54gkTC1-AmwVVA7HiQd-a_5lnL1uU,427
|
|
1240
1248
|
django_spire/notification/app/urls/page_urls.py,sha256=K2jwK-zoY0N-kcQ4tsYmUZtnKm5m0-b2KHv4bCU2xC0,302
|
|
1241
|
-
django_spire/notification/app/urls/template_urls.py,sha256=
|
|
1249
|
+
django_spire/notification/app/urls/template_urls.py,sha256=EpMmjpnyUVU5e5-W5zXfNRm03_FNPx32PtOrXfr0Qg8,485
|
|
1242
1250
|
django_spire/notification/app/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1243
1251
|
django_spire/notification/app/views/json_views.py,sha256=WhdbbeZyzf408vHMUgMQ9ERbawQk085A-Nlrxzve56U,1466
|
|
1244
|
-
django_spire/notification/app/views/page_views.py,sha256=
|
|
1245
|
-
django_spire/notification/app/views/template_views.py,sha256=
|
|
1252
|
+
django_spire/notification/app/views/page_views.py,sha256=IojY_oLWrcgOfUaWn2UERkEU3Ka3Ie9nAqCDv_Jb4S8,782
|
|
1253
|
+
django_spire/notification/app/views/template_views.py,sha256=h9UgUXaLxHzLwZhdduVgKHIZYelFrYSupNlQumis3uw,1775
|
|
1246
1254
|
django_spire/notification/email/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1247
1255
|
django_spire/notification/email/admin.py,sha256=KY7AbmQkc07HeQcpiTgcyohemrUBY5eHpniUkD1mLeU,316
|
|
1248
1256
|
django_spire/notification/email/apps.py,sha256=H3ukWNu1aYgNEYsXQwH6WlIAvF0wGl4UBdOWXF-UBm0,456
|
|
@@ -1401,8 +1409,8 @@ django_spire/theme/urls/page_urls.py,sha256=Oak3x_xwQEb01NKdrsB1nk6yPaOEnheuSG1m
|
|
|
1401
1409
|
django_spire/theme/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1402
1410
|
django_spire/theme/views/json_views.py,sha256=PWwVTaty0BVGbj65L5cxex6JNhc-xVAI_rEYjbJWqEM,1893
|
|
1403
1411
|
django_spire/theme/views/page_views.py,sha256=WenjOa6Welpu3IMolY56ZwBjy4aK9hpbiMNuygjAl1A,3922
|
|
1404
|
-
django_spire-0.24.
|
|
1405
|
-
django_spire-0.24.
|
|
1406
|
-
django_spire-0.24.
|
|
1407
|
-
django_spire-0.24.
|
|
1408
|
-
django_spire-0.24.
|
|
1412
|
+
django_spire-0.24.3.dist-info/licenses/LICENSE.md,sha256=ZAeCT76WvaoEZE9xPhihyWjTwH0wQZXQmyRsnV2VPFs,1091
|
|
1413
|
+
django_spire-0.24.3.dist-info/METADATA,sha256=cNiLXLnSnLFw9Bw7YbGw-FcKJPNaziT58JqIagte4lU,5127
|
|
1414
|
+
django_spire-0.24.3.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
1415
|
+
django_spire-0.24.3.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
|
|
1416
|
+
django_spire-0.24.3.dist-info/RECORD,,
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{% extends 'django_spire/card/title_card.html' %}
|
|
2
|
-
|
|
3
|
-
{% block card_title %}
|
|
4
|
-
Notification List
|
|
5
|
-
{% endblock %}
|
|
6
|
-
|
|
7
|
-
{% block card_title_content %}
|
|
8
|
-
{% for app_notification in notification_list %}
|
|
9
|
-
{% include app_notification.template %}
|
|
10
|
-
{% endfor %}
|
|
11
|
-
{% endblock %}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|