django-spire 0.23.12__py3-none-any.whl → 0.23.13__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 (45) hide show
  1. django_spire/consts.py +1 -1
  2. django_spire/contrib/generic_views/modal_views.py +6 -1
  3. django_spire/core/static/django_spire/css/app-import.css +5 -0
  4. django_spire/core/static/django_spire/css/app-printing.css +31 -0
  5. django_spire/core/static/django_spire/css/bootstrap-extension.css +14 -2
  6. django_spire/core/tag/mixins.py +1 -1
  7. django_spire/core/templates/django_spire/page/full_page.html +2 -2
  8. django_spire/core/templates/django_spire/table/base.html +1 -1
  9. django_spire/core/templates/django_spire/table/element/row.html +20 -16
  10. django_spire/knowledge/migrations/0009_alter_collection_tags_alter_entry_tags.py +24 -0
  11. django_spire/metric/__init__.py +0 -0
  12. django_spire/metric/apps.py +19 -0
  13. django_spire/metric/report/__init__.py +0 -0
  14. django_spire/metric/report/admin.py +8 -0
  15. django_spire/metric/report/apps.py +25 -0
  16. django_spire/metric/report/auth/__init__.py +0 -0
  17. django_spire/metric/report/auth/controller.py +17 -0
  18. django_spire/metric/report/auth/tests/__init__.py +0 -0
  19. django_spire/metric/report/auth/tests/test_controller.py +86 -0
  20. django_spire/metric/report/enums.py +14 -0
  21. django_spire/metric/report/migrations/0001_initial.py +28 -0
  22. django_spire/metric/report/migrations/__init__.py +0 -0
  23. django_spire/metric/report/models.py +24 -0
  24. django_spire/metric/report/querysets.py +26 -0
  25. django_spire/metric/report/registry.py +38 -0
  26. django_spire/metric/report/report.py +187 -0
  27. django_spire/metric/report/templates/django_spire/metric/report/element/report_sub_navigation_element.html +34 -0
  28. django_spire/metric/report/templates/django_spire/metric/report/form/report_form.html +67 -0
  29. django_spire/metric/report/templates/django_spire/metric/report/page/report_page.html +37 -0
  30. django_spire/metric/report/templates/django_spire/metric/report/print/report_print.html +73 -0
  31. django_spire/metric/report/tests/__init__.py +0 -0
  32. django_spire/metric/report/tests/test_urls/__init__.py +0 -0
  33. django_spire/metric/report/tests/test_urls/test_page_urls.py +16 -0
  34. django_spire/metric/report/tools.py +11 -0
  35. django_spire/metric/report/urls/__init__.py +10 -0
  36. django_spire/metric/report/urls/page_urls.py +14 -0
  37. django_spire/metric/report/views/__init__.py +0 -0
  38. django_spire/metric/report/views/page_views.py +100 -0
  39. django_spire/metric/urls/__init__.py +10 -0
  40. django_spire/settings.py +3 -0
  41. {django_spire-0.23.12.dist-info → django_spire-0.23.13.dist-info}/METADATA +1 -1
  42. {django_spire-0.23.12.dist-info → django_spire-0.23.13.dist-info}/RECORD +45 -14
  43. {django_spire-0.23.12.dist-info → django_spire-0.23.13.dist-info}/WHEEL +0 -0
  44. {django_spire-0.23.12.dist-info → django_spire-0.23.13.dist-info}/licenses/LICENSE.md +0 -0
  45. {django_spire-0.23.12.dist-info → django_spire-0.23.13.dist-info}/top_level.txt +0 -0
django_spire/consts.py CHANGED
@@ -1,4 +1,4 @@
1
- __VERSION__ = '0.23.12'
1
+ __VERSION__ = '0.23.13'
2
2
 
3
3
  MAINTENANCE_MODE_SETTINGS_NAME = 'MAINTENANCE_MODE'
4
4
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
+ from django.contrib import messages
5
6
  from django.http import HttpResponseRedirect
6
7
  from django.template.response import TemplateResponse
7
8
 
@@ -26,7 +27,8 @@ def dispatch_modal_delete_form_content(
26
27
  # Present and past tense of verb
27
28
  verbs: tuple[str, str] = ('delete', 'deleted'),
28
29
  return_url: str | None = None,
29
- template: str = 'django_spire/modal/content/dispatch_modal_delete_confirmation_content.html'
30
+ template: str = 'django_spire/modal/content/dispatch_modal_delete_confirmation_content.html',
31
+ show_success_message: bool = False
30
32
  ) -> HttpResponseRedirect | TemplateResponse:
31
33
  if context_data is None:
32
34
  context_data = {}
@@ -45,6 +47,9 @@ def dispatch_modal_delete_form_content(
45
47
  auto_add_activity=auto_add_activity
46
48
  )
47
49
 
50
+ if show_success_message:
51
+ messages.success(request, f'Successfully deleted {model_name}.')
52
+
48
53
  else:
49
54
  show_form_errors(request, form)
50
55
 
@@ -10,4 +10,9 @@
10
10
  @import url('app-side-panel.css');
11
11
  @import url('app-text.css');
12
12
  @import url('app-template.css');
13
+
14
+ /* Must be Second to Last */
13
15
  @import url('app-override.css');
16
+
17
+ /* Must be at the Bottom */
18
+ @import url('app-printing.css');
@@ -0,0 +1,31 @@
1
+ @media print {
2
+ .hide-on-print {
3
+ display: none !important;
4
+ }
5
+
6
+ @page {
7
+ margin: 1cm !important;
8
+ }
9
+
10
+ div {
11
+ page-break-inside: auto !important;
12
+ }
13
+
14
+ .page-break-before {
15
+ page-break-before: always !important;
16
+ display: block;
17
+ height: 1px;
18
+ clear: both;
19
+ }
20
+
21
+ .table-hover > tbody > tr:hover > * {
22
+ --bs-table-bg-state: var(--app-table-row-color) !important;
23
+ background-color: var(--app-table-row-color) !important;
24
+ color: var(--app-default-text-color) !important;
25
+ }
26
+
27
+ }
28
+
29
+ .table-print {
30
+ font-family: monospace !important;
31
+ }
@@ -188,11 +188,23 @@ h1, h2, h3, h4, h5, h6 {
188
188
  }
189
189
 
190
190
  .fs--2 {
191
- font-size: .75rem !important;
191
+ font-size: .70rem !important;
192
+ }
193
+
194
+ .fs--3 {
195
+ font-size: .55rem !important;
192
196
  }
193
197
 
194
198
  .fs-7 {
195
- font-size: .85rem !important;
199
+ font-size: .80rem !important;
200
+ }
201
+
202
+ .fs-8 {
203
+ font-size: .70rem !important;
204
+ }
205
+
206
+ .fs-8 {
207
+ font-size: .55rem !important;
196
208
  }
197
209
 
198
210
  .fw-400 {
@@ -7,7 +7,7 @@ from django_spire.core.tag.tools import simplify_and_weight_tag_set_to_dict, sim
7
7
 
8
8
 
9
9
  class TagModelMixin(models.Model):
10
- tags = models.ManyToManyField(Tag, related_name='+', null=True, blank=True, editable=False)
10
+ tags = models.ManyToManyField(Tag, related_name='+', blank=True, editable=False)
11
11
 
12
12
  class Meta:
13
13
  abstract = True
@@ -149,14 +149,14 @@
149
149
  }
150
150
  }"
151
151
  >
152
- <div class="d-none d-lg-block pe-1 side-navigation">
152
+ <div class="d-none d-lg-block pe-1 side-navigation hide-on-print">
153
153
  {% block full_page_side_navigation %}
154
154
  {% include 'django_spire/navigation/side_navigation.html' %}
155
155
  {% endblock %}
156
156
  </div>
157
157
 
158
158
  <div class="container-fluid d-flex flex-column">
159
- <div class="row sticky-top">
159
+ <div class="row sticky-top hide-on-print">
160
160
  <div class="col-12">
161
161
  {% block full_page_top_navigation %}
162
162
  {% include 'django_spire/navigation/top_navigation.html' %}
@@ -141,7 +141,7 @@
141
141
  {% block scroll_container %}
142
142
  <div
143
143
  class="position-relative table-container flex-grow-1"
144
- style="min-height: 200px; overflow-x: auto; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;"
144
+ style="min-height: {{ table_height|default:'200px;' }} overflow-x: auto; overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch;"
145
145
  x-ref="scroll_container"
146
146
  :data-table-id="table_id"
147
147
  >
@@ -74,23 +74,27 @@
74
74
  @select-all-rows.window="handle_select_all_rows($event)"
75
75
  @toggle-row-state.window="handle_toggle_row_state($event)"
76
76
  >
77
- <td>
78
- <input
79
- :checked="is_checked"
80
- @change="handle_checkbox_change()"
81
- type="checkbox"
82
- >
83
- </td>
77
+ {% block table_checkbox %}
78
+ <td>
79
+ <input
80
+ :checked="is_checked"
81
+ @change="handle_checkbox_change()"
82
+ type="checkbox"
83
+ >
84
+ </td>
85
+ {% endblock %}
84
86
 
85
- <td>
86
- <template x-if="has_children">
87
- <i
88
- :class="is_open ? 'bi-chevron-down' : 'bi-chevron-right'"
89
- @click="$dispatch('toggle-row', { row_id: row_id, table_id: table_id })"
90
- class="bi cursor-pointer"
91
- ></i>
92
- </template>
93
- </td>
87
+ {% block table_child_toggle %}
88
+ <td>
89
+ <template x-if="has_children">
90
+ <i
91
+ :class="is_open ? 'bi-chevron-down' : 'bi-chevron-right'"
92
+ @click="$dispatch('toggle-row', { row_id: row_id, table_id: table_id })"
93
+ class="bi cursor-pointer"
94
+ ></i>
95
+ </template>
96
+ </td>
97
+ {% endblock %}
94
98
 
95
99
  {% block table_cell %}{% endblock %}
96
100
  </tr>
@@ -0,0 +1,24 @@
1
+ # Generated by Django 5.2.8 on 2026-01-11 18:54
2
+
3
+ from django.db import migrations, models
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ('django_spire_core', '0001_initial'),
10
+ ('django_spire_knowledge', '0008_collection_tags_entry_tags'),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.AlterField(
15
+ model_name='collection',
16
+ name='tags',
17
+ field=models.ManyToManyField(blank=True, editable=False, related_name='+', to='django_spire_core.tag'),
18
+ ),
19
+ migrations.AlterField(
20
+ model_name='entry',
21
+ name='tags',
22
+ field=models.ManyToManyField(blank=True, editable=False, related_name='+', to='django_spire_core.tag'),
23
+ ),
24
+ ]
File without changes
@@ -0,0 +1,19 @@
1
+ from __future__ import annotations
2
+
3
+ from django.apps import AppConfig
4
+
5
+ from django_spire.utils import check_required_apps
6
+
7
+
8
+ class MetricConfig(AppConfig):
9
+ default_auto_field = 'django.db.models.BigAutoField'
10
+ label = 'metric'
11
+ name = 'django_spire.metric'
12
+
13
+ REQUIRED_APPS = ('django_spire_core',)
14
+
15
+ URLPATTERNS_INCLUDE = 'django_spire.metric.urls'
16
+ URLPATTERNS_NAMESPACE = 'metric'
17
+
18
+ def ready(self) -> None:
19
+ check_required_apps(self.label)
File without changes
@@ -0,0 +1,8 @@
1
+ from django_spire.metric.report.models import ReportRun
2
+ from django.contrib import admin
3
+
4
+
5
+ @admin.register(ReportRun)
6
+ class ReportRunAdmin(admin.ModelAdmin):
7
+ list_display = ('id', 'report_key_stack_verbose', 'datetime')
8
+ ordering = ('-datetime',)
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from django.apps import AppConfig
4
+
5
+ from django_spire.utils import check_required_apps
6
+
7
+
8
+ class ReportConfig(AppConfig):
9
+ default_auto_field = 'django.db.models.BigAutoField'
10
+ label = 'django_spire_metric_report'
11
+ name = 'django_spire.metric.report'
12
+
13
+ MODEL_PERMISSIONS = (
14
+ {
15
+ 'name': 'report',
16
+ 'verbose_name': 'Report',
17
+ 'model_class_path': 'django_spire.metric.report.models.ReportRun',
18
+ 'is_proxy_model': False,
19
+ },
20
+ )
21
+
22
+ REQUIRED_APPS = ('django_spire_core',)
23
+
24
+ def ready(self) -> None:
25
+ check_required_apps(self.label)
File without changes
@@ -0,0 +1,17 @@
1
+ from __future__ import annotations
2
+
3
+ from django_spire.auth.controller.controller import BaseAuthController
4
+
5
+
6
+ class BaseReportAuthController(BaseAuthController):
7
+ def can_add(self):
8
+ return self.request.user.has_perm('django_spire_metric_report.add_reportrun')
9
+
10
+ def can_change(self):
11
+ return self.request.user.has_perm('django_spire_metric_report.change_reportrun')
12
+
13
+ def can_delete(self):
14
+ return self.request.user.has_perm('django_spire_metric_report.delete_reportrun')
15
+
16
+ def can_view(self):
17
+ return self.request.user.has_perm('django_spire_metric_report.view_reportrun')
File without changes
@@ -0,0 +1,86 @@
1
+ from __future__ import annotations
2
+
3
+ from django.contrib.auth.models import Permission
4
+ from django.contrib.contenttypes.models import ContentType
5
+ from django.test import RequestFactory
6
+
7
+ from django_spire.auth.user.tests.factories import create_user
8
+ from django_spire.core.tests.test_cases import BaseTestCase
9
+ from django_spire.metric.report.auth.controller import BaseReportAuthController
10
+ from django_spire.metric.report.models import ReportRun
11
+
12
+
13
+ class BaseReportAuthControllerTests(BaseTestCase):
14
+ def setUp(self):
15
+ super().setUp()
16
+ self.factory = RequestFactory()
17
+ self.user = create_user(username='test_auth_user')
18
+ self.request = self.factory.get('/')
19
+ self.request.user = self.user
20
+
21
+ content_type = ContentType.objects.get_for_model(ReportRun)
22
+
23
+ self.view_permission = Permission.objects.get(
24
+ codename='view_reportrun',
25
+ content_type=content_type
26
+ )
27
+ self.add_permission = Permission.objects.get(
28
+ codename='add_reportrun',
29
+ content_type=content_type
30
+ )
31
+ self.change_permission = Permission.objects.get(
32
+ codename='change_reportrun',
33
+ content_type=content_type
34
+ )
35
+ self.delete_permission = Permission.objects.get(
36
+ codename='delete_reportrun',
37
+ content_type=content_type
38
+ )
39
+
40
+ def _refresh_user_and_request(self):
41
+ self.user = type(self.user).objects.get(pk=self.user.pk)
42
+ self.request.user = self.user
43
+
44
+ def test_can_view_without_permission(self):
45
+ controller = BaseReportAuthController(self.request)
46
+ assert controller.can_view() is False
47
+
48
+ def test_can_view_with_permission(self):
49
+ self.user.user_permissions.add(self.view_permission)
50
+ self._refresh_user_and_request()
51
+
52
+ controller = BaseReportAuthController(self.request)
53
+ assert controller.can_view() is True
54
+
55
+ def test_can_add_without_permission(self):
56
+ controller = BaseReportAuthController(self.request)
57
+ assert controller.can_add() is False
58
+
59
+ def test_can_add_with_permission(self):
60
+ self.user.user_permissions.add(self.add_permission)
61
+ self._refresh_user_and_request()
62
+
63
+ controller = BaseReportAuthController(self.request)
64
+ assert controller.can_add() is True
65
+
66
+ def test_can_change_without_permission(self):
67
+ controller = BaseReportAuthController(self.request)
68
+ assert controller.can_change() is False
69
+
70
+ def test_can_change_with_permission(self):
71
+ self.user.user_permissions.add(self.change_permission)
72
+ self._refresh_user_and_request()
73
+
74
+ controller = BaseReportAuthController(self.request)
75
+ assert controller.can_change() is True
76
+
77
+ def test_can_delete_without_permission(self):
78
+ controller = BaseReportAuthController(self.request)
79
+ assert controller.can_delete() is False
80
+
81
+ def test_can_delete_with_permission(self):
82
+ self.user.user_permissions.add(self.delete_permission)
83
+ self._refresh_user_and_request()
84
+
85
+ controller = BaseReportAuthController(self.request)
86
+ assert controller.can_delete() is True
@@ -0,0 +1,14 @@
1
+ from enum import Enum
2
+
3
+
4
+ class ColumnType(str, Enum):
5
+ TEXT = 'text'
6
+ CHOICE = 'choice'
7
+ NUMBER = 'number'
8
+ DECIMAL_1 = 'decimal_1'
9
+ DECIMAL_2 = 'decimal_2'
10
+ DECIMAL_3 = 'decimal_3'
11
+ DOLLAR = 'dollar'
12
+ PERCENT = 'percent'
13
+
14
+
@@ -0,0 +1,28 @@
1
+ # Generated by Django 5.2.10 on 2026-01-08 20:41
2
+
3
+ import django.utils.timezone
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='ReportRun',
17
+ fields=[
18
+ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
19
+ ('report_key_stack', models.TextField()),
20
+ ('datetime', models.DateTimeField(default=django.utils.timezone.now)),
21
+ ],
22
+ options={
23
+ 'verbose_name': 'Report Run',
24
+ 'verbose_name_plural': 'Report Runs',
25
+ 'db_table': 'django_spire_metric_report_run',
26
+ },
27
+ ),
28
+ ]
File without changes
@@ -0,0 +1,24 @@
1
+ from django.db import models
2
+ from django.utils.timezone import now
3
+
4
+ from django_spire.metric.report.querysets import ReportRunQuerySet
5
+
6
+
7
+ class ReportRun(models.Model):
8
+ report_key_stack = models.TextField()
9
+ datetime = models.DateTimeField(default=now)
10
+
11
+ objects = ReportRunQuerySet.as_manager()
12
+
13
+ @property
14
+ def report_button_text(self) -> str:
15
+ return self.report_key_stack.split('|')[-1]
16
+
17
+ @property
18
+ def report_key_stack_verbose(self):
19
+ return self.report_key_stack.replace('|', ' > ')
20
+
21
+ class Meta:
22
+ verbose_name = 'Report Run'
23
+ verbose_name_plural = 'Report Runs'
24
+ db_table = 'django_spire_metric_report_run'
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from django.db.models import QuerySet, Count
6
+
7
+ if TYPE_CHECKING:
8
+ pass
9
+
10
+
11
+ class ReportRunQuerySet(QuerySet):
12
+ def by_popular(self):
13
+ return (
14
+ self
15
+ .values('report_key_stack')
16
+ .annotate(
17
+ run_count=Count('report_key_stack')
18
+ )
19
+ .order_by('-run_count')
20
+ )
21
+
22
+ def by_top_ten(self):
23
+ return self.by_popular()[:10]
24
+
25
+ def run_count(self, report_key_stack: str) -> int:
26
+ return self.filter(report_key_stack=report_key_stack).count()
@@ -0,0 +1,38 @@
1
+ from typing import Self
2
+
3
+ from django_spire.metric.report.report import BaseReport
4
+
5
+
6
+ class ReportRegistry:
7
+ category: str | None = None
8
+ report_names_classes: dict[str, BaseReport] = {}
9
+ report_registries: list[Self] = []
10
+
11
+ def __init__(self):
12
+ for report_registry in self.report_registries:
13
+ self.add_registry(report_registry)
14
+
15
+ def add_registry(
16
+ self,
17
+ report_registry: Self
18
+ ):
19
+ if report_registry.category is None:
20
+ message = 'Report Registry category is required'
21
+ raise ValueError(message)
22
+
23
+ # if report_registry.category in self.report_names_classes:
24
+ # message = f'Report Registry category "{report_registry.category}" already exists'
25
+ # raise ValueError(message)
26
+
27
+ self.report_names_classes[report_registry.category] = report_registry.report_names_classes
28
+
29
+ def get_report_from_key_stack(self, report_key_stack: str) -> BaseReport | None:
30
+ current = self.report_names_classes
31
+
32
+ for key in report_key_stack.split('|'):
33
+ if isinstance(current, dict) and key in current:
34
+ current = current[key]
35
+ else:
36
+ return None
37
+
38
+ return current()