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.

Files changed (66) hide show
  1. wbreport/__init__.py +1 -0
  2. wbreport/admin.py +87 -0
  3. wbreport/apps.py +6 -0
  4. wbreport/defaults/__init__.py +0 -0
  5. wbreport/defaults/factsheets/__init__.py +0 -0
  6. wbreport/defaults/factsheets/base.py +990 -0
  7. wbreport/defaults/factsheets/menu.py +93 -0
  8. wbreport/defaults/factsheets/mixins.py +35 -0
  9. wbreport/defaults/factsheets/multitheme.py +947 -0
  10. wbreport/dynamic_preferences_registry.py +15 -0
  11. wbreport/factories/__init__.py +8 -0
  12. wbreport/factories/data_classes.py +48 -0
  13. wbreport/factories/reports.py +79 -0
  14. wbreport/filters.py +37 -0
  15. wbreport/migrations/0001_initial_squashed_squashed_0007_report_key.py +238 -0
  16. wbreport/migrations/0008_alter_report_file_content_type.py +25 -0
  17. wbreport/migrations/0009_alter_report_color_palette.py +27 -0
  18. wbreport/migrations/0010_auto_20240103_0947.py +43 -0
  19. wbreport/migrations/0011_auto_20240207_1629.py +35 -0
  20. wbreport/migrations/0012_reportversion_lock.py +17 -0
  21. wbreport/migrations/0013_alter_reportversion_context.py +18 -0
  22. wbreport/migrations/0014_alter_reportcategory_options_and_more.py +25 -0
  23. wbreport/migrations/__init__.py +0 -0
  24. wbreport/mixins.py +183 -0
  25. wbreport/models.py +781 -0
  26. wbreport/pdf/__init__.py +0 -0
  27. wbreport/pdf/charts/__init__.py +0 -0
  28. wbreport/pdf/charts/legend.py +15 -0
  29. wbreport/pdf/charts/pie.py +169 -0
  30. wbreport/pdf/charts/timeseries.py +77 -0
  31. wbreport/pdf/sandbox/__init__.py +0 -0
  32. wbreport/pdf/sandbox/run.py +17 -0
  33. wbreport/pdf/sandbox/templates/__init__.py +0 -0
  34. wbreport/pdf/sandbox/templates/basic_factsheet.py +908 -0
  35. wbreport/pdf/sandbox/templates/fund_factsheet.py +864 -0
  36. wbreport/pdf/sandbox/templates/long_industry_exposure_factsheet.py +898 -0
  37. wbreport/pdf/sandbox/templates/multistrat_factsheet.py +872 -0
  38. wbreport/pdf/sandbox/templates/testfile.pdf +434 -0
  39. wbreport/pdf/tables/__init__.py +0 -0
  40. wbreport/pdf/tables/aggregated_tables.py +156 -0
  41. wbreport/pdf/tables/data_tables.py +75 -0
  42. wbreport/serializers.py +191 -0
  43. wbreport/tasks.py +60 -0
  44. wbreport/templates/__init__.py +0 -0
  45. wbreport/templatetags/__init__.py +0 -0
  46. wbreport/templatetags/portfolio_tags.py +35 -0
  47. wbreport/tests/__init__.py +0 -0
  48. wbreport/tests/conftest.py +24 -0
  49. wbreport/tests/test_models.py +253 -0
  50. wbreport/tests/test_tasks.py +17 -0
  51. wbreport/tests/test_viewsets.py +0 -0
  52. wbreport/tests/tests.py +12 -0
  53. wbreport/urls.py +29 -0
  54. wbreport/urls_public.py +10 -0
  55. wbreport/viewsets/__init__.py +10 -0
  56. wbreport/viewsets/configs/__init__.py +18 -0
  57. wbreport/viewsets/configs/buttons.py +193 -0
  58. wbreport/viewsets/configs/displays.py +116 -0
  59. wbreport/viewsets/configs/endpoints.py +23 -0
  60. wbreport/viewsets/configs/menus.py +8 -0
  61. wbreport/viewsets/configs/titles.py +30 -0
  62. wbreport/viewsets/viewsets.py +330 -0
  63. wbreport-2.2.1.dist-info/METADATA +6 -0
  64. wbreport-2.2.1.dist-info/RECORD +66 -0
  65. wbreport-2.2.1.dist-info/WHEEL +5 -0
  66. 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
@@ -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"))