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.
Files changed (25) 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/core/templates/django_spire/dropdown/ellipsis_modal_dropdown.html +15 -3
  9. django_spire/core/templates/django_spire/element/attribute_element.html +7 -0
  10. django_spire/core/templates/django_spire/element/copy_to_clipboard_element.html +31 -0
  11. django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown.html +10 -10
  12. django_spire/notification/app/templates/django_spire/notification/app/dropdown/notification_dropdown_content.html +1 -33
  13. django_spire/notification/app/templates/django_spire/notification/app/item/notification_item.html +24 -23
  14. django_spire/notification/app/templates/django_spire/notification/app/page/list_page.html +1 -1
  15. django_spire/notification/app/templates/django_spire/notification/app/scroll/container/dropdown_container.html +54 -0
  16. django_spire/notification/app/templates/django_spire/notification/app/scroll/item/items.html +5 -0
  17. django_spire/notification/app/urls/template_urls.py +9 -2
  18. django_spire/notification/app/views/page_views.py +2 -10
  19. django_spire/notification/app/views/template_views.py +22 -7
  20. {django_spire-0.24.2.dist-info → django_spire-0.24.3.dist-info}/METADATA +1 -1
  21. {django_spire-0.24.2.dist-info → django_spire-0.24.3.dist-info}/RECORD +24 -16
  22. django_spire/notification/app/templates/django_spire/notification/app/card/list_card.html +0 -11
  23. {django_spire-0.24.2.dist-info → django_spire-0.24.3.dist-info}/WHEEL +0 -0
  24. {django_spire-0.24.2.dist-info → django_spire-0.24.3.dist-info}/licenses/LICENSE.md +0 -0
  25. {django_spire-0.24.2.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.2'
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']]
@@ -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.2
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=biDSV-Uptfgm170WiJVdnjKxxa_xLSNHvUOESbiBmAY,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
@@ -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.2.dist-info/licenses/LICENSE.md,sha256=ZAeCT76WvaoEZE9xPhihyWjTwH0wQZXQmyRsnV2VPFs,1091
1405
- django_spire-0.24.2.dist-info/METADATA,sha256=pVb0KnuCc5tuu28_0UqjZvt-NySVfnujEs7V4TFYbRQ,5127
1406
- django_spire-0.24.2.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
1407
- django_spire-0.24.2.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1408
- django_spire-0.24.2.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,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 %}