django-spire 0.24.3__py3-none-any.whl → 0.25.0__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.0'
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:
@@ -97,8 +102,15 @@ class BaseReport(ABC):
97
102
  choices_method = getattr(self, f'{name}_choices', None)
98
103
 
99
104
  if choices_method and isinstance(choices_method, Callable):
100
- arguments[name]['choices'] = choices_method()
101
- arguments[name]['annotation'] = 'select'
105
+ choices = tuple(choices_method())
106
+
107
+ self.validate_choices(tuple(choices))
108
+
109
+ arguments[name]['choices'] = choices
110
+ if param.annotation.__name__ == 'list':
111
+ arguments[name]['annotation'] = 'multi_select'
112
+ else:
113
+ arguments[name]['annotation'] = 'select'
102
114
  else:
103
115
  arguments[name]['annotation'] = param.annotation.__name__
104
116
 
@@ -108,10 +120,21 @@ class BaseReport(ABC):
108
120
  def run(self, **kwargs: Any):
109
121
  raise NotImplementedError
110
122
 
111
- def add_blank_row(self):
123
+ def add_blank_row(
124
+ self,
125
+ text: str = '',
126
+ page_break: bool = False,
127
+ border_top: bool = False,
128
+ border_bottom: bool = False
129
+ ):
112
130
  self.add_row(
113
- cell_values=[' '],
131
+ cell_values=[
132
+ text
133
+ ],
114
134
  span_all_columns=True,
135
+ page_break=page_break,
136
+ border_top=border_top,
137
+ border_bottom=border_bottom,
115
138
  )
116
139
 
117
140
  def add_column(
@@ -129,24 +152,30 @@ class BaseReport(ABC):
129
152
  def add_divider_row(
130
153
  self,
131
154
  title: str,
155
+ description: str | None = None,
132
156
  page_break: bool = False,
157
+ border_bottom: bool = True,
133
158
  ):
134
159
  self.add_row(
135
160
  cell_values=[title],
161
+ cell_sub_values=[description] if description else None,
136
162
  bold=True,
137
163
  page_break=page_break,
138
164
  span_all_columns=True,
165
+ border_bottom=border_bottom,
139
166
  )
140
167
 
141
168
  def add_footer_row(
142
169
  self,
143
170
  cell_values: list[Any],
144
171
  cell_sub_values: list[Any] | None = None,
172
+ border_top: bool = True,
145
173
  ):
146
174
  self.add_row(
147
175
  cell_values=cell_values,
148
176
  cell_sub_values=cell_sub_values,
149
177
  bold=True,
178
+ border_top=border_top,
150
179
  )
151
180
 
152
181
  def add_row(
@@ -157,6 +186,8 @@ class BaseReport(ABC):
157
186
  page_break: bool = False,
158
187
  span_all_columns: bool = False,
159
188
  table_break: bool = False,
189
+ border_top: bool = False,
190
+ border_bottom: bool = False,
160
191
  ):
161
192
  if span_all_columns or table_break:
162
193
  if len(cell_values) > 1:
@@ -183,5 +214,40 @@ class BaseReport(ABC):
183
214
  page_break=page_break,
184
215
  span_all_columns=span_all_columns,
185
216
  table_break=table_break,
217
+ border_top=border_top,
218
+ border_bottom=border_bottom,
186
219
  )
187
220
  )
221
+
222
+ @staticmethod
223
+ def validate_choices(choices: tuple):
224
+ if not isinstance(choices, tuple):
225
+ raise TypeError(f'choices must be a tuple not {type(choices)}')
226
+ if not all(isinstance(item, tuple) and len(item) == 2 for item in choices):
227
+ raise ValueError('choices must contain tuples of length 2')
228
+
229
+ def to_markdown(self) -> str:
230
+ markdown = ''
231
+
232
+ for column in self.columns:
233
+ markdown += f'| {column.title} '
234
+
235
+ markdown += '|\n'
236
+
237
+ for column in self.columns:
238
+ markdown += '| ' + '-' * len(column.title) + ' '
239
+
240
+ markdown += '|\n'
241
+
242
+ for row in self.rows:
243
+ if row.span_all_columns:
244
+ markdown += f'| {row.cells[0].value}' + '|' * len(self.columns) + '\n'
245
+ continue
246
+
247
+ else:
248
+ for cell in row.cells:
249
+ markdown += f'| {cell.value_verbose()} '
250
+
251
+ markdown += '|\n'
252
+
253
+ 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,9 +58,30 @@ 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
86
  get_request_value = request.GET.get(argument, None)
65
87
 
@@ -73,7 +95,7 @@ def report_view(request: WSGIRequest) -> TemplateResponse:
73
95
  ReportRun.objects.create(
74
96
  report_key_stack=report_key_stack,
75
97
  )
76
- report.run()
98
+ report.run(**context_data['report_run_arguments_values'])
77
99
 
78
100
  context_data['report'] = report
79
101
  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.0
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=Fj8BeYEP9huSqBiu0ditvWkltjRMWJETSfvv53PtSHM,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=AQlVGhj_a3mCHCEgSj2qtmOx5sB0nDpLpUIyrdo1QM4,7817
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=2_tUC8aEduzdFotfFA6uQrfVwc9cZf4ZcZ6fTAujMSc,4533
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.0.dist-info/licenses/LICENSE.md,sha256=ZAeCT76WvaoEZE9xPhihyWjTwH0wQZXQmyRsnV2VPFs,1091
1414
+ django_spire-0.25.0.dist-info/METADATA,sha256=fOFp3X9L_byesQXucsL17pYMGaV58t2JoM9n_Rz7l4U,5128
1415
+ django_spire-0.25.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
1416
+ django_spire-0.25.0.dist-info/top_level.txt,sha256=xf3QV1e--ONkVpgMDQE9iqjQ1Vg4--_6C8wmO-KxPHQ,13
1417
+ django_spire-0.25.0.dist-info/RECORD,,