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 +1 -1
- django_spire/core/static/django_spire/css/app-printing.css +25 -0
- django_spire/metric/report/__init__.py +7 -0
- django_spire/metric/report/helper.py +92 -0
- django_spire/metric/report/report.py +80 -13
- django_spire/metric/report/templates/django_spire/metric/report/form/report_form.html +32 -10
- django_spire/metric/report/templates/django_spire/metric/report/print/report_print.html +29 -16
- django_spire/metric/report/tools.py +2 -0
- django_spire/metric/report/views/page_views.py +33 -6
- {django_spire-0.24.3.dist-info → django_spire-0.25.1.dist-info}/METADATA +2 -2
- {django_spire-0.24.3.dist-info → django_spire-0.25.1.dist-info}/RECORD +14 -13
- {django_spire-0.24.3.dist-info → django_spire-0.25.1.dist-info}/WHEEL +0 -0
- {django_spire-0.24.3.dist-info → django_spire-0.25.1.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.24.3.dist-info → django_spire-0.25.1.dist-info}/top_level.txt +0 -0
django_spire/consts.py
CHANGED
|
@@ -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,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
|
-
|
|
35
|
-
|
|
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
|
|
39
|
+
elif cell_type == ColumnType.NUMBER:
|
|
38
40
|
return f"{float(value):,.0f}"
|
|
39
|
-
elif
|
|
41
|
+
elif cell_type == ColumnType.PERCENT:
|
|
40
42
|
return f"{float(value):.1f}%"
|
|
41
|
-
elif
|
|
43
|
+
elif cell_type == ColumnType.DECIMAL_1:
|
|
42
44
|
return f"{float(value):.1f}"
|
|
43
|
-
elif
|
|
45
|
+
elif cell_type == ColumnType.DECIMAL_2:
|
|
44
46
|
return f"{float(value):.2f}"
|
|
45
|
-
elif
|
|
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
|
-
|
|
101
|
-
|
|
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(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
<
|
|
41
|
-
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
{
|
|
56
|
+
{% if cell.value %}
|
|
57
|
+
{{ cell.value }}
|
|
58
|
+
{% else %}
|
|
59
|
+
|
|
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
|
-
<
|
|
62
|
-
|
|
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,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
|
-
|
|
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
|
|
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
|
+
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.
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
1188
|
-
django_spire/metric/report/tools.py,sha256=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
1413
|
-
django_spire-0.
|
|
1414
|
-
django_spire-0.
|
|
1415
|
-
django_spire-0.
|
|
1416
|
-
django_spire-0.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|