wbreport 2.2.1__py2.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.
Potentially problematic release.
This version of wbreport might be problematic. Click here for more details.
- wbreport/__init__.py +1 -0
- wbreport/admin.py +87 -0
- wbreport/apps.py +6 -0
- wbreport/defaults/__init__.py +0 -0
- wbreport/defaults/factsheets/__init__.py +0 -0
- wbreport/defaults/factsheets/base.py +990 -0
- wbreport/defaults/factsheets/menu.py +93 -0
- wbreport/defaults/factsheets/mixins.py +35 -0
- wbreport/defaults/factsheets/multitheme.py +947 -0
- wbreport/dynamic_preferences_registry.py +15 -0
- wbreport/factories/__init__.py +8 -0
- wbreport/factories/data_classes.py +48 -0
- wbreport/factories/reports.py +79 -0
- wbreport/filters.py +37 -0
- wbreport/migrations/0001_initial_squashed_squashed_0007_report_key.py +238 -0
- wbreport/migrations/0008_alter_report_file_content_type.py +25 -0
- wbreport/migrations/0009_alter_report_color_palette.py +27 -0
- wbreport/migrations/0010_auto_20240103_0947.py +43 -0
- wbreport/migrations/0011_auto_20240207_1629.py +35 -0
- wbreport/migrations/0012_reportversion_lock.py +17 -0
- wbreport/migrations/0013_alter_reportversion_context.py +18 -0
- wbreport/migrations/0014_alter_reportcategory_options_and_more.py +25 -0
- wbreport/migrations/__init__.py +0 -0
- wbreport/mixins.py +183 -0
- wbreport/models.py +781 -0
- wbreport/pdf/__init__.py +0 -0
- wbreport/pdf/charts/__init__.py +0 -0
- wbreport/pdf/charts/legend.py +15 -0
- wbreport/pdf/charts/pie.py +169 -0
- wbreport/pdf/charts/timeseries.py +77 -0
- wbreport/pdf/sandbox/__init__.py +0 -0
- wbreport/pdf/sandbox/run.py +17 -0
- wbreport/pdf/sandbox/templates/__init__.py +0 -0
- wbreport/pdf/sandbox/templates/basic_factsheet.py +908 -0
- wbreport/pdf/sandbox/templates/fund_factsheet.py +864 -0
- wbreport/pdf/sandbox/templates/long_industry_exposure_factsheet.py +898 -0
- wbreport/pdf/sandbox/templates/multistrat_factsheet.py +872 -0
- wbreport/pdf/sandbox/templates/testfile.pdf +434 -0
- wbreport/pdf/tables/__init__.py +0 -0
- wbreport/pdf/tables/aggregated_tables.py +156 -0
- wbreport/pdf/tables/data_tables.py +75 -0
- wbreport/serializers.py +191 -0
- wbreport/tasks.py +60 -0
- wbreport/templates/__init__.py +0 -0
- wbreport/templatetags/__init__.py +0 -0
- wbreport/templatetags/portfolio_tags.py +35 -0
- wbreport/tests/__init__.py +0 -0
- wbreport/tests/conftest.py +24 -0
- wbreport/tests/test_models.py +253 -0
- wbreport/tests/test_tasks.py +17 -0
- wbreport/tests/test_viewsets.py +0 -0
- wbreport/tests/tests.py +12 -0
- wbreport/urls.py +29 -0
- wbreport/urls_public.py +10 -0
- wbreport/viewsets/__init__.py +10 -0
- wbreport/viewsets/configs/__init__.py +18 -0
- wbreport/viewsets/configs/buttons.py +193 -0
- wbreport/viewsets/configs/displays.py +116 -0
- wbreport/viewsets/configs/endpoints.py +23 -0
- wbreport/viewsets/configs/menus.py +8 -0
- wbreport/viewsets/configs/titles.py +30 -0
- wbreport/viewsets/viewsets.py +330 -0
- wbreport-2.2.1.dist-info/METADATA +6 -0
- wbreport-2.2.1.dist-info/RECORD +66 -0
- wbreport-2.2.1.dist-info/WHEEL +5 -0
- wbreport-2.2.1.dist-info/licenses/LICENSE +4 -0
|
File without changes
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from reportlab.lib.colors import grey
|
|
3
|
+
from reportlab.lib.enums import TA_CENTER
|
|
4
|
+
from reportlab.lib.styles import ParagraphStyle
|
|
5
|
+
from reportlab.platypus import Paragraph, Spacer, Table, TableStyle
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_simple_aggregated_table(
|
|
9
|
+
df,
|
|
10
|
+
width,
|
|
11
|
+
row_height,
|
|
12
|
+
header_style,
|
|
13
|
+
row_style,
|
|
14
|
+
data_style,
|
|
15
|
+
grid_color,
|
|
16
|
+
offset=None,
|
|
17
|
+
debug=False,
|
|
18
|
+
):
|
|
19
|
+
table_data = list()
|
|
20
|
+
|
|
21
|
+
# Generate header column
|
|
22
|
+
table_row = [Spacer(width=0, height=0)] * 2 if offset else 1
|
|
23
|
+
|
|
24
|
+
years = list(df.keys())
|
|
25
|
+
months = df[years[0]].keys()
|
|
26
|
+
for column in months:
|
|
27
|
+
table_row.append(Paragraph(str(column).upper(), style=header_style))
|
|
28
|
+
table_data.append(table_row)
|
|
29
|
+
data_style_fake = ParagraphStyle(
|
|
30
|
+
fontName="customfont", fontSize=6, leading=6, name="s_table_center", alignment=TA_CENTER, textColor=grey
|
|
31
|
+
)
|
|
32
|
+
# Generate table
|
|
33
|
+
for year, row in df.items():
|
|
34
|
+
table_row = list()
|
|
35
|
+
if offset:
|
|
36
|
+
table_row.append(Spacer(width=0, height=0))
|
|
37
|
+
|
|
38
|
+
table_row.append(Paragraph(str(year), style=row_style))
|
|
39
|
+
|
|
40
|
+
for month, element in row.items():
|
|
41
|
+
if element.get("performance", None) is None:
|
|
42
|
+
table_row.append(Spacer(width=0, height=0))
|
|
43
|
+
else:
|
|
44
|
+
value = element["performance"]
|
|
45
|
+
value_str = f"{value:.1%}" if (isinstance(value, float) and not np.isinf(value)) else str(value)
|
|
46
|
+
if element.get("calculated", False):
|
|
47
|
+
table_row.append(Paragraph(value_str, style=data_style_fake))
|
|
48
|
+
else:
|
|
49
|
+
table_row.append(Paragraph(value_str, style=row_style))
|
|
50
|
+
# else:
|
|
51
|
+
# table_row.append(
|
|
52
|
+
# Paragraph(f"<strong>{row.iloc[-1]:.1f}%</strong>", style=data_style)
|
|
53
|
+
# )
|
|
54
|
+
table_data.append(table_row)
|
|
55
|
+
|
|
56
|
+
num_cols = len(months) + 1
|
|
57
|
+
|
|
58
|
+
cols = list()
|
|
59
|
+
if offset:
|
|
60
|
+
cols.append(offset)
|
|
61
|
+
|
|
62
|
+
cols.extend([(width - offset or 0) / num_cols] * num_cols)
|
|
63
|
+
rows = [row_height] * (len(years) + 1)
|
|
64
|
+
|
|
65
|
+
table = Table(table_data, colWidths=cols, rowHeights=rows)
|
|
66
|
+
|
|
67
|
+
table_styles = [
|
|
68
|
+
("LINEBEFORE", (2, 0), (2, -1), 0.25, grid_color),
|
|
69
|
+
("LINEBEFORE", (-1, 0), (-1, -1), 0.25, grid_color),
|
|
70
|
+
("LINEABOVE", (1, 1), (-1, 1), 0.25, grid_color),
|
|
71
|
+
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
|
72
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 0),
|
|
73
|
+
("RIGHTPADDING", (2, 1), (-1, -1), offset / 2),
|
|
74
|
+
]
|
|
75
|
+
|
|
76
|
+
if debug:
|
|
77
|
+
table_styles.extend(
|
|
78
|
+
[
|
|
79
|
+
("BOX", (0, 0), (-1, -1), 0.25, grid_color),
|
|
80
|
+
("INNERGRID", (0, 0), (-1, -1), 0.25, grid_color),
|
|
81
|
+
]
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
table.setStyle(TableStyle(table_styles))
|
|
85
|
+
return table
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def get_fund_table(
|
|
89
|
+
df,
|
|
90
|
+
width,
|
|
91
|
+
row_height,
|
|
92
|
+
header_style,
|
|
93
|
+
row_style,
|
|
94
|
+
data_style,
|
|
95
|
+
grid_color,
|
|
96
|
+
background_color,
|
|
97
|
+
offset=None,
|
|
98
|
+
debug=False,
|
|
99
|
+
):
|
|
100
|
+
table_data = [
|
|
101
|
+
[Paragraph("<strong>THE ATONRÂ FUND SHARE CLASSES AND LOADS</strong>", style=header_style)],
|
|
102
|
+
[Spacer(width=0, height=0)],
|
|
103
|
+
]
|
|
104
|
+
|
|
105
|
+
# Generate header column
|
|
106
|
+
table_row = []
|
|
107
|
+
labels = df.columns
|
|
108
|
+
for label in labels:
|
|
109
|
+
table_row.append(Paragraph(f"<strong>{str(label).upper()}</strong>", style=header_style))
|
|
110
|
+
table_data.append(table_row)
|
|
111
|
+
|
|
112
|
+
# Generate table
|
|
113
|
+
for index, row in df.iterrows():
|
|
114
|
+
table_row = list()
|
|
115
|
+
|
|
116
|
+
for value in row:
|
|
117
|
+
# value = product_data[label]
|
|
118
|
+
# if isinstance(value, (Decimal, int, float)):
|
|
119
|
+
# value_str = f"{value:.1%}"
|
|
120
|
+
# else:
|
|
121
|
+
# value_str = str(value)
|
|
122
|
+
value_str = value if value else ""
|
|
123
|
+
table_row.append(Paragraph(str(value_str), style=row_style))
|
|
124
|
+
table_data.append(table_row)
|
|
125
|
+
|
|
126
|
+
num_cols = len(labels)
|
|
127
|
+
|
|
128
|
+
cols = list()
|
|
129
|
+
|
|
130
|
+
cols.extend([(width) / num_cols] * num_cols)
|
|
131
|
+
rows = [row_height] * (df.shape[0] + 3)
|
|
132
|
+
|
|
133
|
+
table = Table(table_data, colWidths=cols, rowHeights=rows)
|
|
134
|
+
|
|
135
|
+
table_styles = [
|
|
136
|
+
("LINEBEFORE", (1, 0), (1, -1), 0.25, grid_color),
|
|
137
|
+
("LINEABOVE", (0, 1), (-1, 1), 0.25, grid_color),
|
|
138
|
+
("LINEABOVE", (0, 0), (-1, 0), 0.25, grid_color),
|
|
139
|
+
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
|
140
|
+
("RIGHTPADDING", (0, 0), (-1, -1), 0),
|
|
141
|
+
("RIGHTPADDING", (2, 1), (-1, -1), offset / 2),
|
|
142
|
+
("BACKGROUND", (0, 0), (-1, 0), background_color),
|
|
143
|
+
("SPAN", (0, 0), (-1, 0)), # Col Span Title
|
|
144
|
+
("SPAN", (0, 1), (-1, 1)),
|
|
145
|
+
]
|
|
146
|
+
|
|
147
|
+
if debug:
|
|
148
|
+
table_styles.extend(
|
|
149
|
+
[
|
|
150
|
+
("BOX", (0, 0), (-1, -1), 0.25, grid_color),
|
|
151
|
+
("INNERGRID", (0, 0), (-1, -1), 0.25, grid_color),
|
|
152
|
+
]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
table.setStyle(TableStyle(table_styles))
|
|
156
|
+
return table
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from reportlab.lib.units import cm
|
|
2
|
+
from reportlab.platypus import Paragraph, Spacer, Table, TableStyle
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def get_simple_data_table(
|
|
6
|
+
headers,
|
|
7
|
+
data,
|
|
8
|
+
width,
|
|
9
|
+
header_row_height,
|
|
10
|
+
data_row_height,
|
|
11
|
+
margin,
|
|
12
|
+
header_style,
|
|
13
|
+
data_style,
|
|
14
|
+
grid_color,
|
|
15
|
+
offset=None,
|
|
16
|
+
debug=False,
|
|
17
|
+
):
|
|
18
|
+
table_data = list()
|
|
19
|
+
|
|
20
|
+
# Generate Headers
|
|
21
|
+
table_row = list()
|
|
22
|
+
if offset:
|
|
23
|
+
table_row.append(Spacer(width=0, height=0))
|
|
24
|
+
|
|
25
|
+
for header in headers[:-1]:
|
|
26
|
+
table_row.extend([Paragraph(header, style=header_style), Spacer(width=0, height=0)])
|
|
27
|
+
else:
|
|
28
|
+
table_row.append(Paragraph(headers[-1], style=header_style))
|
|
29
|
+
|
|
30
|
+
table_data.append(table_row)
|
|
31
|
+
table_data.append([Spacer(width=0, height=0) for _ in range(len(data) + (1 if offset else 0))])
|
|
32
|
+
for row in data:
|
|
33
|
+
table_row = list()
|
|
34
|
+
if offset:
|
|
35
|
+
table_row.append(Spacer(width=0, height=0))
|
|
36
|
+
|
|
37
|
+
for item in row[:-1]:
|
|
38
|
+
table_row.extend([Paragraph(item, style=data_style), Spacer(width=0, height=0)])
|
|
39
|
+
else:
|
|
40
|
+
table_row.append(Paragraph(row[-1], style=data_style))
|
|
41
|
+
|
|
42
|
+
table_data.append(table_row)
|
|
43
|
+
|
|
44
|
+
rows = [header_row_height, 0.16 * cm]
|
|
45
|
+
rows.extend([data_row_height] * len(data))
|
|
46
|
+
|
|
47
|
+
cols = list()
|
|
48
|
+
if offset:
|
|
49
|
+
cols.append(offset)
|
|
50
|
+
|
|
51
|
+
col_width = (width - (offset or 0) - (len(headers) - 1) * margin) / len(headers)
|
|
52
|
+
cols.extend([col_width, margin] * len(headers))
|
|
53
|
+
|
|
54
|
+
table = Table(table_data, colWidths=cols, rowHeights=rows)
|
|
55
|
+
table_styles = [
|
|
56
|
+
("LINEABOVE", (1, 0), (1, 0), 0.25, grid_color),
|
|
57
|
+
("LINEBELOW", (1, 0), (1, 0), 0.25, grid_color),
|
|
58
|
+
("LINEABOVE", (3, 0), (3, 0), 0.25, grid_color),
|
|
59
|
+
("LINEBELOW", (3, 0), (3, 0), 0.25, grid_color),
|
|
60
|
+
("LINEABOVE", (5, 0), (5, 0), 0.25, grid_color),
|
|
61
|
+
("LINEBELOW", (5, 0), (5, 0), 0.25, grid_color),
|
|
62
|
+
("VALIGN", (0, 0), (-1, 0), "MIDDLE"),
|
|
63
|
+
("LEFTPADDING", (0, 0), (-1, -1), 0),
|
|
64
|
+
]
|
|
65
|
+
|
|
66
|
+
if debug:
|
|
67
|
+
table_styles.extend(
|
|
68
|
+
[
|
|
69
|
+
("BOX", (0, 0), (-1, -1), 0.25, grid_color),
|
|
70
|
+
("INNERGRID", (0, 0), (-1, -1), 0.25, grid_color),
|
|
71
|
+
]
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
table.setStyle(TableStyle(table_styles))
|
|
75
|
+
return table
|
wbreport/serializers.py
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
from rest_framework.reverse import reverse
|
|
2
|
+
from wbcore import serializers as wb_serializers
|
|
3
|
+
from wbcore.content_type.serializers import (
|
|
4
|
+
ContentTypeRepresentationSerializer,
|
|
5
|
+
DynamicObjectIDRepresentationSerializer,
|
|
6
|
+
)
|
|
7
|
+
from wbcore.contrib.authentication.authentication import inject_short_lived_token
|
|
8
|
+
from wbmailing.serializers import MailingListRepresentationSerializer
|
|
9
|
+
|
|
10
|
+
from .models import Report, ReportCategory, ReportClass, ReportVersion
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ReportVersionRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
14
|
+
class Meta:
|
|
15
|
+
model = ReportVersion
|
|
16
|
+
fields = (
|
|
17
|
+
"id",
|
|
18
|
+
"title",
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ReportClassRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
23
|
+
class Meta:
|
|
24
|
+
model = ReportClass
|
|
25
|
+
fields = ("id", "title")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class ReportCategoryRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
29
|
+
class Meta:
|
|
30
|
+
model = ReportCategory
|
|
31
|
+
fields = ("id", "title")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ReportRepresentationSerializer(wb_serializers.RepresentationSerializer):
|
|
35
|
+
_detail = wb_serializers.HyperlinkField(reverse_name="report:report-detail")
|
|
36
|
+
|
|
37
|
+
class Meta:
|
|
38
|
+
model = Report
|
|
39
|
+
fields = ("id", "title", "_detail")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class ReportCategoryModelSerializer(wb_serializers.ModelSerializer):
|
|
43
|
+
class Meta:
|
|
44
|
+
model = ReportCategory
|
|
45
|
+
fields = ("id", "title")
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class ReportVersionModelSerializer(wb_serializers.ModelSerializer):
|
|
49
|
+
_report = ReportRepresentationSerializer(source="report")
|
|
50
|
+
parameters = wb_serializers.JSONTableField()
|
|
51
|
+
|
|
52
|
+
@wb_serializers.register_resource()
|
|
53
|
+
def version_resources(self, instance, request, user):
|
|
54
|
+
res = {}
|
|
55
|
+
if instance.report.is_accessible(user) and instance.report.is_active and not instance.disabled:
|
|
56
|
+
if user.is_superuser:
|
|
57
|
+
res["update_context"] = reverse(
|
|
58
|
+
"wbreport:reportversion-updatecontext", args=[instance.id], request=request
|
|
59
|
+
)
|
|
60
|
+
if not instance.report.file_disabled:
|
|
61
|
+
res["file"] = reverse("report:reportversion-file", args=[instance.id], request=request)
|
|
62
|
+
|
|
63
|
+
res["html"] = reverse("report:reportversion-html", args=[instance.id], request=request)
|
|
64
|
+
if instance.report.mailing_list:
|
|
65
|
+
res["send_email"] = reverse("report:reportversion-sendemail", args=[instance.id], request=request)
|
|
66
|
+
return res
|
|
67
|
+
|
|
68
|
+
from wbcore.contrib.authentication.authentication import TokenAuthentication
|
|
69
|
+
|
|
70
|
+
@wb_serializers.register_resource()
|
|
71
|
+
@inject_short_lived_token(view_name="public_report:report_version")
|
|
72
|
+
def public_html_resources(self, instance, request, user):
|
|
73
|
+
res = {}
|
|
74
|
+
if instance.report.is_accessible(user) and instance.report.is_active and not instance.disabled:
|
|
75
|
+
res["public_html"] = reverse("public_report:report_version", args=[instance.lookup], request=request)
|
|
76
|
+
return res
|
|
77
|
+
|
|
78
|
+
class Meta:
|
|
79
|
+
model = ReportVersion
|
|
80
|
+
fields = (
|
|
81
|
+
"id",
|
|
82
|
+
"uuid",
|
|
83
|
+
"title",
|
|
84
|
+
"creation_date",
|
|
85
|
+
"version_date",
|
|
86
|
+
"update_date",
|
|
87
|
+
"is_primary",
|
|
88
|
+
"disabled",
|
|
89
|
+
"lookup",
|
|
90
|
+
"parameters",
|
|
91
|
+
"comment",
|
|
92
|
+
"report",
|
|
93
|
+
"_report",
|
|
94
|
+
"parameters",
|
|
95
|
+
"_additional_resources",
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class ReportModelSerializer(wb_serializers.ModelSerializer):
|
|
100
|
+
_mailing_list = MailingListRepresentationSerializer(many=False, source="mailing_list")
|
|
101
|
+
_category = ReportCategoryRepresentationSerializer(source="category")
|
|
102
|
+
_report_class = ReportClassRepresentationSerializer(source="report_class")
|
|
103
|
+
_parent_report = ReportRepresentationSerializer(source="parent_report")
|
|
104
|
+
_content_type = ContentTypeRepresentationSerializer(source="content_type")
|
|
105
|
+
_object_id = DynamicObjectIDRepresentationSerializer(
|
|
106
|
+
source="object_id",
|
|
107
|
+
optional_get_parameters={"content_type": "content_type"},
|
|
108
|
+
depends_on=[{"field": "content_type", "options": {}}],
|
|
109
|
+
)
|
|
110
|
+
_group_key = wb_serializers.CharField(read_only=True)
|
|
111
|
+
|
|
112
|
+
@wb_serializers.register_resource()
|
|
113
|
+
def versions_resources(self, instance, request, user):
|
|
114
|
+
res = {}
|
|
115
|
+
if instance.is_accessible(user):
|
|
116
|
+
res["versions"] = reverse("wbreport:report-version-list", args=[instance.id], request=request)
|
|
117
|
+
res["reports"] = f'{reverse("wbreport:report-list", args=[], request=request)}?parent_report={instance.id}'
|
|
118
|
+
if instance.child_reports.exists():
|
|
119
|
+
res["bundle_versions"] = reverse("wbreport:report-bundletreport", args=[instance.id], request=request)
|
|
120
|
+
|
|
121
|
+
if user.is_superuser:
|
|
122
|
+
res.update(
|
|
123
|
+
{
|
|
124
|
+
"bulk_create_reports": reverse(
|
|
125
|
+
"wbreport:report-bulkcreatereport", args=[instance.id], request=request
|
|
126
|
+
),
|
|
127
|
+
"generate_next_reports": reverse(
|
|
128
|
+
"wbreport:report-generatenextreports", args=[instance.id], request=request
|
|
129
|
+
),
|
|
130
|
+
"switch_primary_versions": reverse(
|
|
131
|
+
"wbreport:report-switchprimaryversions", args=[instance.id], request=request
|
|
132
|
+
),
|
|
133
|
+
"update_versions_context": reverse(
|
|
134
|
+
"wbreport:report-updatecontext", args=[instance.id], request=request
|
|
135
|
+
),
|
|
136
|
+
}
|
|
137
|
+
)
|
|
138
|
+
return res
|
|
139
|
+
|
|
140
|
+
@wb_serializers.register_resource()
|
|
141
|
+
@inject_short_lived_token(view_name="public_report:report_version")
|
|
142
|
+
def public_resources(self, instance, request, user):
|
|
143
|
+
res = {}
|
|
144
|
+
if instance.is_accessible(user) and (
|
|
145
|
+
primary_snap := instance.versions.filter(is_primary=True, disabled=False).first()
|
|
146
|
+
):
|
|
147
|
+
res["primary_version"] = reverse(
|
|
148
|
+
"public_report:report_version", args=[primary_snap.lookup], request=request
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
if (
|
|
152
|
+
(last_version := instance.versions.latest("creation_date"))
|
|
153
|
+
and not last_version.disabled
|
|
154
|
+
and last_version != primary_snap
|
|
155
|
+
):
|
|
156
|
+
res["last_version"] = reverse(
|
|
157
|
+
"public_report:report_version", args=[last_version.lookup], request=request
|
|
158
|
+
)
|
|
159
|
+
return res
|
|
160
|
+
|
|
161
|
+
class Meta:
|
|
162
|
+
model = Report
|
|
163
|
+
dependency_map = {
|
|
164
|
+
"object_id": ["content_type"],
|
|
165
|
+
}
|
|
166
|
+
fields = (
|
|
167
|
+
"id",
|
|
168
|
+
"title",
|
|
169
|
+
"namespace",
|
|
170
|
+
"_mailing_list",
|
|
171
|
+
"mailing_list",
|
|
172
|
+
"category",
|
|
173
|
+
"_category",
|
|
174
|
+
"report_class",
|
|
175
|
+
"_report_class",
|
|
176
|
+
"is_active",
|
|
177
|
+
"base_color",
|
|
178
|
+
"permission_type",
|
|
179
|
+
"file_disabled",
|
|
180
|
+
"file_content_type",
|
|
181
|
+
"title",
|
|
182
|
+
"logo_file",
|
|
183
|
+
"_parent_report",
|
|
184
|
+
"parent_report",
|
|
185
|
+
"_content_type",
|
|
186
|
+
"object_id",
|
|
187
|
+
"_object_id",
|
|
188
|
+
"content_type",
|
|
189
|
+
"_additional_resources",
|
|
190
|
+
"_group_key",
|
|
191
|
+
)
|
wbreport/tasks.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import zipfile
|
|
2
|
+
from io import BytesIO
|
|
3
|
+
|
|
4
|
+
from celery import shared_task
|
|
5
|
+
from django.conf import settings
|
|
6
|
+
from django.core.mail import EmailMultiAlternatives
|
|
7
|
+
from django.template.loader import get_template
|
|
8
|
+
from slugify import slugify
|
|
9
|
+
from wbcore.contrib.authentication.models import User
|
|
10
|
+
from wbcore.utils.html import convert_html2text
|
|
11
|
+
from wbreport.models import Report, ReportVersion
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@shared_task()
|
|
15
|
+
def generate_and_send_current_report_file(user_id, parent_report_id, parameters=None):
|
|
16
|
+
zip_buffer = BytesIO()
|
|
17
|
+
parent_report = Report.objects.get(id=parent_report_id)
|
|
18
|
+
if not parameters:
|
|
19
|
+
parameters = parent_report.last_version.parameters
|
|
20
|
+
report_date = parent_report.report_class.get_version_date(parameters)
|
|
21
|
+
with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zip_file:
|
|
22
|
+
summary = ""
|
|
23
|
+
for version in ReportVersion.objects.filter(
|
|
24
|
+
parameters=parameters,
|
|
25
|
+
report__parent_report=parent_report,
|
|
26
|
+
report__is_active=True,
|
|
27
|
+
report__file_disabled=False,
|
|
28
|
+
disabled=False,
|
|
29
|
+
):
|
|
30
|
+
basename = f"report_{slugify(version.title)}"
|
|
31
|
+
try:
|
|
32
|
+
output = version.generate_file()
|
|
33
|
+
zip_file.writestr(output.name, output.getvalue())
|
|
34
|
+
msg = f"VALID: Report {basename} generated with success"
|
|
35
|
+
except Exception as e:
|
|
36
|
+
msg = f"ERROR: Could not generate Report {basename} (error: {e})"
|
|
37
|
+
print(msg) # noqa: T201
|
|
38
|
+
summary += f"{msg}\n"
|
|
39
|
+
|
|
40
|
+
zip_file.writestr("summary.txt", summary)
|
|
41
|
+
|
|
42
|
+
html = get_template("notifications/email_template.html")
|
|
43
|
+
notification = {
|
|
44
|
+
"message": f"Please find all the reports you requested for {parent_report.title}",
|
|
45
|
+
"title": "Your Report bundle",
|
|
46
|
+
}
|
|
47
|
+
html_content = html.render({"notification": notification})
|
|
48
|
+
|
|
49
|
+
msg = EmailMultiAlternatives(
|
|
50
|
+
"Your Report bundle",
|
|
51
|
+
body=convert_html2text(html_content),
|
|
52
|
+
from_email=settings.DEFAULT_FROM_EMAIL,
|
|
53
|
+
to=[User.objects.get(id=user_id).email],
|
|
54
|
+
)
|
|
55
|
+
msg.attach_alternative(html_content, "text/html")
|
|
56
|
+
|
|
57
|
+
zip_buffer.seek(0)
|
|
58
|
+
|
|
59
|
+
msg.attach(f"reports_bundle_{report_date}.zip", zip_buffer.read(), "application/zip")
|
|
60
|
+
msg.send()
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from datetime import date, datetime
|
|
2
|
+
from decimal import Decimal
|
|
3
|
+
|
|
4
|
+
from django import template
|
|
5
|
+
|
|
6
|
+
register = template.Library()
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@register.filter
|
|
10
|
+
def percent_filter(value, precision=2):
|
|
11
|
+
if value is not None:
|
|
12
|
+
if isinstance(value, float) or isinstance(value, Decimal):
|
|
13
|
+
return f"{value:,.{precision}%}"
|
|
14
|
+
else:
|
|
15
|
+
return value
|
|
16
|
+
return ""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@register.filter
|
|
20
|
+
def value_filter(value, precision=1):
|
|
21
|
+
if value is not None:
|
|
22
|
+
if isinstance(value, float) or isinstance(value, Decimal):
|
|
23
|
+
return "%.*f" % (precision, value)
|
|
24
|
+
elif isinstance(value, datetime) or isinstance(value, date):
|
|
25
|
+
return value.strftime("%d-%b-%y")
|
|
26
|
+
else:
|
|
27
|
+
return value
|
|
28
|
+
return ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@register.filter
|
|
32
|
+
def parse_title_date(value):
|
|
33
|
+
if value is not None and not isinstance(value, str):
|
|
34
|
+
return value.strftime("%B %Y")
|
|
35
|
+
return value
|
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from django.apps import apps
|
|
2
|
+
from django.db.models.signals import pre_migrate
|
|
3
|
+
from pytest_factoryboy import register
|
|
4
|
+
from wbcore.contrib.color.factories import ColorGradientFactory
|
|
5
|
+
from wbcore.contrib.geography.tests.signals import app_pre_migration
|
|
6
|
+
from wbmailing.factories import MailTemplateFactory
|
|
7
|
+
from wbreport.factories import (
|
|
8
|
+
ReportAssetFactory,
|
|
9
|
+
ReportCategoryFactory,
|
|
10
|
+
ReportClassFactory,
|
|
11
|
+
ReportFactory,
|
|
12
|
+
ReportVersionFactory,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
register(ColorGradientFactory)
|
|
16
|
+
register(ReportAssetFactory)
|
|
17
|
+
register(ReportCategoryFactory)
|
|
18
|
+
register(ReportClassFactory)
|
|
19
|
+
register(ReportFactory)
|
|
20
|
+
register(ReportVersionFactory)
|
|
21
|
+
register(MailTemplateFactory)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
pre_migrate.connect(app_pre_migration, sender=apps.get_app_config("wbreport"))
|