django-spire 0.23.12__py3-none-any.whl → 0.24.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 +1 -1
- django_spire/contrib/generic_views/modal_views.py +6 -1
- django_spire/core/static/django_spire/css/app-import.css +5 -0
- django_spire/core/static/django_spire/css/app-printing.css +31 -0
- django_spire/core/static/django_spire/css/bootstrap-extension.css +14 -2
- django_spire/core/tag/mixins.py +1 -1
- django_spire/core/templates/django_spire/page/full_page.html +2 -2
- django_spire/core/templates/django_spire/table/base.html +1 -1
- django_spire/core/templates/django_spire/table/element/row.html +20 -16
- django_spire/knowledge/migrations/0009_alter_collection_tags_alter_entry_tags.py +24 -0
- django_spire/metric/__init__.py +0 -0
- django_spire/metric/apps.py +19 -0
- django_spire/metric/report/__init__.py +0 -0
- django_spire/metric/report/admin.py +8 -0
- django_spire/metric/report/apps.py +25 -0
- django_spire/metric/report/auth/__init__.py +0 -0
- django_spire/metric/report/auth/controller.py +17 -0
- django_spire/metric/report/auth/tests/__init__.py +0 -0
- django_spire/metric/report/auth/tests/test_controller.py +86 -0
- django_spire/metric/report/enums.py +14 -0
- django_spire/metric/report/migrations/0001_initial.py +28 -0
- django_spire/metric/report/migrations/__init__.py +0 -0
- django_spire/metric/report/models.py +24 -0
- django_spire/metric/report/querysets.py +26 -0
- django_spire/metric/report/registry.py +38 -0
- django_spire/metric/report/report.py +187 -0
- django_spire/metric/report/templates/django_spire/metric/report/element/report_sub_navigation_element.html +34 -0
- django_spire/metric/report/templates/django_spire/metric/report/form/report_form.html +67 -0
- django_spire/metric/report/templates/django_spire/metric/report/page/report_page.html +37 -0
- django_spire/metric/report/templates/django_spire/metric/report/print/report_print.html +73 -0
- django_spire/metric/report/tests/__init__.py +0 -0
- django_spire/metric/report/tests/test_urls/__init__.py +0 -0
- django_spire/metric/report/tests/test_urls/test_page_urls.py +16 -0
- django_spire/metric/report/tools.py +11 -0
- django_spire/metric/report/urls/__init__.py +10 -0
- django_spire/metric/report/urls/page_urls.py +14 -0
- django_spire/metric/report/views/__init__.py +0 -0
- django_spire/metric/report/views/page_views.py +100 -0
- django_spire/metric/urls/__init__.py +10 -0
- django_spire/settings.py +3 -0
- {django_spire-0.23.12.dist-info → django_spire-0.24.0.dist-info}/METADATA +1 -1
- {django_spire-0.23.12.dist-info → django_spire-0.24.0.dist-info}/RECORD +45 -14
- {django_spire-0.23.12.dist-info → django_spire-0.24.0.dist-info}/WHEEL +0 -0
- {django_spire-0.23.12.dist-info → django_spire-0.24.0.dist-info}/licenses/LICENSE.md +0 -0
- {django_spire-0.23.12.dist-info → django_spire-0.24.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from dataclasses import field
|
|
5
|
+
from typing import Literal, Callable, Any
|
|
6
|
+
|
|
7
|
+
from django_spire.metric.report.enums import ColumnType
|
|
8
|
+
from django_spire.metric.report.tools import get_text_alignment_css_class
|
|
9
|
+
|
|
10
|
+
ColumnLiteralType = Literal['text', 'choice', 'number', 'dollar', 'percent']
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ReportColumn:
|
|
15
|
+
title: str
|
|
16
|
+
sub_title: str | None = None
|
|
17
|
+
type: ColumnType = ColumnType.TEXT
|
|
18
|
+
sub_type: ColumnType = ColumnType.TEXT
|
|
19
|
+
|
|
20
|
+
def css_class(self) -> str:
|
|
21
|
+
return get_text_alignment_css_class(self.type)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class ReportCell:
|
|
26
|
+
value: Any
|
|
27
|
+
sub_value: Any = None
|
|
28
|
+
type: ColumnType = ColumnType.TEXT
|
|
29
|
+
sub_type: ColumnType = ColumnType.TEXT
|
|
30
|
+
|
|
31
|
+
def css_class(self) -> str:
|
|
32
|
+
return get_text_alignment_css_class(self.type)
|
|
33
|
+
|
|
34
|
+
def cell_value_verbose(self, value):
|
|
35
|
+
if self.type == ColumnType.DOLLAR:
|
|
36
|
+
return f"${float(value):,.2f}"
|
|
37
|
+
elif self.type == ColumnType.NUMBER:
|
|
38
|
+
return f"{float(value):,.0f}"
|
|
39
|
+
elif self.type == ColumnType.PERCENT:
|
|
40
|
+
return f"{float(value):.1f}%"
|
|
41
|
+
elif self.type == ColumnType.DECIMAL_1:
|
|
42
|
+
return f"{float(value):.1f}"
|
|
43
|
+
elif self.type == ColumnType.DECIMAL_2:
|
|
44
|
+
return f"{float(value):.2f}"
|
|
45
|
+
elif self.type == ColumnType.DECIMAL_3:
|
|
46
|
+
return f"{float(value):.3f}"
|
|
47
|
+
|
|
48
|
+
return str(value)
|
|
49
|
+
|
|
50
|
+
def value_verbose(self):
|
|
51
|
+
return self.cell_value_verbose(self.value)
|
|
52
|
+
|
|
53
|
+
def sub_value_verbose(self):
|
|
54
|
+
return self.cell_value_verbose(self.sub_value)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass
|
|
58
|
+
class ReportRow:
|
|
59
|
+
cells: list[ReportCell] = field(default_factory=list)
|
|
60
|
+
bold: bool = False
|
|
61
|
+
page_break: bool = False
|
|
62
|
+
span_all_columns: bool = False
|
|
63
|
+
table_break: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class BaseReport(ABC):
|
|
67
|
+
title: str
|
|
68
|
+
description: str | None = None
|
|
69
|
+
is_financially_accurate: bool = False
|
|
70
|
+
ColumnType: type[ColumnType] = ColumnType
|
|
71
|
+
|
|
72
|
+
def __init__(self):
|
|
73
|
+
if not self.title:
|
|
74
|
+
message = 'Report title is required'
|
|
75
|
+
raise ValueError(message)
|
|
76
|
+
|
|
77
|
+
self.columns: list[ReportColumn] = []
|
|
78
|
+
self.rows: list[ReportRow] = []
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def column_count(self) -> int:
|
|
82
|
+
return len(self.columns)
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def is_ready(self):
|
|
86
|
+
return len(self.columns) > 0
|
|
87
|
+
|
|
88
|
+
@property
|
|
89
|
+
def run_arguments(self) -> dict[str, dict[str, str]]:
|
|
90
|
+
arguments = {}
|
|
91
|
+
signature = inspect.signature(self.run)
|
|
92
|
+
|
|
93
|
+
for name, param in signature.parameters.items():
|
|
94
|
+
arguments[name] = {}
|
|
95
|
+
arguments[name]['default'] = param.default
|
|
96
|
+
|
|
97
|
+
choices_method = getattr(self, f'{name}_choices', None)
|
|
98
|
+
|
|
99
|
+
if choices_method and isinstance(choices_method, Callable):
|
|
100
|
+
arguments[name]['choices'] = choices_method()
|
|
101
|
+
arguments[name]['annotation'] = 'select'
|
|
102
|
+
else:
|
|
103
|
+
arguments[name]['annotation'] = param.annotation.__name__
|
|
104
|
+
|
|
105
|
+
return arguments
|
|
106
|
+
|
|
107
|
+
@abstractmethod
|
|
108
|
+
def run(self, **kwargs: Any):
|
|
109
|
+
raise NotImplementedError
|
|
110
|
+
|
|
111
|
+
def add_blank_row(self):
|
|
112
|
+
self.add_row(
|
|
113
|
+
cell_values=[' '],
|
|
114
|
+
span_all_columns=True,
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
def add_column(
|
|
118
|
+
self,
|
|
119
|
+
title: str,
|
|
120
|
+
sub_title: str | None = None,
|
|
121
|
+
type: ColumnType = ColumnType.TEXT,
|
|
122
|
+
sub_type: ColumnType = ColumnType.TEXT,
|
|
123
|
+
|
|
124
|
+
):
|
|
125
|
+
self.columns.append(
|
|
126
|
+
ReportColumn(title=title, sub_title=sub_title, type=type, sub_type=sub_type)
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
def add_divider_row(
|
|
130
|
+
self,
|
|
131
|
+
title: str,
|
|
132
|
+
page_break: bool = False,
|
|
133
|
+
):
|
|
134
|
+
self.add_row(
|
|
135
|
+
cell_values=[title],
|
|
136
|
+
bold=True,
|
|
137
|
+
page_break=page_break,
|
|
138
|
+
span_all_columns=True,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def add_footer_row(
|
|
142
|
+
self,
|
|
143
|
+
cell_values: list[Any],
|
|
144
|
+
cell_sub_values: list[Any] | None = None,
|
|
145
|
+
):
|
|
146
|
+
self.add_row(
|
|
147
|
+
cell_values=cell_values,
|
|
148
|
+
cell_sub_values=cell_sub_values,
|
|
149
|
+
bold=True,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def add_row(
|
|
153
|
+
self,
|
|
154
|
+
cell_values: list[Any],
|
|
155
|
+
cell_sub_values: list[Any] | None = None,
|
|
156
|
+
bold: bool = False,
|
|
157
|
+
page_break: bool = False,
|
|
158
|
+
span_all_columns: bool = False,
|
|
159
|
+
table_break: bool = False,
|
|
160
|
+
):
|
|
161
|
+
if span_all_columns or table_break:
|
|
162
|
+
if len(cell_values) > 1:
|
|
163
|
+
message = 'Cannot span all columns or have a table break with more than one cell value'
|
|
164
|
+
raise ValueError(message)
|
|
165
|
+
|
|
166
|
+
elif len(cell_values) != len(self.columns) or (
|
|
167
|
+
cell_sub_values is not None and len(cell_sub_values) != len(self.columns)):
|
|
168
|
+
message = f'Number of cell values ({len(cell_values)}) and sub values ({len(cell_sub_values) if cell_sub_values else "None"}) must match number of columns: {len(self.columns)}'
|
|
169
|
+
raise ValueError(message)
|
|
170
|
+
|
|
171
|
+
self.rows.append(
|
|
172
|
+
ReportRow(
|
|
173
|
+
cells=[
|
|
174
|
+
ReportCell(
|
|
175
|
+
value=cell_values[i],
|
|
176
|
+
sub_value=cell_sub_values[i] if cell_sub_values else None,
|
|
177
|
+
type=self.columns[i].type,
|
|
178
|
+
sub_type=self.columns[i].sub_type
|
|
179
|
+
)
|
|
180
|
+
for i in range(len(cell_values))
|
|
181
|
+
],
|
|
182
|
+
bold=bold,
|
|
183
|
+
page_break=page_break,
|
|
184
|
+
span_all_columns=span_all_columns,
|
|
185
|
+
table_break=table_break,
|
|
186
|
+
)
|
|
187
|
+
)
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{% load variable_types %}
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
<div class="row">
|
|
5
|
+
<div class="col {% if depth %}ps-4{% endif %}">
|
|
6
|
+
|
|
7
|
+
{% if sub_nav_value|is_dict %}
|
|
8
|
+
|
|
9
|
+
{% for key, val in sub_nav_value.items %}
|
|
10
|
+
|
|
11
|
+
{% if val|is_dict %}
|
|
12
|
+
|
|
13
|
+
<div class="fs--1 pb-1">{{ key }}</div>
|
|
14
|
+
|
|
15
|
+
{% if key_stack is None %}
|
|
16
|
+
{% include 'django_spire/metric/report/element/report_sub_navigation_element.html' with depth=True sub_nav_value=val key_stack=key %}
|
|
17
|
+
{% else %}
|
|
18
|
+
{% include 'django_spire/metric/report/element/report_sub_navigation_element.html' with depth=True sub_nav_value=val key_stack=key_stack|add:'|'|add:key %}
|
|
19
|
+
{% endif %}
|
|
20
|
+
|
|
21
|
+
{% else %}
|
|
22
|
+
<div class="fs--1 pb-1">
|
|
23
|
+
<a href="?report_key_stack={{ key_stack }}|{{ key }}">> {{ key }}</a>
|
|
24
|
+
</div>
|
|
25
|
+
{% endif %}
|
|
26
|
+
|
|
27
|
+
{% endfor %}
|
|
28
|
+
|
|
29
|
+
{% endif %}
|
|
30
|
+
|
|
31
|
+
</div>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
{% load string_formating %}
|
|
2
|
+
|
|
3
|
+
<div class="row">
|
|
4
|
+
<div class="col-11 col-md-10 col-lg-8 col-xl-6 mx-auto my-3">
|
|
5
|
+
|
|
6
|
+
<div class="row">
|
|
7
|
+
<div class="col fs-5 text-center">
|
|
8
|
+
{{ report.title }}
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
{% if report.description %}
|
|
12
|
+
<div class="row">
|
|
13
|
+
<div class="col text-center text-muted fs--1 mb-3">
|
|
14
|
+
{{ report.description }}
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
{% endif %}
|
|
18
|
+
|
|
19
|
+
<form class="form" method="GET">
|
|
20
|
+
<input class="form-control" type="hidden" name="report_key_stack"
|
|
21
|
+
value="{{ request.GET.report_key_stack }}">
|
|
22
|
+
<input class="form-control" type="hidden" name="report_should_run" value="True">
|
|
23
|
+
|
|
24
|
+
{% for argument, params in report_run_arguments.items %}
|
|
25
|
+
<div class="row">
|
|
26
|
+
<div class="col mb-2">
|
|
27
|
+
|
|
28
|
+
<label class="form-label">
|
|
29
|
+
{{ argument|underscores_to_spaces|title }}
|
|
30
|
+
</label>
|
|
31
|
+
{% if params.annotation == 'str' %}
|
|
32
|
+
<input class="form-control" type="text" name="{{ argument }}" value="{{ params.default }}">
|
|
33
|
+
{% elif params.annotation == 'int' %}
|
|
34
|
+
<input class="form-control" type="number" name="{{ argument }}"
|
|
35
|
+
value="{{ params.default }}">
|
|
36
|
+
{% elif params.annotation == 'float' %}
|
|
37
|
+
<input class="form-control" type="number" name="{{ argument }}"
|
|
38
|
+
value="{{ params.default }}">
|
|
39
|
+
{% elif params.annotation == 'bool' %}
|
|
40
|
+
<br><input class="form-check-input" type="checkbox" name="{{ argument }}"
|
|
41
|
+
value="{{ params.default }}">
|
|
42
|
+
{% elif params.annotation == 'select' %}
|
|
43
|
+
<select class="form-select" name="{{ argument }}" aria-selected="{{ params.default }}">
|
|
44
|
+
{% for key, val in params.choices %}
|
|
45
|
+
<option value="{{ key }}">{{ val }}</option>
|
|
46
|
+
{% endfor %}
|
|
47
|
+
</select>
|
|
48
|
+
{% endif %}
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
{% endfor %}
|
|
52
|
+
|
|
53
|
+
<div class="row">
|
|
54
|
+
<div class="col mt-3 text-center">
|
|
55
|
+
<input class="btn btn-primary" type="submit" value="Run Report">
|
|
56
|
+
</div>
|
|
57
|
+
</div>
|
|
58
|
+
<div class="row">
|
|
59
|
+
<div class="col mt-3 text-center text-muted fs--1">
|
|
60
|
+
This report has been informative {{ report_run_count }} times!
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
</form>
|
|
64
|
+
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{% extends 'django_spire/page/full_page.html' %}
|
|
2
|
+
|
|
3
|
+
{% load variable_types %}
|
|
4
|
+
|
|
5
|
+
{% block full_page_sub_navigation_title %}
|
|
6
|
+
Reports
|
|
7
|
+
{% endblock %}
|
|
8
|
+
|
|
9
|
+
{% block full_page_sub_navigation %}
|
|
10
|
+
{% if not report.is_ready %}
|
|
11
|
+
{% include 'django_spire/metric/report/element/report_sub_navigation_element.html' with depth=0 sub_nav_value=registry.report_names_classes %}
|
|
12
|
+
{% endif %}
|
|
13
|
+
{% endblock %}
|
|
14
|
+
|
|
15
|
+
{% block full_page_content %}
|
|
16
|
+
{% if report %}
|
|
17
|
+
{% if report.is_ready %}
|
|
18
|
+
{% include 'django_spire/metric/report/print/report_print.html' %}
|
|
19
|
+
{% else %}
|
|
20
|
+
{% include 'django_spire/metric/report/form/report_form.html' %}
|
|
21
|
+
{% endif %}
|
|
22
|
+
{% else %}
|
|
23
|
+
<div class="row">
|
|
24
|
+
<div class="col fs-5 my-3 text-center">
|
|
25
|
+
Top 10 Reports
|
|
26
|
+
</div>
|
|
27
|
+
</div>
|
|
28
|
+
{% for report_run in top_ten_report_runs %}
|
|
29
|
+
<div class="row">
|
|
30
|
+
<div class="col text-center pb-2">
|
|
31
|
+
<a href="?report_key_stack={{ report_run.report_key_stack }}">{{ report_run.report_key_stack_verbose }}</a><br>
|
|
32
|
+
<div class="text-muted fs--1">{{ report_run.run_count }} Total Runs</div>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
{% endfor %}
|
|
36
|
+
{% endif %}
|
|
37
|
+
{% endblock %}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
{% load string_formating %}
|
|
2
|
+
|
|
3
|
+
<div class="row mb-1 font-monospace">
|
|
4
|
+
<div class="col">
|
|
5
|
+
<div class="fw-bold">
|
|
6
|
+
{{ report.title }}
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
9
|
+
</div>
|
|
10
|
+
|
|
11
|
+
<div class="row mx-0 mb-3 text-muted fw-normal fs--2 border font-monospace">
|
|
12
|
+
{% if report.description %}
|
|
13
|
+
<div class="col-auto ps-1 pe-3">
|
|
14
|
+
Description: {{ report.description }}
|
|
15
|
+
</div>
|
|
16
|
+
{% endif %}
|
|
17
|
+
<div class="col-auto ps-1 pe-3">
|
|
18
|
+
Printed On: {% now "D, M j, Y" %} at {% now "g:i A" %}
|
|
19
|
+
</div>
|
|
20
|
+
<div class="col-auto ps-1 pe-3">
|
|
21
|
+
Financially Accurate: {% if report.is_financially_accurate %}True{% else %}False{% endif %}
|
|
22
|
+
</div>
|
|
23
|
+
{% for argument, value in report_run_arguments_values.items %}
|
|
24
|
+
<div class="col-auto ps-1 pe-3">
|
|
25
|
+
{{ argument|underscores_to_spaces|title }}: {{ value }}
|
|
26
|
+
</div>
|
|
27
|
+
{% endfor %}
|
|
28
|
+
</div>
|
|
29
|
+
|
|
30
|
+
<div class="row">
|
|
31
|
+
<div class="col fs--2">
|
|
32
|
+
|
|
33
|
+
<table class="table table-sm table-hover table-print">
|
|
34
|
+
<thead>
|
|
35
|
+
{% for column in report.columns %}
|
|
36
|
+
<th class="align-text-top {{ column.css_class }}">
|
|
37
|
+
{{ column.title }}
|
|
38
|
+
{% if column.sub_title %}
|
|
39
|
+
<span class="text-muted fw-normal fs--2">
|
|
40
|
+
<br>{{ column.sub_title }}
|
|
41
|
+
</span>
|
|
42
|
+
{% endif %}
|
|
43
|
+
</th>
|
|
44
|
+
{% endfor %}
|
|
45
|
+
</thead>
|
|
46
|
+
<tbody>
|
|
47
|
+
{% for row in report.rows %}
|
|
48
|
+
<tr class="{% if row.bold %}fw-bold{% endif %}"
|
|
49
|
+
{% if row.page_break %}style="page-break-before: always;"{% endif %}>
|
|
50
|
+
{% if row.span_all_columns %}
|
|
51
|
+
<td colspan="{{ report.column_count }}">
|
|
52
|
+
{% for cell in row.cells %}
|
|
53
|
+
{{ cell.value|safe }}
|
|
54
|
+
{% endfor %}
|
|
55
|
+
</td>
|
|
56
|
+
{% else %}
|
|
57
|
+
{% for cell in row.cells %}
|
|
58
|
+
<td class="{{ cell.css_class }}">
|
|
59
|
+
{{ cell.value_verbose }}
|
|
60
|
+
{% if cell.sub_value %}
|
|
61
|
+
<br>
|
|
62
|
+
{{ cell.sub_value_verbose }}
|
|
63
|
+
{% endif %}
|
|
64
|
+
</td>
|
|
65
|
+
{% endfor %}
|
|
66
|
+
{% endif %}
|
|
67
|
+
</tr>
|
|
68
|
+
{% endfor %}
|
|
69
|
+
</tbody>
|
|
70
|
+
</table>
|
|
71
|
+
|
|
72
|
+
</div>
|
|
73
|
+
</div>
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from django.urls import reverse
|
|
4
|
+
|
|
5
|
+
from django_spire.core.tests.test_cases import BaseTestCase
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ReportPageUrlsTests(BaseTestCase):
|
|
9
|
+
def setUp(self):
|
|
10
|
+
super().setUp()
|
|
11
|
+
|
|
12
|
+
def test_report_view_url_path(self):
|
|
13
|
+
response = self.client.get(
|
|
14
|
+
reverse('django_spire:metric:report:page:report')
|
|
15
|
+
)
|
|
16
|
+
assert response.status_code == 200
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
from django_spire.metric.report.enums import ColumnType
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_text_alignment_css_class(column_type: ColumnType) -> str:
|
|
5
|
+
if column_type in (ColumnType.DOLLAR, ColumnType.NUMBER, ColumnType.PERCENT, ColumnType.DECIMAL_1,
|
|
6
|
+
ColumnType.DECIMAL_2, ColumnType.DECIMAL_3):
|
|
7
|
+
return 'text-end'
|
|
8
|
+
if column_type == ColumnType.CHOICE:
|
|
9
|
+
return 'text-center'
|
|
10
|
+
|
|
11
|
+
return 'text-start'
|
|
File without changes
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.urls import reverse
|
|
7
|
+
|
|
8
|
+
from django_spire.auth.controller.controller import AppAuthController
|
|
9
|
+
from django_spire.contrib import Breadcrumbs
|
|
10
|
+
from django_spire.contrib.generic_views import portal_views
|
|
11
|
+
from django_spire.core.utils import get_object_from_module_string
|
|
12
|
+
from django_spire.metric.report.models import ReportRun
|
|
13
|
+
from django_spire.metric.report.registry import ReportRegistry
|
|
14
|
+
|
|
15
|
+
if TYPE_CHECKING:
|
|
16
|
+
from django.core.handlers.wsgi import WSGIRequest
|
|
17
|
+
from django.template.response import TemplateResponse
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@AppAuthController('report').permission_required('can_view')
|
|
21
|
+
def report_view(request: WSGIRequest) -> TemplateResponse:
|
|
22
|
+
breadcrumbs = Breadcrumbs()
|
|
23
|
+
|
|
24
|
+
breadcrumbs.add_breadcrumb(
|
|
25
|
+
'Reports',
|
|
26
|
+
reverse('django_spire:metric:report:page:report'),
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
page_report_registry = ReportRegistry()
|
|
30
|
+
|
|
31
|
+
for report_registry in settings.DJANGO_SPIRE_REPORT_REGISTRIES:
|
|
32
|
+
report_registry_class = get_object_from_module_string(
|
|
33
|
+
report_registry
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
page_report_registry.add_registry(
|
|
37
|
+
report_registry_class()
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
context_data = {
|
|
41
|
+
'registry': page_report_registry,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if request.GET:
|
|
45
|
+
report_key_stack = request.GET.get('report_key_stack', None)
|
|
46
|
+
|
|
47
|
+
if report_key_stack:
|
|
48
|
+
report = page_report_registry.get_report_from_key_stack(report_key_stack)
|
|
49
|
+
|
|
50
|
+
if report:
|
|
51
|
+
for key in report_key_stack.split('|'):
|
|
52
|
+
breadcrumbs.add_breadcrumb(
|
|
53
|
+
key,
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
context_data['report_run_arguments'] = report.run_arguments
|
|
57
|
+
|
|
58
|
+
context_data['report_run_arguments_values'] = {}
|
|
59
|
+
|
|
60
|
+
for argument in report.run_arguments:
|
|
61
|
+
if context_data['report_run_arguments'][argument]['annotation'] == 'bool':
|
|
62
|
+
get_request_value = request.GET.get(argument, False)
|
|
63
|
+
else:
|
|
64
|
+
get_request_value = request.GET.get(argument, None)
|
|
65
|
+
|
|
66
|
+
context_data['report_run_arguments_values'][argument] = get_request_value
|
|
67
|
+
|
|
68
|
+
if request.GET.get('report_should_run', 'false').lower() == 'true':
|
|
69
|
+
for argument, value in context_data['report_run_arguments_values'].items():
|
|
70
|
+
if value is None:
|
|
71
|
+
break
|
|
72
|
+
else:
|
|
73
|
+
ReportRun.objects.create(
|
|
74
|
+
report_key_stack=report_key_stack,
|
|
75
|
+
)
|
|
76
|
+
report.run()
|
|
77
|
+
|
|
78
|
+
context_data['report'] = report
|
|
79
|
+
context_data['report_run_count'] = ReportRun.objects.run_count(report_key_stack)
|
|
80
|
+
|
|
81
|
+
else:
|
|
82
|
+
top_ten_report_runs = [
|
|
83
|
+
{
|
|
84
|
+
**report_run,
|
|
85
|
+
'report_key_stack_verbose': report_run['report_key_stack'].replace('|', ' > '),
|
|
86
|
+
}
|
|
87
|
+
for report_run in
|
|
88
|
+
ReportRun.objects.by_top_ten()
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
context_data['top_ten_report_runs'] = top_ten_report_runs
|
|
92
|
+
|
|
93
|
+
return portal_views.template_view(
|
|
94
|
+
request,
|
|
95
|
+
page_title='Reports',
|
|
96
|
+
page_description='More Reporting Info',
|
|
97
|
+
breadcrumbs=breadcrumbs,
|
|
98
|
+
context_data=context_data,
|
|
99
|
+
template='django_spire/metric/report/page/report_page.html'
|
|
100
|
+
)
|
django_spire/settings.py
CHANGED
|
@@ -5,6 +5,7 @@ DJANGO_SPIRE_AUTH_CONTROLLERS = {
|
|
|
5
5
|
'ai_chat': 'django_spire.ai.chat.auth.controller.BaseAiChatAuthController',
|
|
6
6
|
'help_desk': 'django_spire.help_desk.auth.controller.BaseHelpDeskAuthController',
|
|
7
7
|
'knowledge': 'django_spire.knowledge.auth.controller.BaseKnowledgeAuthController',
|
|
8
|
+
'report': 'django_spire.metric.report.auth.controller.BaseReportAuthController',
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
# AI Settings
|
|
@@ -23,6 +24,8 @@ DJANGO_SPIRE_AI_INTENT_CHAT_ROUTERS = {
|
|
|
23
24
|
},
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
DJANGO_SPIRE_REPORT_REGISTRIES = []
|
|
28
|
+
|
|
26
29
|
# Theme Settings
|
|
27
30
|
DJANGO_SPIRE_DEFAULT_THEME = 'default-light'
|
|
28
31
|
DJANGO_SPIRE_THEME_PATH = '/static/django_spire/css/themes/{family}/app-{mode}.css'
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-spire
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.24.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.
|