django-spire 0.24.1__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.
Files changed (27) hide show
  1. django_spire/consts.py +1 -1
  2. django_spire/contrib/admin/__init__.py +0 -0
  3. django_spire/contrib/admin/admin.py +140 -0
  4. django_spire/contrib/choices/__init__.py +0 -0
  5. django_spire/contrib/choices/choices.py +9 -0
  6. django_spire/contrib/choices/tests/__init__.py +0 -0
  7. django_spire/contrib/choices/tests/test_choices.py +62 -0
  8. django_spire/contrib/service/django_model_service.py +65 -47
  9. django_spire/contrib/service/tests/test_service.py +13 -66
  10. django_spire/core/templates/django_spire/dropdown/ellipsis_modal_dropdown.html +15 -3
  11. django_spire/core/templates/django_spire/element/attribute_element.html +7 -0
  12. django_spire/core/templates/django_spire/element/copy_to_clipboard_element.html +31 -0
  13. django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown.html +10 -10
  14. django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown_content.html +1 -33
  15. django_spire/notification/app/templates/django_spire/notification/app/item/notification_item.html +24 -23
  16. django_spire/notification/app/templates/django_spire/notification/app/page/list_page.html +1 -1
  17. django_spire/notification/app/templates/django_spire/notification/app/scroll/container/dropdown_container.html +54 -0
  18. django_spire/notification/app/templates/django_spire/notification/app/scroll/item/items.html +5 -0
  19. django_spire/notification/app/urls/template_urls.py +9 -2
  20. django_spire/notification/app/views/page_views.py +2 -10
  21. django_spire/notification/app/views/template_views.py +22 -7
  22. {django_spire-0.24.1.dist-info → django_spire-0.24.3.dist-info}/METADATA +1 -1
  23. {django_spire-0.24.1.dist-info → django_spire-0.24.3.dist-info}/RECORD +26 -18
  24. {django_spire-0.24.1.dist-info → django_spire-0.24.3.dist-info}/WHEEL +1 -1
  25. django_spire/notification/app/templates/django_spire/notification/app/card/list_card.html +0 -11
  26. {django_spire-0.24.1.dist-info → django_spire-0.24.3.dist-info}/licenses/LICENSE.md +0 -0
  27. {django_spire-0.24.1.dist-info → django_spire-0.24.3.dist-info}/top_level.txt +0 -0
django_spire/consts.py CHANGED
@@ -1,4 +1,4 @@
1
- __VERSION__ = '0.24.1'
1
+ __VERSION__ = '0.24.3'
2
2
 
3
3
  MAINTENANCE_MODE_SETTINGS_NAME = 'MAINTENANCE_MODE'
4
4
 
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
@@ -0,0 +1,9 @@
1
+ import json
2
+
3
+ from django.db.models import TextChoices
4
+
5
+
6
+ class SpireTextChoices(TextChoices):
7
+ @classmethod
8
+ def to_glue_choices(cls) -> str:
9
+ return json.dumps(cls.choices)
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']]
@@ -3,15 +3,18 @@ from __future__ import annotations
3
3
  import logging
4
4
 
5
5
  from abc import ABC
6
+ from itertools import chain
6
7
  from typing import Generic, TypeVar
7
8
 
9
+ from django.contrib.contenttypes.fields import GenericForeignKey
8
10
  from django.db import transaction
9
- from django.db.models import Model
11
+ from django.db.models import Model, Field, AutoField, ForeignObjectRel, FileField
10
12
 
11
13
  from django_spire.contrib.constructor.django_model_constructor import BaseDjangoModelConstructor
12
14
  from django_spire.contrib.service.exceptions import ServiceError
13
15
 
14
16
 
17
+
15
18
  log = logging.getLogger(__name__)
16
19
 
17
20
  TypeDjangoModel_co = TypeVar('TypeDjangoModel_co', bound=Model, covariant=True)
@@ -22,63 +25,78 @@ class BaseDjangoModelService(
22
25
  ABC,
23
26
  Generic[TypeDjangoModel_co]
24
27
  ):
25
- def _get_concrete_fields(self) -> dict:
26
- return {
27
- field.name: field
28
- for field in self.obj._meta.get_fields()
29
- if field.concrete and not field.many_to_many and not field.one_to_many
30
- }
31
-
32
- def _get_touched_fields(self, concrete_fields: dict, **field_data: dict) -> list[str]:
33
- foreign_key_id_aliases = {f"{name}_id" for name, field in concrete_fields.items() if field.many_to_one}
34
- allowed = set(concrete_fields) | foreign_key_id_aliases
35
-
36
- touched_fields: list[str] = []
37
-
38
- for field, value in field_data.items():
39
- if field not in allowed:
40
- log.warning(f'Field {field!r} is not valid for {self.obj.__class__.__name__}')
41
- continue
28
+ def _set_non_m2m_fields(self, **field_data: dict):
29
+ model_fields = self.obj._meta.fields
42
30
 
43
- model_field = concrete_fields.get(field.removesuffix("_id"), None)
31
+ file_field_list = []
32
+ updated_fields = []
44
33
 
45
- if model_field and (getattr(model_field, 'auto_created', False) or not model_field.editable):
34
+ for field in model_fields:
35
+ if isinstance(field, AutoField) or field.name not in field_data:
46
36
  continue
47
37
 
48
- setattr(self.obj, field, value)
49
-
50
- touched_fields.append(field.removesuffix('_id'))
51
-
52
- return touched_fields
53
-
54
- def validate_model_obj(self, **field_data: dict) -> list[str]:
55
- concrete_fields = self._get_concrete_fields()
56
- touched_fields = self._get_touched_fields(concrete_fields, **field_data)
38
+ # Defer saving file-type fields until after the other fields, so a
39
+ # callable upload_to can use the values from other fields (from django's construct_instance).
40
+ if isinstance(field, FileField):
41
+ file_field_list.append(field)
42
+ updated_fields.append(field.name)
43
+ else:
44
+ field.save_form_data(self.obj, field_data[field.name])
45
+ updated_fields.append(field.name)
46
+
47
+ # Update foreign key id aliases in field_data for
48
+ # related fields that weren't already updated above
49
+ foreign_key_id_aliases = [
50
+ f"{field.name}_id" for field in model_fields
51
+ if f"{field.name}_id" in field_data and field.many_to_one and field.name not in updated_fields
52
+ ]
53
+
54
+ for field_name in foreign_key_id_aliases:
55
+ setattr(self.obj, field_name, field_data[field_name])
56
+
57
+ # Update file fields deferred from earlier
58
+ for field in file_field_list:
59
+ field.save_form_data(self.obj, field_data[field.name])
60
+
61
+ def _set_m2m_fields(self, **field_data):
62
+ model_meta = self.obj._meta
63
+
64
+ for field in chain(model_meta.many_to_many, model_meta.private_fields):
65
+ if not hasattr(field, "save_form_data"):
66
+ continue
67
+ if field.name in field_data:
68
+ field.save_form_data(self.obj, field_data[field.name])
57
69
 
58
- exclude = [field for field in concrete_fields if field not in touched_fields]
59
- self.obj.full_clean(exclude=exclude)
70
+ @transaction.atomic
71
+ def save_model_obj(self, **field_data: dict | None) -> tuple[Model, bool]:
72
+ """
73
+ This is the core service method for saving a Django model object with field data provided via kwargs.
74
+ It will update the object with the given kwargs and handle any upstream attribute changes that were applied
75
+ directly to the model instance (i.e. it can also be called without any kwargs, similar to `Model.save()`).
60
76
 
61
- return touched_fields
77
+ Its purpose is to run extra operations related to the model instance that need to run each time
78
+ the model is saved - it is meant to replace the need for overriding `BaseModelForm.save()` or `Model.save()`.
62
79
 
63
- @transaction.atomic
64
- def save_model_obj(self, **field_data: dict) -> tuple[Model, bool]:
65
- new_model_obj_was_created = False
80
+ It is designed to emulate django's `BaseModelForm.save()` method as close as possible:
81
+ first, in `_set_non_m2m_fields`, it updates the fields on `self.obj` using logic similar to `django.forms.models.construct_instance`,
82
+ then it calls self.obj.save(), then updates the m2m fields on the object instance
83
+ using logic similar to django's `BaseModelForm._save_m2m()` method. In all cases,
84
+ it treats the incoming `field_data` exactly the same as `cleaned_data` is treated
85
+ in the django code that it is emulating, and therefore does not perform any validation
86
+ on the data or the model instance as it is assumed that field_data has already been validated upstream.
66
87
 
67
- if not field_data:
68
- message = f'Field data is required to save on {self.obj.__class__.__name__}'
69
- raise ServiceError(message)
88
+ Args:
89
+ **field_data:
70
90
 
71
- touched_fields = self.validate_model_obj(**field_data)
91
+ Returns:
92
+ tuple[Model, bool]
72
93
 
73
- if self.model_obj_is_new:
74
- new_model_obj_was_created = True
75
- self.obj.save()
94
+ """
76
95
 
77
- elif touched_fields:
78
- self.obj.save(update_fields=touched_fields)
96
+ new_model_obj_was_created = True if self.model_obj_is_new else False
79
97
 
80
- else:
81
- message = f'{self.obj.__class__.__name__} is not a new object or there was no touched fields to update.'
82
- log.warning(message)
98
+ self._set_non_m2m_fields(**field_data)
99
+ self.obj.save()
100
+ self._set_m2m_fields(**field_data)
83
101
 
84
102
  return self.obj, new_model_obj_was_created
@@ -37,66 +37,26 @@ class TestBaseDjangoModelService(TestCase):
37
37
  password='testpass' # noqa: S106
38
38
  )
39
39
 
40
- def test_get_concrete_fields_returns_dict(self) -> None:
40
+ def test_set_non_m2m_fields_sets_attribute(self) -> None:
41
41
  service = UserService(self.user)
42
-
43
- result = service._get_concrete_fields()
44
-
45
- assert isinstance(result, dict)
46
- assert 'username' in result
47
- assert 'email' in result
48
-
49
- def test_get_touched_fields_returns_list(self) -> None:
50
- service = UserService(self.user)
51
- concrete_fields = service._get_concrete_fields()
52
-
53
- result = service._get_touched_fields(concrete_fields, username='newuser')
54
-
55
- assert isinstance(result, list)
56
-
57
- def test_get_touched_fields_sets_attribute(self) -> None:
58
- service = UserService(self.user)
59
- concrete_fields = service._get_concrete_fields()
60
-
61
- service._get_touched_fields(concrete_fields, first_name='NewName')
42
+ service._set_non_m2m_fields(first_name='NewName')
62
43
 
63
44
  assert service.obj.first_name == 'NewName'
64
45
 
65
- def test_get_touched_fields_logs_warning_for_invalid_field(self) -> None:
66
- service = UserService(self.user)
67
- concrete_fields = service._get_concrete_fields()
68
-
69
- with patch('django_spire.contrib.service.django_model_service.log') as mock_log:
70
- service._get_touched_fields(concrete_fields, invalid_field='value')
71
-
72
- mock_log.warning.assert_called_once()
46
+ def test_set_non_m2m_fields_skips_auto_created_fields(self) -> None:
47
+ UserService(self.user)._set_non_m2m_fields(id=999)
73
48
 
74
- def test_get_touched_fields_skips_auto_created_fields(self) -> None:
75
- service = UserService(self.user)
76
- concrete_fields = service._get_concrete_fields()
49
+ assert self.user.id != 999
77
50
 
78
- result = service._get_touched_fields(concrete_fields, id=999)
51
+ def test_has_set_non_m2m_fields_method(self) -> None:
52
+ assert hasattr(BaseDjangoModelService, '_set_non_m2m_fields')
79
53
 
80
- assert 'id' not in result
81
-
82
- def test_has_get_concrete_fields_method(self) -> None:
83
- assert hasattr(BaseDjangoModelService, '_get_concrete_fields')
84
-
85
- def test_has_get_touched_fields_method(self) -> None:
86
- assert hasattr(BaseDjangoModelService, '_get_touched_fields')
54
+ def test_has_set_m2m_fields_method(self) -> None:
55
+ assert hasattr(BaseDjangoModelService, '_set_m2m_fields')
87
56
 
88
57
  def test_has_save_model_obj_method(self) -> None:
89
58
  assert hasattr(BaseDjangoModelService, 'save_model_obj')
90
59
 
91
- def test_has_validate_model_obj_method(self) -> None:
92
- assert hasattr(BaseDjangoModelService, 'validate_model_obj')
93
-
94
- def test_save_model_obj_raises_error_without_field_data(self) -> None:
95
- service = UserService(self.user)
96
-
97
- with pytest.raises(ServiceError, match='Field data is required'):
98
- service.save_model_obj()
99
-
100
60
  def test_save_model_obj_returns_tuple(self) -> None:
101
61
  service = UserService(self.user)
102
62
 
@@ -130,24 +90,11 @@ class TestBaseDjangoModelService(TestCase):
130
90
  assert created is True
131
91
  assert obj.pk is not None
132
92
 
133
- def test_save_model_obj_logs_warning_when_no_changes(self) -> None:
93
+ def test_save_model_updates_obj_with_previous_changes(self) -> None:
134
94
  service = UserService(self.user)
135
95
 
136
- with patch('django_spire.contrib.service.django_model_service.log') as mock_log:
137
- service.save_model_obj(id=self.user.id)
96
+ service.obj.first_name = 'new_test'
138
97
 
139
- mock_log.warning.assert_called_once()
140
-
141
- def test_validate_model_obj_returns_touched_fields(self) -> None:
142
- service = UserService(self.user)
143
-
144
- result = service.validate_model_obj(first_name='Valid')
145
-
146
- assert isinstance(result, list)
147
- assert 'first_name' in result
148
-
149
- def test_validate_model_obj_raises_validation_error(self) -> None:
150
- service = UserService(self.user)
98
+ service.save_model_obj(id=self.user.id)
151
99
 
152
- with pytest.raises(ValidationError):
153
- service.validate_model_obj(email='invalid-email')
100
+ assert service.obj.first_name == 'new_test'
@@ -12,14 +12,26 @@
12
12
 
13
13
  {% block dropdown_content %}
14
14
  {% if view_url %}
15
- {% include 'django_spire/dropdown/element/ellipsis_dropdown_modal_link_element.html' with view_url=view_url link_text='View' %}
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
- {% include 'django_spire/dropdown/element/ellipsis_dropdown_modal_link_element.html' with view_url=edit_url link_text='Edit' %}
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
- {% include 'django_spire/dropdown/element/ellipsis_dropdown_modal_link_element.html' with view_url=delete_url link_text='Delete' link_css='text-app-danger' %}
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
- <div
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 %}
@@ -1,25 +1,26 @@
1
- <div class="row pt-1 px-3">
2
- <div class="col-12">
3
- <div class="row bg-app-layer-two-hover rounded">
4
- {% if app_notification.notification.url %}
5
- <a href="{{ app_notification.notification.url }}" class="col-12">
6
- {% endif %}
7
- <div class="col-12">
8
- <div>
9
- <span class="fs-7 fw-semibold">{{ app_notification.notification.title }}</span>
10
- {% if not app_notification.viewed %}
11
- {% include 'django_spire/badge/primary_badge.html' with badge_text='New' %}
12
- {% endif %}
13
- </div>
14
- <span class="fs--1">{{ app_notification.notification.body }}</span><br>
15
- <span class="text-app-secondary text-muted fs--2">{{ app_notification.verbose_time_since_delivered }}</span>
16
- </div>
17
- {% if app_notification.notification.url %}
18
- </a>
19
- {% endif %}
20
- </div>
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
- </div>
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/notification/app/card/list_card.html" %}
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 %}
@@ -0,0 +1,5 @@
1
+ {% for app_notification in notifications %}
2
+ <div class="col-12 p-0">
3
+ {% include app_notification.template %}
4
+ </div>
5
+ {% endfor %}
@@ -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('notficiation/dropdown/template/',
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={'notification_list': app_notification_list},
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.template.response import TemplateResponse
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
- app_notification_list = []
19
+ notifications = []
19
20
 
20
21
  else:
21
- app_notification_list = (
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.1
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=A90ILXVJYixu3zNw9XSF3jhA9gKjIBnK88lrRZkpHRw,171
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
@@ -463,10 +469,10 @@ django_spire/contrib/seeding/tests/test_enums.py,sha256=tpysk3dKDtOXfD-0ebqSTBba
463
469
  django_spire/contrib/seeding/tests/test_intel.py,sha256=Hi6Jr91nBIPoLGt4KtRZO4-uGq5gTCMltJ8tHMgDCUs,980
464
470
  django_spire/contrib/seeding/tests/test_override.py,sha256=8eliLQBsQ7n1-ARqXLQdZOcrWDD8XhWfYoc_iaDaktM,1843
465
471
  django_spire/contrib/service/__init__.py,sha256=EB44rklqr317T2tDXDW5qocfQDE7b4f2UFjf7DAWj-0,215
466
- django_spire/contrib/service/django_model_service.py,sha256=gky7mEqKw1s7YNa2y2IhlKefBABIZAb_R1QmDVPHqfI,2824
472
+ django_spire/contrib/service/django_model_service.py,sha256=yQyNQZMfidRQKwDcPyXydF0yBSNIG1iSzEuJi-KKRXE,4188
467
473
  django_spire/contrib/service/exceptions.py,sha256=nIfh1kjeCN94eZE12zR4cGtvn3xKC2WAr_1KVakU28E,138
468
474
  django_spire/contrib/service/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
469
- django_spire/contrib/service/tests/test_service.py,sha256=p1d_L4P5TcPlD_imFHWmBMoToU-vJY4RGUrLxqbXhRE,5094
475
+ django_spire/contrib/service/tests/test_service.py,sha256=BlRY-UgQtZwylTj6nderZ-NjKB4Kj9o1iL4zWbGzKhw,3077
470
476
  django_spire/contrib/session/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
471
477
  django_spire/contrib/session/apps.py,sha256=zrDjGlxth4TvLZel-CSGt-CQa-S8Xs92sM-uq5m7d58,257
472
478
  django_spire/contrib/session/controller.py,sha256=4kOTDw4URLy3pCBWqWfwuOKEHzpjNT6iKVDU8-uH7Zk,3022
@@ -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=kUNcg-dSqr1wHjU_NijBAyCsa_7Z6_spVE5jytqYaME,898
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=2SOodzlW7aX3oIHic7g9oyFpRgUKu7dtZX6IEybSbrU,1487
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/card/list_card.html,sha256=lpBbGkZG3CcAP1E6ztm_GbW60lO-0CGnaeE2pfKRSmQ,275
1224
- django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown.html,sha256=IzliW2h7WrmnL8oRhMc_fcTD1_dYg8fLEni7JjfWyHM,1927
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=TwLbKo33MHbyKrBalQTTJ_vqXBLyygNduPe1GdzfN7s,1231
1228
- django_spire/notification/app/templates/django_spire/notification/app/page/list_page.html,sha256=9cEpntq9BQLWdIKa2cq77-KDFivzNPfPKnvQiuLl0fs,166
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=j6d3-yZu9NY47m69a-JhcnxXnM20x5Xi5qWE4A78zVk,331
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=Wt6vUoD3qS2b8K3JFj-YKbE81oF0F3Tfi_IEOoadKoA,961
1245
- django_spire/notification/app/views/template_views.py,sha256=4yNJdw-KBLRqK16HMx13geCHjb4wLBjrw8TAcXU9fiE,1173
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.1.dist-info/licenses/LICENSE.md,sha256=ZAeCT76WvaoEZE9xPhihyWjTwH0wQZXQmyRsnV2VPFs,1091
1405
- django_spire-0.24.1.dist-info/METADATA,sha256=5NmKsUBFFIY13Mj1ET72NddFFiKxCMfZ32oPCjE4Nt0,5127
1406
- django_spire-0.24.1.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
1407
- django_spire-0.24.1.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1408
- django_spire-0.24.1.dist-info/RECORD,,
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,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.10.1)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
@@ -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 %}