django-spire 0.22.4__py3-none-any.whl → 0.23.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. django_spire/auth/group/forms.py +3 -4
  2. django_spire/auth/group/utils.py +1 -2
  3. django_spire/auth/group/views/form_views.py +22 -14
  4. django_spire/auth/group/views/page_views.py +1 -1
  5. django_spire/auth/mfa/utils.py +1 -1
  6. django_spire/auth/permissions/consts.py +2 -1
  7. django_spire/auth/permissions/decorators.py +1 -2
  8. django_spire/auth/permissions/permissions.py +1 -3
  9. django_spire/comment/factories.py +1 -1
  10. django_spire/comment/mixins.py +1 -1
  11. django_spire/comment/views.py +1 -1
  12. django_spire/consts.py +1 -1
  13. django_spire/contrib/breadcrumb/breadcrumbs.py +1 -1
  14. django_spire/contrib/form/confirmation_forms.py +1 -1
  15. django_spire/contrib/form/utils.py +26 -16
  16. django_spire/contrib/generic_views/modal_views.py +1 -1
  17. django_spire/contrib/generic_views/portal_views.py +18 -55
  18. django_spire/contrib/ordering/validators.py +4 -8
  19. django_spire/contrib/queryset/enums.py +2 -3
  20. django_spire/contrib/queryset/mixins.py +17 -6
  21. django_spire/contrib/session/controller.py +0 -1
  22. django_spire/core/context_processors.py +1 -1
  23. django_spire/core/management/commands/spire_startapp_pkg/user_input.py +1 -1
  24. django_spire/core/middleware/maintenance.py +2 -1
  25. django_spire/core/middleware.py +2 -1
  26. django_spire/core/redirect/generic_redirect.py +1 -1
  27. django_spire/core/redirect/safe_redirect.py +2 -1
  28. django_spire/core/shortcuts.py +4 -2
  29. django_spire/core/table/__init__.py +0 -0
  30. django_spire/core/table/enums.py +18 -0
  31. django_spire/core/templates/django_spire/card/infinite_scroll_card.html +3 -137
  32. django_spire/core/templates/django_spire/container/infinite_scroll_container.html +64 -0
  33. django_spire/core/templates/django_spire/infinite_scroll/base.html +348 -0
  34. django_spire/core/templates/django_spire/infinite_scroll/element/footer.html +11 -0
  35. django_spire/core/templates/django_spire/infinite_scroll/scroll.html +142 -0
  36. django_spire/core/templates/django_spire/item/infinite_scroll_item.html +33 -0
  37. django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_section_element.html +19 -0
  38. django_spire/core/templates/django_spire/lazy_tab/element/lazy_tab_trigger_element.html +15 -0
  39. django_spire/core/templates/django_spire/lazy_tab/lazy_tab.html +157 -0
  40. django_spire/core/templates/django_spire/page/infinite_scroll_list_page.html +7 -0
  41. django_spire/core/templates/django_spire/table/base.html +185 -373
  42. django_spire/core/templates/django_spire/table/element/footer.html +7 -15
  43. django_spire/core/templates/django_spire/table/element/header.html +1 -1
  44. django_spire/core/templates/django_spire/table/element/row.html +15 -7
  45. django_spire/core/templatetags/spire_core_tags.py +1 -2
  46. django_spire/file/fields.py +1 -1
  47. django_spire/file/interfaces.py +1 -1
  48. django_spire/file/views.py +1 -1
  49. django_spire/history/activity/utils.py +1 -1
  50. django_spire/history/mixins.py +0 -4
  51. django_spire/notification/app/context_data.py +3 -1
  52. django_spire/notification/app/views/json_views.py +1 -1
  53. django_spire/notification/app/views/page_views.py +2 -1
  54. django_spire/notification/app/views/template_views.py +2 -2
  55. django_spire/profiling/middleware/profiling.py +2 -2
  56. django_spire/profiling/panel.py +2 -2
  57. django_spire/testing/__init__.py +0 -0
  58. django_spire/testing/playwright/__init__.py +64 -0
  59. django_spire/testing/playwright/components/__init__.py +45 -0
  60. django_spire/testing/playwright/components/accordion.py +55 -0
  61. django_spire/testing/playwright/components/attribute_element.py +73 -0
  62. django_spire/testing/playwright/components/base_session_filter_form.py +57 -0
  63. django_spire/testing/playwright/components/breadcrumb_element.py +56 -0
  64. django_spire/testing/playwright/components/card.py +102 -0
  65. django_spire/testing/playwright/components/dropdown.py +87 -0
  66. django_spire/testing/playwright/components/infinite_scroll.py +158 -0
  67. django_spire/testing/playwright/components/lazy_tab.py +92 -0
  68. django_spire/testing/playwright/components/modal.py +101 -0
  69. django_spire/testing/playwright/components/navigation.py +119 -0
  70. django_spire/testing/playwright/components/notification_bell.py +59 -0
  71. django_spire/testing/playwright/components/theme_selector.py +46 -0
  72. django_spire/testing/playwright/components/toast.py +72 -0
  73. django_spire/testing/playwright/fixtures.py +54 -0
  74. django_spire/testing/playwright/pages/__init__.py +6 -0
  75. django_spire/testing/playwright/pages/base.py +24 -0
  76. django_spire/theme/models.py +1 -1
  77. django_spire/theme/tests/test_context_processor.py +0 -1
  78. django_spire/theme/views/json_views.py +1 -1
  79. django_spire/theme/views/page_views.py +1 -1
  80. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/METADATA +4 -1
  81. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/RECORD +84 -54
  82. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/WHEEL +0 -0
  83. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/licenses/LICENSE.md +0 -0
  84. {django_spire-0.22.4.dist-info → django_spire-0.23.2.dist-info}/top_level.txt +0 -0
@@ -5,9 +5,6 @@ import json
5
5
  from django import forms
6
6
  from django.contrib.auth.models import Group, User
7
7
 
8
- from crispy_forms.helper import FormHelper
9
- from crispy_forms.layout import Column, Layout, Row, Submit
10
-
11
8
  from django_spire.auth.group.factories import bulk_create_groups_from_names
12
9
 
13
10
 
@@ -28,8 +25,10 @@ class GroupNamesForm(forms.Form):
28
25
  class GroupForm(forms.ModelForm):
29
26
  def clean_name(self):
30
27
  name = self.cleaned_data['name']
28
+
31
29
  if name.lower() == 'all users':
32
- raise forms.ValidationError('"All Users" is a reserved name. Please choose another name.')
30
+ message = '"All Users" is a reserved name. Please choose another name.'
31
+ raise forms.ValidationError(message)
33
32
 
34
33
  return name
35
34
  class Meta:
@@ -1,7 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
-
4
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
5
4
 
6
5
  from django_spire.auth.permissions.consts import VALID_PERMISSION_LEVELS, PERMISSIONS_LEVEL_CHOICES
7
6
 
@@ -1,12 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from django.core.handlers.wsgi import WSGIRequest
4
3
  from django.http import HttpResponseRedirect
5
4
  from django.shortcuts import get_object_or_404
6
- from django.template.response import TemplateResponse
7
5
  from django.urls import reverse
8
6
 
9
7
  import django_glue as dg
8
+
10
9
  from django_spire.auth.group import models, forms
11
10
  from django_spire.auth.group.utils import set_group_users
12
11
  from django_spire.auth.permissions.decorators import permission_required
@@ -16,12 +15,17 @@ from django_spire.contrib.form.utils import show_form_errors
16
15
  from django_spire.contrib.generic_views import portal_views
17
16
  from django_spire.core.shortcuts import get_object_or_null_obj
18
17
  from django_spire.history.activity.utils import add_form_activity
18
+ from typing import TYPE_CHECKING
19
+
20
+ if TYPE_CHECKING:
21
+ from django.core.handlers.wsgi import WSGIRequest
22
+ from django.template.response import TemplateResponse
19
23
 
20
24
 
21
25
  @permission_required('django_spire_auth_group.change_authgroup')
22
26
  def form_view(
23
- request: WSGIRequest,
24
- pk: int = 0
27
+ request: WSGIRequest,
28
+ pk: int = 0
25
29
  ) -> TemplateResponse | HttpResponseRedirect:
26
30
  group = get_object_or_null_obj(models.AuthGroup, pk=pk)
27
31
 
@@ -36,8 +40,8 @@ def form_view(
36
40
 
37
41
  return_url = reverse('django_spire:auth:group:page:list')
38
42
  return HttpResponseRedirect(return_url)
39
- else:
40
- show_form_errors(request, form)
43
+
44
+ show_form_errors(request, form)
41
45
 
42
46
  form = forms.GroupForm(instance=group)
43
47
 
@@ -54,11 +58,12 @@ def form_view(
54
58
 
55
59
  @permission_required('django_spire_auth_group.add_authgroup')
56
60
  def user_form_view(
57
- request: WSGIRequest,
58
- pk: int
61
+ request: WSGIRequest,
62
+ pk: int
59
63
  ) -> TemplateResponse | HttpResponseRedirect:
60
64
  group = get_object_or_404(models.AuthGroup, pk=pk)
61
65
  user_choices = AuthUser.services.get_user_choices()
66
+
62
67
  selected_user_ids = list(
63
68
  AuthUser.objects
64
69
  .filter(groups=group)
@@ -84,8 +89,8 @@ def user_form_view(
84
89
 
85
90
  return_url = reverse('django_spire:auth:group:page:detail', kwargs={'pk': pk})
86
91
  return HttpResponseRedirect(return_url)
87
- else:
88
- show_form_errors(request, form)
92
+
93
+ show_form_errors(request, form)
89
94
 
90
95
  form = forms.GroupUserForm()
91
96
 
@@ -124,9 +129,9 @@ def delete_form_view(request: WSGIRequest, pk: int) -> TemplateResponse:
124
129
 
125
130
  @permission_required('django_spire_auth_group.delete_authgroup')
126
131
  def group_remove_user_form_view(
127
- request: WSGIRequest,
128
- group_pk: int,
129
- pk: int
132
+ request: WSGIRequest,
133
+ group_pk: int,
134
+ pk: int
130
135
  ) -> HttpResponseRedirect | TemplateResponse:
131
136
  group = get_object_or_404(models.AuthGroup, pk=group_pk)
132
137
  user = get_object_or_404(AuthUser, pk=pk)
@@ -147,7 +152,10 @@ def group_remove_user_form_view(
147
152
  )
148
153
 
149
154
  return HttpResponseRedirect(
150
- reverse('django_spire:auth:group:page:detail', kwargs={'pk': group_pk})
155
+ reverse(
156
+ 'django_spire:auth:group:page:detail',
157
+ kwargs={'pk': group_pk}
158
+ )
151
159
  )
152
160
 
153
161
  form = DeleteConfirmationForm(request.GET, obj=user)
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from django.shortcuts import get_object_or_404
6
6
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from django_spire.auth.mfa.models import MfaCode
6
6
 
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import Literal
3
+ from typing import Literal
4
4
 
5
5
  from django_spire.auth.group.models import AuthGroup
6
6
  from django_spire.auth.user.models import AuthUser
7
7
 
8
+
8
9
  PERMISSIONS_LEVEL_CHOICES = (
9
10
  (0, 'None'),
10
11
  (1, 'View'),
@@ -1,9 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import functools
4
- from typing import Sequence
5
4
 
6
- from typing_extensions import TYPE_CHECKING
5
+ from typing import Sequence, TYPE_CHECKING
7
6
 
8
7
  from django.core.exceptions import PermissionDenied
9
8
  from django.http import HttpResponseRedirect
@@ -1,8 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
4
-
5
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
6
4
 
7
5
  from django.contrib.auth.models import Permission, Group, User
8
6
  from django.contrib.contenttypes.models import ContentType
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  if TYPE_CHECKING:
6
6
  from django.contrib.auth.models import User
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from django.contrib.contenttypes.fields import GenericRelation
6
6
  from django.db import models
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from django.contrib import messages
6
6
  from django.contrib.auth.decorators import login_required
django_spire/consts.py CHANGED
@@ -1,4 +1,4 @@
1
- __VERSION__ = '0.22.4'
1
+ __VERSION__ = '0.23.2'
2
2
 
3
3
  MAINTENANCE_MODE_SETTINGS_NAME = 'MAINTENANCE_MODE'
4
4
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TypedDict
3
+ from typing import TypedDict
4
4
 
5
5
 
6
6
  class BreadcrumbDict(TypedDict):
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import Callable, TYPE_CHECKING
3
+ from typing import Callable, TYPE_CHECKING
4
4
 
5
5
  from django import forms
6
6
 
@@ -1,30 +1,40 @@
1
1
  from __future__ import annotations
2
2
 
3
- from django.forms import Form
4
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
5
4
 
6
5
  from django.contrib import messages
7
6
 
8
7
  if TYPE_CHECKING:
9
8
  from django.core.handlers.wsgi import WSGIRequest
9
+ from django.forms import Form
10
10
 
11
11
 
12
- def show_form_errors(request: WSGIRequest, *forms: Form) -> None:
13
- for form in forms:
14
- for field_name, error_list in form.errors.items():
15
- for error in error_list.data:
16
- error_message = ''
12
+ def form_errors_as_list(form: Form) -> list[str]:
13
+ form_errors = []
14
+
15
+ for field_name, error_list in form.errors.items():
16
+ for error in error_list.data:
17
+ error_message = ''
18
+
19
+ if field_name != '__all__':
20
+ error_message += f'{field_name.title()}: '
17
21
 
18
- if field_name != '__all__':
19
- error_message += f'{field_name.title()}: '
22
+ if hasattr(error, 'message_responses'):
23
+ error_message += f'{" ".join(error.message_responses)}'
20
24
 
21
- if hasattr(error, 'message_responses'):
22
- error_message += f'{" ".join(error.message_responses)}'
25
+ elif hasattr(error, 'messages'):
26
+ error_message += f'{" ".join(error.messages)}'
23
27
 
24
- elif hasattr(error, 'messages'):
25
- error_message += f'{" ".join(error.messages)}'
28
+ else:
29
+ message = 'Error message not found.'
30
+ raise Exception(message)
26
31
 
27
- else:
28
- raise Exception('Error message not found.')
32
+ form_errors.append(error_message)
29
33
 
30
- messages.error(request, error_message)
34
+ return form_errors
35
+
36
+
37
+ def show_form_errors(request: WSGIRequest, *forms: Form) -> None:
38
+ for form in forms:
39
+ for error in form_errors_as_list(form):
40
+ messages.error(request=request, message=error)
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
4
4
 
5
5
  from django.http import HttpResponseRedirect
6
6
  from django.template.response import TemplateResponse
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import Any, Callable, TYPE_CHECKING
3
+ from typing import Any, Callable, TYPE_CHECKING
4
4
 
5
5
  from django.db.models import QuerySet
6
6
  from django.http import HttpResponseRedirect
@@ -121,34 +121,40 @@ def delete_form_view(
121
121
  def infinite_scrolling_view(
122
122
  request: WSGIRequest,
123
123
  *,
124
- context_data: dict[str, Any],
125
124
  queryset: QuerySet | list,
126
125
  queryset_name: str,
127
- template: str
126
+ template: str,
127
+ context_data: dict[str, Any] | None = None
128
128
  ) -> TemplateResponse:
129
129
  if context_data is None:
130
130
  context_data = {}
131
131
 
132
- current_page = int(request.GET.get('page', 1))
133
- page_size = int(request.GET.get('page_size', 10))
132
+ default_batch_size = 25
134
133
 
135
- start = (current_page - 1) * page_size
136
- end = start + page_size
134
+ page = int(request.GET.get('page', 1))
135
+
136
+ batch_size = (
137
+ context_data.get('batch_size')
138
+ if 'batch_size' in context_data
139
+ else request.GET.get('batch_size', default_batch_size)
140
+ )
137
141
 
138
- object_list = queryset[start:end]
142
+ batch_size = int(batch_size)
143
+ offset = (page - 1) * batch_size
139
144
 
140
- length = (
145
+ total_count = (
141
146
  queryset.count()
142
147
  if isinstance(queryset, QuerySet)
143
148
  else len(queryset)
144
149
  )
145
150
 
146
- has_next = end < length
151
+ object_list = queryset[offset:offset + batch_size]
152
+ has_next = offset + batch_size < total_count
147
153
 
148
154
  base_context_data = {
149
- 'current_page': current_page,
155
+ 'batch_size': batch_size,
150
156
  'has_next': has_next,
151
- 'page_size': page_size,
157
+ 'total_count': total_count,
152
158
  queryset_name: object_list
153
159
  }
154
160
 
@@ -270,49 +276,6 @@ def model_form_view(
270
276
  )
271
277
 
272
278
 
273
- def table_view(
274
- request: WSGIRequest,
275
- *,
276
- queryset: QuerySet,
277
- queryset_name: str,
278
- template: str,
279
- context_data: dict[str, Any] | None = None
280
- ) -> TemplateResponse:
281
- if context_data is None:
282
- context_data = {}
283
-
284
- default_batch_size = 25
285
-
286
- page = int(request.GET.get('page', 1))
287
-
288
- batch_size = (
289
- context_data.get('batch_size')
290
- if 'batch_size' in context_data
291
- else request.GET.get('batch_size', default_batch_size)
292
- )
293
-
294
- batch_size = int(batch_size)
295
- offset = (page - 1) * batch_size
296
-
297
- total_count = queryset.count()
298
- object_list = queryset[offset:offset + batch_size]
299
- has_next = offset + batch_size < total_count
300
-
301
- base_context_data = {
302
- queryset_name: object_list,
303
- 'has_next': has_next,
304
- 'total_count': total_count,
305
- }
306
-
307
- context_data.update(base_context_data)
308
-
309
- return TemplateResponse(
310
- request,
311
- context=context_data,
312
- template=template
313
- )
314
-
315
-
316
279
  def template_view(
317
280
  request: WSGIRequest,
318
281
  page_title: str,
@@ -1,15 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
-
4
- from typing_extensions import TYPE_CHECKING
5
-
6
- from django.db.models import QuerySet
3
+ from typing import TYPE_CHECKING
7
4
 
8
5
  from django_spire.contrib.ordering.exceptions import OrderingMixinException
9
6
 
10
7
  if TYPE_CHECKING:
11
- from typing import Callable
12
- from django.db.models import Model
8
+ from django.db.models import Model, QuerySet
13
9
 
14
10
 
15
11
  class OrderingMixinValidator:
@@ -37,9 +33,9 @@ class OrderingMixinValidator:
37
33
  Validates that the destination and origin and insertion objects and position are valid.
38
34
  Returns tuple of validity and applicable error messages.
39
35
  """
40
- self._validate_position()
41
36
 
42
- return False if self._errors else True
37
+ self._validate_position()
38
+ return not self._errors
43
39
 
44
40
  def _validate_position(self):
45
41
  """Ensure position is valid."""
@@ -1,7 +1,6 @@
1
- from enum import Enum
1
+ from enum import StrEnum
2
2
 
3
3
 
4
- # Todo: Change to string enum
5
- class SessionFilterActionEnum(Enum):
4
+ class SessionFilterActionEnum(StrEnum):
6
5
  CLEAR = 'Clear'
7
6
  FILTER = 'Filter'
@@ -22,24 +22,35 @@ class SessionFilterQuerySetMixin(QuerySet):
22
22
  ) -> QuerySet:
23
23
  # Session keys must match to process new queryset data
24
24
 
25
- action = request.GET.get('action')
25
+ try:
26
+ action = SessionFilterActionEnum(request.GET.get('action'))
27
+ except ValueError:
28
+ action = None
29
+
26
30
  form = form_class(request.GET)
27
31
 
28
32
  if form.is_valid():
29
33
  session = SessionController(request=request, session_key=session_key)
30
34
 
31
- # Todo: Change actions into an enum
32
- if action == SessionFilterActionEnum.CLEAR.value:
35
+ if action == SessionFilterActionEnum.CLEAR:
33
36
  session.purge()
34
37
  return self
35
38
 
36
- # The user has submitted the filter form
37
- if action == SessionFilterActionEnum.FILTER.value and session_key == request.GET.get('session_filter_key'):
38
-
39
+ # Apply filters when the user submits the filter form
40
+ if (
41
+ action == SessionFilterActionEnum.FILTER
42
+ and session_key == request.GET.get('session_filter_key')
43
+ ):
39
44
  # Update session data
40
45
  for key, value in form.cleaned_data.items():
41
46
  session.add_data(key, value)
42
47
 
48
+ # If the session is expired, return the unfiltered queryset
49
+ if session.is_expired:
50
+ return self
51
+
52
+ # When no new filter data is applied and session is NOT yet expired,
53
+ # return the original queryset
43
54
  return self.bulk_filter(session.data)
44
55
  else:
45
56
  show_form_errors(request, form)
@@ -75,7 +75,6 @@ class SessionController:
75
75
  self._set_modified()
76
76
 
77
77
  def _clean(self) -> None:
78
-
79
78
  if self._TIMEOUT_KEY in self.data and self.is_expired:
80
79
  self.request.session.pop(self.session_key)
81
80
  self._set_modified()
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import Any, TYPE_CHECKING
3
+ from typing import Any, TYPE_CHECKING
4
4
 
5
5
  from django_spire.auth.controller.controller import AppAuthController
6
6
  from django_spire.conf import settings
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
 
5
- from typing_extensions import TYPE_CHECKING
5
+ from typing import TYPE_CHECKING
6
6
 
7
7
  from django.core.management.base import CommandError
8
8
 
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from django.conf import settings
4
6
  from django.template.response import TemplateResponse
5
- from typing_extensions import TYPE_CHECKING
6
7
 
7
8
  from django_spire.consts import MAINTENANCE_MODE_SETTINGS_NAME
8
9
 
@@ -1,8 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from typing import TYPE_CHECKING
4
+
3
5
  from django.conf import settings
4
6
  from django.template.response import TemplateResponse
5
- from typing_extensions import TYPE_CHECKING
6
7
 
7
8
  from django_spire.consts import MAINTENANCE_MODE_SETTINGS_NAME
8
9
 
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import Any
3
+ from typing import Any
4
4
 
5
5
  from django.http import HttpResponse
6
6
  from django.urls import reverse
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing_extensions import TYPE_CHECKING
3
+ from typing import TYPE_CHECKING
4
+
4
5
  from urllib.parse import parse_qs, unquote, urlencode, urlparse, urlunparse
5
6
 
6
7
  from django.urls import reverse, NoReverseMatch
@@ -2,14 +2,16 @@ from __future__ import annotations
2
2
 
3
3
  import json
4
4
 
5
+ from typing import TYPE_CHECKING, TypeVar
6
+
5
7
  from django.contrib.contenttypes.models import ContentType
6
8
  from django.db.models import Model
7
- from typing_extensions import TYPE_CHECKING, TypeVar
8
9
 
9
10
  if TYPE_CHECKING:
11
+ from typing import Any
12
+
10
13
  from django.db.models import QuerySet
11
14
  from django.http import HttpRequest
12
- from typing_extensions import Any
13
15
 
14
16
 
15
17
  T = TypeVar('T', bound=Model)
File without changes
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import StrEnum
4
+
5
+
6
+ class ResponsiveMode(StrEnum):
7
+ """
8
+ It defines how tables handle responsive behavior at different viewport sizes.
9
+
10
+ Collapse: Any columns with breakpoint attributes are hidden at smaller viewports
11
+ and shown at their specified breakpoint.
12
+
13
+ Scroll: All columns remain visible regardless of viewport size. The table
14
+ container enables horizontal scrolling to accommodate the full width.
15
+ """
16
+
17
+ COLLAPSE = 'collapse'
18
+ SCROLL = 'scroll'