django-spire 0.24.3__py3-none-any.whl → 0.25.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
django_spire/consts.py CHANGED
@@ -1,4 +1,4 @@
1
- __VERSION__ = '0.24.3'
1
+ __VERSION__ = '0.25.1'
2
2
 
3
3
  MAINTENANCE_MODE_SETTINGS_NAME = 'MAINTENANCE_MODE'
4
4
 
@@ -1,4 +1,29 @@
1
+ .fs-print-1 {
2
+ font-size: 1.0rem !important;
3
+ }
4
+
5
+ .fs-print-2 {
6
+ font-size: 0.90rem !important;
7
+ }
8
+
9
+ .fs-print-3 {
10
+ font-size: 0.70rem !important;
11
+ }
12
+
1
13
  @media print {
14
+
15
+ .fs-print-1 {
16
+ font-size: .80rem !important;
17
+ }
18
+
19
+ .fs-print-2 {
20
+ font-size: .70rem !important;
21
+ }
22
+
23
+ .fs-print-3 {
24
+ font-size: .55rem !important;
25
+ }
26
+
2
27
  .hide-on-print {
3
28
  display: none !important;
4
29
  }
@@ -0,0 +1,7 @@
1
+ from django_spire.metric.report.report import BaseReport
2
+ from django_spire.metric.report.registry import ReportRegistry
3
+
4
+ __all__ = [
5
+ 'BaseReport',
6
+ 'ReportRegistry',
7
+ ]
@@ -0,0 +1,92 @@
1
+ import calendar
2
+ from datetime import datetime, timedelta
3
+
4
+ from django.utils.timezone import now
5
+
6
+
7
+ class Helper:
8
+ @property
9
+ def thirty_days_ago(self) -> datetime:
10
+ return self.today - timedelta(days=30)
11
+
12
+ @property
13
+ def sixty_days_ago(self) -> datetime:
14
+ return self.today - timedelta(days=60)
15
+
16
+ @property
17
+ def ninety_days_ago(self) -> datetime:
18
+ return self.today - timedelta(days=90)
19
+
20
+ @property
21
+ def today(self) -> datetime:
22
+ return now()
23
+
24
+ @property
25
+ def tomorrow(self) -> datetime:
26
+ return self.today + timedelta(days=1)
27
+
28
+ @property
29
+ def yesterday(self) -> datetime:
30
+ return self.today - timedelta(days=1)
31
+
32
+ @property
33
+ def start_of_current_week(self) -> datetime:
34
+ return self.today - timedelta(days=self.today.weekday())
35
+
36
+ @property
37
+ def end_of_current_week(self) -> datetime:
38
+ return self.start_of_week + timedelta(days=6)
39
+
40
+ @property
41
+ def start_of_last_week(self) -> datetime:
42
+ return self.start_of_week - timedelta(days=6)
43
+
44
+ @property
45
+ def end_of_last_week(self) -> datetime:
46
+ return self.end_of_week - timedelta(days=6)
47
+
48
+ @property
49
+ def start_of_current_month(self) -> datetime:
50
+ return self.today.replace(day=1)
51
+
52
+ @property
53
+ def end_of_current_month(self) -> datetime:
54
+ _, last_day = calendar.monthrange(self.today.year, self.today.month)
55
+ return self.today.replace(day=last_day)
56
+
57
+ @property
58
+ def start_of_last_month(self) -> datetime:
59
+ return self._add_months(self.today, -1).replace(day=1)
60
+
61
+ @property
62
+ def end_of_last_month(self) -> datetime:
63
+ last_month = self._add_months(self.today, -1)
64
+ _, last_day = calendar.monthrange(last_month.year, last_month.month)
65
+
66
+ return last_month.replace(day=last_day)
67
+
68
+ @property
69
+ def start_of_current_year(self) -> datetime:
70
+ return self.today.replace(month=1, day=1)
71
+
72
+ @property
73
+ def end_of_current_year(self) -> datetime:
74
+ return self.today.replace(month=12, day=31)
75
+
76
+ @property
77
+ def start_of_last_year(self) -> datetime:
78
+ return self.today.replace(year=self.today.year - 1, month=1, day=1)
79
+
80
+ @property
81
+ def end_of_last_year(self) -> datetime:
82
+ return self.today.replace(year=self.today.year - 1, month=12, day=31)
83
+
84
+ @staticmethod
85
+ def _add_months(datetime_, months):
86
+ month = datetime_.month - 1 + months
87
+ year = datetime_.year + month // 12
88
+ month = month % 12 + 1
89
+
90
+ day = min(datetime_.day, calendar.monthrange(year, month)[1])
91
+
92
+ return datetime_.replace(year=year, month=month, day=day)
@@ -5,6 +5,7 @@ from dataclasses import field
5
5
  from typing import Literal, Callable, Any
6
6
 
7
7
  from django_spire.metric.report.enums import ColumnType
8
+ from django_spire.metric.report.helper import Helper
8
9
  from django_spire.metric.report.tools import get_text_alignment_css_class
9
10
 
10
11
  ColumnLiteralType = Literal['text', 'choice', 'number', 'dollar', 'percent']
@@ -31,27 +32,28 @@ class ReportCell:
31
32
  def css_class(self) -> str:
32
33
  return get_text_alignment_css_class(self.type)
33
34
 
34
- def cell_value_verbose(self, value):
35
- if self.type == ColumnType.DOLLAR:
35
+ @staticmethod
36
+ def cell_value_verbose(value, cell_type):
37
+ if cell_type == ColumnType.DOLLAR:
36
38
  return f"${float(value):,.2f}"
37
- elif self.type == ColumnType.NUMBER:
39
+ elif cell_type == ColumnType.NUMBER:
38
40
  return f"{float(value):,.0f}"
39
- elif self.type == ColumnType.PERCENT:
41
+ elif cell_type == ColumnType.PERCENT:
40
42
  return f"{float(value):.1f}%"
41
- elif self.type == ColumnType.DECIMAL_1:
43
+ elif cell_type == ColumnType.DECIMAL_1:
42
44
  return f"{float(value):.1f}"
43
- elif self.type == ColumnType.DECIMAL_2:
45
+ elif cell_type == ColumnType.DECIMAL_2:
44
46
  return f"{float(value):.2f}"
45
- elif self.type == ColumnType.DECIMAL_3:
47
+ elif cell_type == ColumnType.DECIMAL_3:
46
48
  return f"{float(value):.3f}"
47
49
 
48
50
  return str(value)
49
51
 
50
52
  def value_verbose(self):
51
- return self.cell_value_verbose(self.value)
53
+ return self.cell_value_verbose(self.value, self.type)
52
54
 
53
55
  def sub_value_verbose(self):
54
- return self.cell_value_verbose(self.sub_value)
56
+ return self.cell_value_verbose(self.sub_value, self.sub_type)
55
57
 
56
58
 
57
59
  @dataclass
@@ -61,6 +63,8 @@ class ReportRow:
61
63
  page_break: bool = False
62
64
  span_all_columns: bool = False
63
65
  table_break: bool = False
66
+ border_top: bool = False
67
+ border_bottom: bool = False
64
68
 
65
69
 
66
70
  class BaseReport(ABC):
@@ -68,6 +72,7 @@ class BaseReport(ABC):
68
72
  description: str | None = None
69
73
  is_financially_accurate: bool = False
70
74
  ColumnType: type[ColumnType] = ColumnType
75
+ helper: Helper = Helper()
71
76
 
72
77
  def __init__(self):
73
78
  if not self.title:
@@ -93,12 +98,20 @@ class BaseReport(ABC):
93
98
  for name, param in signature.parameters.items():
94
99
  arguments[name] = {}
95
100
  arguments[name]['default'] = param.default
101
+ arguments[name]['annotation_class'] = param.annotation
96
102
 
97
103
  choices_method = getattr(self, f'{name}_choices', None)
98
104
 
99
105
  if choices_method and isinstance(choices_method, Callable):
100
- arguments[name]['choices'] = choices_method()
101
- arguments[name]['annotation'] = 'select'
106
+ choices = tuple(choices_method())
107
+
108
+ self.validate_choices(tuple(choices))
109
+
110
+ arguments[name]['choices'] = choices
111
+ if param.annotation.__name__ == 'list':
112
+ arguments[name]['annotation'] = 'multi_select'
113
+ else:
114
+ arguments[name]['annotation'] = 'select'
102
115
  else:
103
116
  arguments[name]['annotation'] = param.annotation.__name__
104
117
 
@@ -108,10 +121,21 @@ class BaseReport(ABC):
108
121
  def run(self, **kwargs: Any):
109
122
  raise NotImplementedError
110
123
 
111
- def add_blank_row(self):
124
+ def add_blank_row(
125
+ self,
126
+ text: str = '',
127
+ page_break: bool = False,
128
+ border_top: bool = False,
129
+ border_bottom: bool = False
130
+ ):
112
131
  self.add_row(
113
- cell_values=[' '],
132
+ cell_values=[
133
+ text
134
+ ],
114
135
  span_all_columns=True,
136
+ page_break=page_break,
137
+ border_top=border_top,
138
+ border_bottom=border_bottom,
115
139
  )
116
140
 
117
141
  def add_column(
@@ -129,24 +153,30 @@ class BaseReport(ABC):
129
153
  def add_divider_row(
130
154
  self,
131
155
  title: str,
156
+ description: str | None = None,
132
157
  page_break: bool = False,
158
+ border_bottom: bool = True,
133
159
  ):
134
160
  self.add_row(
135
161
  cell_values=[title],
162
+ cell_sub_values=[description] if description else None,
136
163
  bold=True,
137
164
  page_break=page_break,
138
165
  span_all_columns=True,
166
+ border_bottom=border_bottom,
139
167
  )
140
168
 
141
169
  def add_footer_row(
142
170
  self,
143
171
  cell_values: list[Any],
144
172
  cell_sub_values: list[Any] | None = None,
173
+ border_top: bool = True,
145
174
  ):
146
175
  self.add_row(
147
176
  cell_values=cell_values,
148
177
  cell_sub_values=cell_sub_values,
149
178
  bold=True,
179
+ border_top=border_top,
150
180
  )
151
181
 
152
182
  def add_row(
@@ -157,6 +187,8 @@ class BaseReport(ABC):
157
187
  page_break: bool = False,
158
188
  span_all_columns: bool = False,
159
189
  table_break: bool = False,
190
+ border_top: bool = False,
191
+ border_bottom: bool = False,
160
192
  ):
161
193
  if span_all_columns or table_break:
162
194
  if len(cell_values) > 1:
@@ -183,5 +215,40 @@ class BaseReport(ABC):
183
215
  page_break=page_break,
184
216
  span_all_columns=span_all_columns,
185
217
  table_break=table_break,
218
+ border_top=border_top,
219
+ border_bottom=border_bottom,
186
220
  )
187
221
  )
222
+
223
+ @staticmethod
224
+ def validate_choices(choices: tuple):
225
+ if not isinstance(choices, tuple):
226
+ raise TypeError(f'choices must be a tuple not {type(choices)}')
227
+ if not all(isinstance(item, tuple) and len(item) == 2 for item in choices):
228
+ raise ValueError('choices must contain tuples of length 2')
229
+
230
+ def to_markdown(self) -> str:
231
+ markdown = ''
232
+
233
+ for column in self.columns:
234
+ markdown += f'| {column.title} '
235
+
236
+ markdown += '|\n'
237
+
238
+ for column in self.columns:
239
+ markdown += '| ' + '-' * len(column.title) + ' '
240
+
241
+ markdown += '|\n'
242
+
243
+ for row in self.rows:
244
+ if row.span_all_columns:
245
+ markdown += f'| {row.cells[0].value}' + '|' * len(self.columns) + '\n'
246
+ continue
247
+
248
+ else:
249
+ for cell in row.cells:
250
+ markdown += f'| {cell.value_verbose()} '
251
+
252
+ markdown += '|\n'
253
+
254
+ return markdown
@@ -4,7 +4,7 @@
4
4
  <div class="col-11 col-md-10 col-lg-8 col-xl-6 mx-auto my-3">
5
5
 
6
6
  <div class="row">
7
- <div class="col fs-5 text-center">
7
+ <div class="col h5 text-center text-app-primary">
8
8
  {{ report.title }}
9
9
  </div>
10
10
  </div>
@@ -23,12 +23,19 @@
23
23
 
24
24
  {% for argument, params in report_run_arguments.items %}
25
25
  <div class="row">
26
- <div class="col mb-2">
27
-
26
+ <div class="col">
28
27
  <label class="form-label">
29
- {{ argument|underscores_to_spaces|title }}
28
+ {{ argument|underscores_to_spaces|title }} <span class="text-danger">*</span>
30
29
  </label>
31
- {% if params.annotation == 'str' %}
30
+ </div>
31
+ </div>
32
+ <div class="row">
33
+ <div class="col mb-2 ps-4">
34
+ {% if params.annotation == 'date' %}
35
+ <input class="form-control" type="date" name="{{ argument }}" value="{{ params.default|date:'Y-m-d' }}">
36
+ {% elif params.annotation == 'datetime' %}
37
+ <input class="form-control" type="datetime-local" name="{{ argument }}" value="{{ params.default|date:'Y-m-d\\TH:i:s' }}">
38
+ {% elif params.annotation == 'str' %}
32
39
  <input class="form-control" type="text" name="{{ argument }}" value="{{ params.default }}">
33
40
  {% elif params.annotation == 'int' %}
34
41
  <input class="form-control" type="number" name="{{ argument }}"
@@ -37,22 +44,37 @@
37
44
  <input class="form-control" type="number" name="{{ argument }}"
38
45
  value="{{ params.default }}">
39
46
  {% elif params.annotation == 'bool' %}
40
- <br><input class="form-check-input" type="checkbox" name="{{ argument }}"
41
- value="{{ params.default }}">
47
+ <div class="form-check form-switch">
48
+ <input class="form-check-input" type="checkbox" role="switch" {% if params.default %}checked{% endif %} id="flexSwitchCheckDefault">
49
+ </div>
50
+ {% elif params.annotation == 'multi_select' %}
51
+ <select class="form-select" multiple size="8" name="{{ argument }}" aria-selected="{{ params.default }}">
52
+ {% for key, val in params.choices %}
53
+ <option value="{{ key }}">{{ val }}</option>
54
+ {% endfor %}
55
+ </select>
42
56
  {% elif params.annotation == 'select' %}
43
57
  <select class="form-select" name="{{ argument }}" aria-selected="{{ params.default }}">
44
58
  {% for key, val in params.choices %}
45
59
  <option value="{{ key }}">{{ val }}</option>
46
60
  {% endfor %}
47
61
  </select>
62
+ {% else %}
63
+ <br>
64
+ <span class="text-danger">Unsupported parameter type: {{ params.annotation }}</span>
48
65
  {% endif %}
49
66
  </div>
50
67
  </div>
51
68
  {% endfor %}
52
69
 
53
- <div class="row">
54
- <div class="col mt-3 text-center">
55
- <input class="btn btn-primary" type="submit" value="Run Report">
70
+ <div x-data="{
71
+ loading: false
72
+ }" class="row">
73
+ <div class="col mt-3 text-center" x-show="!loading">
74
+ <input class="btn btn-app-primary" @click="loading = true" type="submit" value="Run Report">
75
+ </div>
76
+ <div class="col mt-3 text-center text-app-primary" x-show="loading">
77
+ <span class="spinner-border" role="status" aria-hidden="true"></span>
56
78
  </div>
57
79
  </div>
58
80
  <div class="row">
@@ -2,41 +2,41 @@
2
2
 
3
3
  <div class="row mb-1 font-monospace">
4
4
  <div class="col">
5
- <div class="fw-bold">
5
+ <div class="h5 fw-bold">
6
6
  {{ report.title }}
7
7
  </div>
8
8
  </div>
9
9
  </div>
10
10
 
11
- <div class="row mx-0 mb-3 text-muted fw-normal fs--2 border font-monospace">
11
+ <div class="row mx-0 mb-3 text-muted fw-normal fs-print-2 border font-monospace">
12
12
  {% if report.description %}
13
13
  <div class="col-auto ps-1 pe-3">
14
- Description: {{ report.description }}
14
+ Description: <span class="fw-bold">{{ report.description }}</span>
15
15
  </div>
16
16
  {% endif %}
17
- <div class="col-auto ps-1 pe-3">
18
- Printed On: {% now "D, M j, Y" %} at {% now "g:i A" %}
17
+ <div class="col-auto ps-1 pe-3">
18
+ Printed On: <span class="fw-bold">{% now "D, M j, Y" %} at {% now "g:i A" %}</span>
19
19
  </div>
20
- <div class="col-auto ps-1 pe-3">
21
- Financially Accurate: {% if report.is_financially_accurate %}True{% else %}False{% endif %}
20
+ <div class="col-auto ps-1 pe-3">
21
+ Financially Accurate: <span class="fw-bold">{% if report.is_financially_accurate %}True{% else %}False{% endif %}</span>
22
22
  </div>
23
23
  {% for argument, value in report_run_arguments_values.items %}
24
24
  <div class="col-auto ps-1 pe-3">
25
- {{ argument|underscores_to_spaces|title }}: {{ value }}
25
+ {{ argument|underscores_to_spaces|title }}: <span class="fw-bold">{{ value }}</span>
26
26
  </div>
27
27
  {% endfor %}
28
28
  </div>
29
29
 
30
30
  <div class="row">
31
- <div class="col fs--2">
31
+ <div class="col fs-print-2">
32
32
 
33
- <table class="table table-sm table-hover table-print">
33
+ <table class="table table-sm table-hover table-print align-middle">
34
34
  <thead>
35
35
  {% for column in report.columns %}
36
- <th class="align-text-top {{ column.css_class }}">
36
+ <th class="align-text-top {{ column.css_class }}" style="border-bottom-width: 4px;">
37
37
  {{ column.title }}
38
38
  {% if column.sub_title %}
39
- <span class="text-muted fw-normal fs--2">
39
+ <span class="text-muted fw-normal fs-print-3">
40
40
  <br>{{ column.sub_title }}
41
41
  </span>
42
42
  {% endif %}
@@ -45,12 +45,24 @@
45
45
  </thead>
46
46
  <tbody>
47
47
  {% for row in report.rows %}
48
+ {% if row.page_break %}
49
+ <tr style="page-break-before: always;"></tr>
50
+ {% endif %}
48
51
  <tr class="{% if row.bold %}fw-bold{% endif %}"
49
- {% if row.page_break %}style="page-break-before: always;"{% endif %}>
52
+ style="{% if row.border_top %}border-top-width: 4px;{% endif %}{% if row.border_bottom %}border-bottom-width: 4px;{% endif %}">
50
53
  {% if row.span_all_columns %}
51
54
  <td colspan="{{ report.column_count }}">
52
55
  {% for cell in row.cells %}
53
- {{ cell.value|safe }}
56
+ {% if cell.value %}
57
+ {{ cell.value }}
58
+ {% else %}
59
+ &nbsp;
60
+ {% endif %}
61
+ {% if cell.sub_value %}
62
+ <span class="text-muted fw-normal fs-print-3">
63
+ <br>{{ cell.sub_value_verbose }}
64
+ </span>
65
+ {% endif %}
54
66
  {% endfor %}
55
67
  </td>
56
68
  {% else %}
@@ -58,8 +70,9 @@
58
70
  <td class="{{ cell.css_class }}">
59
71
  {{ cell.value_verbose }}
60
72
  {% if cell.sub_value %}
61
- <br>
62
- {{ cell.sub_value_verbose }}
73
+ <span class="text-muted fw-normal fs-print-3">
74
+ <br>{{ cell.sub_value_verbose }}
75
+ </span>
63
76
  {% endif %}
64
77
  </td>
65
78
  {% endfor %}
@@ -1,3 +1,5 @@
1
+ import datetime
2
+
1
3
  from django_spire.metric.report.enums import ColumnType
2
4
 
3
5
 
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from datetime import datetime
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  from django.conf import settings
@@ -37,9 +38,9 @@ def report_view(request: WSGIRequest) -> TemplateResponse:
37
38
  report_registry_class()
38
39
  )
39
40
 
40
- context_data = {
41
- 'registry': page_report_registry,
42
- }
41
+ context_data = dict()
42
+
43
+ context_data['registry'] = page_report_registry
43
44
 
44
45
  if request.GET:
45
46
  report_key_stack = request.GET.get('report_key_stack', None)
@@ -57,11 +58,37 @@ def report_view(request: WSGIRequest) -> TemplateResponse:
57
58
 
58
59
  context_data['report_run_arguments_values'] = {}
59
60
 
60
- for argument in report.run_arguments:
61
+ for argument in context_data['report_run_arguments']:
61
62
  if context_data['report_run_arguments'][argument]['annotation'] == 'bool':
62
63
  get_request_value = request.GET.get(argument, False)
64
+
65
+ elif context_data['report_run_arguments'][argument]['annotation'] == 'date':
66
+ date_str = request.GET.get(argument, None)
67
+
68
+ if date_str:
69
+ get_request_value = datetime.strptime(date_str, '%Y-%m-%d').date()
70
+ else:
71
+ get_request_value = date_str
72
+
73
+
74
+ elif context_data['report_run_arguments'][argument]['annotation'] == 'datetime':
75
+ datetime_str = request.GET.get(argument, None)
76
+
77
+ if datetime_str:
78
+ get_request_value = datetime.strptime(datetime_str, '%Y-%m-%dT%H:%M:%S')
79
+ else:
80
+ get_request_value = datetime_str
81
+
82
+ elif context_data['report_run_arguments'][argument]['annotation'] == 'multi_select':
83
+ get_request_value = request.GET.getlist(argument, [])
84
+
63
85
  else:
64
- get_request_value = request.GET.get(argument, None)
86
+ value = request.GET.get(argument, None)
87
+
88
+ if value:
89
+ get_request_value = context_data['report_run_arguments'][argument]['annotation_class'](value)
90
+ else:
91
+ get_request_value = value
65
92
 
66
93
  context_data['report_run_arguments_values'][argument] = get_request_value
67
94
 
@@ -73,7 +100,7 @@ def report_view(request: WSGIRequest) -> TemplateResponse:
73
100
  ReportRun.objects.create(
74
101
  report_key_stack=report_key_stack,
75
102
  )
76
- report.run()
103
+ report.run(**context_data['report_run_arguments_values'])
77
104
 
78
105
  context_data['report'] = report
79
106
  context_data['report_run_count'] = ReportRun.objects.run_count(report_key_stack)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-spire
3
- Version: 0.24.3
3
+ Version: 0.25.1
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.
@@ -52,7 +52,7 @@ Requires-Dist: crispy-bootstrap5==2024.10
52
52
  Requires-Dist: dandy>=1.3.5
53
53
  Requires-Dist: django>=5.1.8
54
54
  Requires-Dist: django-crispy-forms==2.3
55
- Requires-Dist: django-glue>=0.8.1
55
+ Requires-Dist: django-glue>=0.8.12
56
56
  Requires-Dist: django-sendgrid-v5
57
57
  Requires-Dist: django-storages==1.14.5
58
58
  Requires-Dist: docutils
@@ -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=pGfQC8jGUV7BYSAN3PnjA-Ur64Gr7KGC5UkkXkKWfOk,171
3
+ django_spire/consts.py,sha256=v9iUsL_VvjnVUvdrkUEMoRjYlFE3E5kd8HWLt6T1LJQ,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
@@ -589,7 +589,7 @@ django_spire/core/static/django_spire/css/app-navigation.css,sha256=maHVWpbWXKu0
589
589
  django_spire/core/static/django_spire/css/app-offcanvas.css,sha256=SxDsONE1eqERJ1gDAP8chjoJ0aD0Q1VHBPqRWJi8-Mw,178
590
590
  django_spire/core/static/django_spire/css/app-override.css,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
591
591
  django_spire/core/static/django_spire/css/app-page.css,sha256=pB-sSZc9NEUkkMcCfzzrCfeiggGmhONn8Eid5HLs8c0,363
592
- django_spire/core/static/django_spire/css/app-printing.css,sha256=AxhN5ujYFHmfVnZbc4y1emn-2WJZqBSwJBDM8tYCuZ8,631
592
+ django_spire/core/static/django_spire/css/app-printing.css,sha256=g9H6PmRGDj--5KIBOWRoK-PhWPsC4ssW8XeQbnutO8M,976
593
593
  django_spire/core/static/django_spire/css/app-side-panel.css,sha256=tZUwmC_yK9ZNnoF-I1y6Nx-EtL445K8-CqBbWUcAi1M,1083
594
594
  django_spire/core/static/django_spire/css/app-template.css,sha256=D5ORspwfokNqRW0HwyCbzjBp2Kf_2XtWKa6FSQuSXOg,485
595
595
  django_spire/core/static/django_spire/css/app-text.css,sha256=YQYhTsXux7vVuZhzsyHV5TuPCpKam8F14GiOnMcGOyk,10141
@@ -1177,15 +1177,16 @@ django_spire/knowledge/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NM
1177
1177
  django_spire/knowledge/views/page_views.py,sha256=WdNW8ranxuAS2GkoVLvvrVqWgxN-o9PTYSZ4KXxQj2E,1011
1178
1178
  django_spire/metric/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1179
1179
  django_spire/metric/apps.py,sha256=_ZhyLypoF4Rcr-BMslEryab1FzCGVMkilporGTZ0uss,471
1180
- django_spire/metric/report/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1180
+ django_spire/metric/report/__init__.py,sha256=9WRhI-CvAIKuaYr3PJY-py5K4LsTpGJWDzy9H2VL-K4,175
1181
1181
  django_spire/metric/report/admin.py,sha256=Q52v-IjRbesXvgqQizYsfkOTzGtHmfyVso-KWEYO30o,254
1182
1182
  django_spire/metric/report/apps.py,sha256=0xJJtEueUbvbrG4dOy2F5PqwUbORxSZq8IyOhOEVLUs,645
1183
1183
  django_spire/metric/report/enums.py,sha256=cQkbLWwOjQ0_UWbsfPbLRJ9b6iSHsF7-oljQL0aoATA,247
1184
+ django_spire/metric/report/helper.py,sha256=G1ytJRW3vhP6WED9PQ0lzuw48gCIOG9gm9afjFT9bO4,2637
1184
1185
  django_spire/metric/report/models.py,sha256=HGXc1If1gV4wQCgZMHnfN2iMGPxtFUPfrcFZpWgpzeQ,674
1185
1186
  django_spire/metric/report/querysets.py,sha256=8uu6AJIxtl-bkGg_tua4tFEYRubY_z5Va7crQhsoKGk,606
1186
1187
  django_spire/metric/report/registry.py,sha256=uXkuWYXIMzfaMrz10paoOCMjcgr3g-BWicGnUzj4eeg,1239
1187
- django_spire/metric/report/report.py,sha256=UywsuKcCsXSOF87IKIweYMJyqkuN0Th57-CwONdxlOY,5668
1188
- django_spire/metric/report/tools.py,sha256=NYNpOVgOfNCC961MQOIl1w8amlLHsE7AsSkuSAdV2bM,414
1188
+ django_spire/metric/report/report.py,sha256=0NPiZtsjkH5j5Pr1z7o0i7MPkF-EkMhuZrtakxek2u8,7884
1189
+ django_spire/metric/report/tools.py,sha256=P-kSbfQo_RB_-zgvQ_qpXW_voTerdw-W5NZVX6N8yNQ,431
1189
1190
  django_spire/metric/report/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1190
1191
  django_spire/metric/report/auth/controller.py,sha256=0_wh3nE9aDGNfvY_-EaCYsPTi0wACOS2_YyO5nJjjsE,614
1191
1192
  django_spire/metric/report/auth/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -1193,16 +1194,16 @@ django_spire/metric/report/auth/tests/test_controller.py,sha256=l0btFlyB7kgYptdo
1193
1194
  django_spire/metric/report/migrations/0001_initial.py,sha256=8uwwGLEDofWN8ecHYykGpxEsPbGeNRhCPMVe3UcsI6Q,806
1194
1195
  django_spire/metric/report/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1195
1196
  django_spire/metric/report/templates/django_spire/metric/report/element/report_sub_navigation_element.html,sha256=10asi6WTj4It6ey8OkmqoCz3Ta69ZFwEHbq45_BGzoY,1031
1196
- django_spire/metric/report/templates/django_spire/metric/report/form/report_form.html,sha256=Xz1uBOswxEyiLGlVxoz2GeG4qY2IR3MUh28SGvAOKLQ,2923
1197
+ django_spire/metric/report/templates/django_spire/metric/report/form/report_form.html,sha256=9flaNvc0ZRUpWPg-OUNHIX5FXTbjCLfPUYUoTVzp2z0,4526
1197
1198
  django_spire/metric/report/templates/django_spire/metric/report/page/report_page.html,sha256=mz2KmVdefaGB94_csYzJyph3egpxOjwdPKlISdINYNA,1289
1198
- django_spire/metric/report/templates/django_spire/metric/report/print/report_print.html,sha256=enqh51N8ItkxoqWMAgVo-iitRmhDBXWTL-mesKJn5vs,2605
1199
+ django_spire/metric/report/templates/django_spire/metric/report/print/report_print.html,sha256=W___XhSa47sUXpkq2Fs5KP_zEzwpTjzoM1JL1SYYfZE,3544
1199
1200
  django_spire/metric/report/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1200
1201
  django_spire/metric/report/tests/test_urls/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1201
1202
  django_spire/metric/report/tests/test_urls/test_page_urls.py,sha256=XGu_4RwJmVKjvqDhNnxwblaObwJY5b_lLUg-p7QjA5I,410
1202
1203
  django_spire/metric/report/urls/__init__.py,sha256=6XGBdmEMaaycPNzll5VRoXgfKOfGtwlNR0KYzkiqN4g,206
1203
1204
  django_spire/metric/report/urls/page_urls.py,sha256=bWWgilrnJK7rzMLwVNhbFN9vGdlurqaWLzWHsthjlOA,234
1204
1205
  django_spire/metric/report/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1205
- django_spire/metric/report/views/page_views.py,sha256=bebYMm22uxZ8Cggy9JxA8O0vdC1rgpsjoCBlPT_jQ_o,3454
1206
+ django_spire/metric/report/views/page_views.py,sha256=6y3mEiPOO28n1KOPV47SVCmbbhoPEaZ1icn--E8WSb8,4762
1206
1207
  django_spire/metric/urls/__init__.py,sha256=9C7ZpLABKJJvVr-tlbeE91SZWghTmtCwy20gWsmJ_wc,200
1207
1208
  django_spire/notification/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1208
1209
  django_spire/notification/admin.py,sha256=Upl86FjJc0Z3zfuZmHa9O531E5SkJf5Sb_Dq7xoGf9E,1091
@@ -1409,8 +1410,8 @@ django_spire/theme/urls/page_urls.py,sha256=Oak3x_xwQEb01NKdrsB1nk6yPaOEnheuSG1m
1409
1410
  django_spire/theme/views/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
1410
1411
  django_spire/theme/views/json_views.py,sha256=PWwVTaty0BVGbj65L5cxex6JNhc-xVAI_rEYjbJWqEM,1893
1411
1412
  django_spire/theme/views/page_views.py,sha256=WenjOa6Welpu3IMolY56ZwBjy4aK9hpbiMNuygjAl1A,3922
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,,
1413
+ django_spire-0.25.1.dist-info/licenses/LICENSE.md,sha256=ZAeCT76WvaoEZE9xPhihyWjTwH0wQZXQmyRsnV2VPFs,1091
1414
+ django_spire-0.25.1.dist-info/METADATA,sha256=dtDpr1euDsABhW-uno8QrFe-AH-8Uq7vVSxt3EUsMLc,5128
1415
+ django_spire-0.25.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
1416
+ django_spire-0.25.1.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1417
+ django_spire-0.25.1.dist-info/RECORD,,