wbcompliance 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.
- wbcompliance/__init__.py +1 -0
- wbcompliance/admin/__init__.py +16 -0
- wbcompliance/admin/compliance_form.py +56 -0
- wbcompliance/admin/compliance_task.py +135 -0
- wbcompliance/admin/compliance_type.py +8 -0
- wbcompliance/admin/risk_management/__init__.py +3 -0
- wbcompliance/admin/risk_management/checks.py +7 -0
- wbcompliance/admin/risk_management/incidents.py +50 -0
- wbcompliance/admin/risk_management/rules.py +63 -0
- wbcompliance/admin/utils.py +46 -0
- wbcompliance/apps.py +14 -0
- wbcompliance/factories/__init__.py +21 -0
- wbcompliance/factories/compliance.py +246 -0
- wbcompliance/factories/risk_management/__init__.py +12 -0
- wbcompliance/factories/risk_management/backends.py +42 -0
- wbcompliance/factories/risk_management/checks.py +12 -0
- wbcompliance/factories/risk_management/incidents.py +84 -0
- wbcompliance/factories/risk_management/rules.py +100 -0
- wbcompliance/filters/__init__.py +2 -0
- wbcompliance/filters/compliances.py +189 -0
- wbcompliance/filters/risk_management/__init__.py +3 -0
- wbcompliance/filters/risk_management/checks.py +22 -0
- wbcompliance/filters/risk_management/incidents.py +113 -0
- wbcompliance/filters/risk_management/rules.py +110 -0
- wbcompliance/filters/risk_management/tables.py +112 -0
- wbcompliance/filters/risk_management/utils.py +3 -0
- wbcompliance/management/__init__.py +10 -0
- wbcompliance/migrations/0001_initial_squashed_squashed_0010_alter_checkedobjectincidentrelationship_resolved_by_and_more.py +1744 -0
- wbcompliance/migrations/0011_alter_riskrule_parameters.py +21 -0
- wbcompliance/migrations/0012_alter_compliancetype_options.py +20 -0
- wbcompliance/migrations/0013_alter_riskrule_unique_together.py +16 -0
- wbcompliance/migrations/0014_alter_reviewcompliancetask_year.py +27 -0
- wbcompliance/migrations/0015_auto_20240103_0957.py +43 -0
- wbcompliance/migrations/0016_checkedobjectincidentrelationship_report_details_and_more.py +37 -0
- wbcompliance/migrations/0017_alter_rulebackend_incident_report_template.py +20 -0
- wbcompliance/migrations/0018_alter_rulecheckedobjectrelationship_unique_together.py +39 -0
- wbcompliance/migrations/0019_rulegroup_riskrule_activation_date_and_more.py +60 -0
- wbcompliance/migrations/__init__.py +0 -0
- wbcompliance/models/__init__.py +20 -0
- wbcompliance/models/compliance_form.py +626 -0
- wbcompliance/models/compliance_task.py +800 -0
- wbcompliance/models/compliance_type.py +133 -0
- wbcompliance/models/enums.py +13 -0
- wbcompliance/models/risk_management/__init__.py +4 -0
- wbcompliance/models/risk_management/backend.py +139 -0
- wbcompliance/models/risk_management/checks.py +194 -0
- wbcompliance/models/risk_management/dispatch.py +41 -0
- wbcompliance/models/risk_management/incidents.py +619 -0
- wbcompliance/models/risk_management/mixins.py +115 -0
- wbcompliance/models/risk_management/rules.py +654 -0
- wbcompliance/permissions.py +32 -0
- wbcompliance/serializers/__init__.py +30 -0
- wbcompliance/serializers/compliance_form.py +320 -0
- wbcompliance/serializers/compliance_task.py +463 -0
- wbcompliance/serializers/compliance_type.py +26 -0
- wbcompliance/serializers/risk_management/__init__.py +19 -0
- wbcompliance/serializers/risk_management/checks.py +53 -0
- wbcompliance/serializers/risk_management/incidents.py +227 -0
- wbcompliance/serializers/risk_management/rules.py +158 -0
- wbcompliance/tasks.py +112 -0
- wbcompliance/tests/__init__.py +0 -0
- wbcompliance/tests/conftest.py +63 -0
- wbcompliance/tests/disable_signals.py +82 -0
- wbcompliance/tests/mixins.py +17 -0
- wbcompliance/tests/risk_management/__init__.py +0 -0
- wbcompliance/tests/risk_management/models/__init__.py +0 -0
- wbcompliance/tests/risk_management/models/test_backends.py +0 -0
- wbcompliance/tests/risk_management/models/test_checks.py +55 -0
- wbcompliance/tests/risk_management/models/test_incidents.py +327 -0
- wbcompliance/tests/risk_management/models/test_rules.py +255 -0
- wbcompliance/tests/signals.py +89 -0
- wbcompliance/tests/test_filters.py +23 -0
- wbcompliance/tests/test_models.py +57 -0
- wbcompliance/tests/test_serializers.py +48 -0
- wbcompliance/tests/test_views.py +377 -0
- wbcompliance/tests/tests.py +21 -0
- wbcompliance/urls.py +238 -0
- wbcompliance/viewsets/__init__.py +40 -0
- wbcompliance/viewsets/buttons/__init__.py +9 -0
- wbcompliance/viewsets/buttons/compliance_form.py +78 -0
- wbcompliance/viewsets/buttons/compliance_task.py +149 -0
- wbcompliance/viewsets/buttons/risk_managment/__init__.py +3 -0
- wbcompliance/viewsets/buttons/risk_managment/checks.py +11 -0
- wbcompliance/viewsets/buttons/risk_managment/incidents.py +51 -0
- wbcompliance/viewsets/buttons/risk_managment/rules.py +35 -0
- wbcompliance/viewsets/compliance_form.py +425 -0
- wbcompliance/viewsets/compliance_task.py +513 -0
- wbcompliance/viewsets/compliance_type.py +38 -0
- wbcompliance/viewsets/display/__init__.py +22 -0
- wbcompliance/viewsets/display/compliance_form.py +317 -0
- wbcompliance/viewsets/display/compliance_task.py +453 -0
- wbcompliance/viewsets/display/compliance_type.py +22 -0
- wbcompliance/viewsets/display/risk_managment/__init__.py +11 -0
- wbcompliance/viewsets/display/risk_managment/checks.py +46 -0
- wbcompliance/viewsets/display/risk_managment/incidents.py +155 -0
- wbcompliance/viewsets/display/risk_managment/rules.py +146 -0
- wbcompliance/viewsets/display/risk_managment/tables.py +51 -0
- wbcompliance/viewsets/endpoints/__init__.py +27 -0
- wbcompliance/viewsets/endpoints/compliance_form.py +207 -0
- wbcompliance/viewsets/endpoints/compliance_task.py +193 -0
- wbcompliance/viewsets/endpoints/compliance_type.py +9 -0
- wbcompliance/viewsets/endpoints/risk_managment/__init__.py +12 -0
- wbcompliance/viewsets/endpoints/risk_managment/checks.py +16 -0
- wbcompliance/viewsets/endpoints/risk_managment/incidents.py +36 -0
- wbcompliance/viewsets/endpoints/risk_managment/rules.py +32 -0
- wbcompliance/viewsets/endpoints/risk_managment/tables.py +14 -0
- wbcompliance/viewsets/menu/__init__.py +17 -0
- wbcompliance/viewsets/menu/compliance_form.py +49 -0
- wbcompliance/viewsets/menu/compliance_task.py +130 -0
- wbcompliance/viewsets/menu/compliance_type.py +17 -0
- wbcompliance/viewsets/menu/risk_management.py +56 -0
- wbcompliance/viewsets/risk_management/__init__.py +21 -0
- wbcompliance/viewsets/risk_management/checks.py +49 -0
- wbcompliance/viewsets/risk_management/incidents.py +204 -0
- wbcompliance/viewsets/risk_management/mixins.py +52 -0
- wbcompliance/viewsets/risk_management/rules.py +179 -0
- wbcompliance/viewsets/risk_management/tables.py +96 -0
- wbcompliance/viewsets/titles/__init__.py +17 -0
- wbcompliance/viewsets/titles/compliance_form.py +101 -0
- wbcompliance/viewsets/titles/compliance_task.py +60 -0
- wbcompliance/viewsets/titles/compliance_type.py +13 -0
- wbcompliance/viewsets/titles/risk_managment/__init__.py +1 -0
- wbcompliance/viewsets/titles/risk_managment/checks.py +0 -0
- wbcompliance/viewsets/titles/risk_managment/incidents.py +0 -0
- wbcompliance/viewsets/titles/risk_managment/rules.py +0 -0
- wbcompliance/viewsets/titles/risk_managment/tables.py +7 -0
- wbcompliance-2.2.1.dist-info/METADATA +7 -0
- wbcompliance-2.2.1.dist-info/RECORD +129 -0
- wbcompliance-2.2.1.dist-info/WHEEL +5 -0
|
@@ -0,0 +1,800 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import date, datetime, timedelta
|
|
3
|
+
|
|
4
|
+
from django.conf import settings
|
|
5
|
+
from django.contrib.auth import get_user_model
|
|
6
|
+
from django.core.validators import MaxValueValidator, MinValueValidator
|
|
7
|
+
from django.db import models
|
|
8
|
+
from django.db.models import Count, Max, Q, QuerySet
|
|
9
|
+
from django.db.models.signals import post_save
|
|
10
|
+
from django.dispatch import receiver
|
|
11
|
+
from django.template.loader import get_template
|
|
12
|
+
from django.utils import timezone
|
|
13
|
+
from django.utils.translation import gettext_lazy as _
|
|
14
|
+
from django_fsm import FSMField, transition
|
|
15
|
+
from pandas.tseries.offsets import MonthEnd, YearEnd
|
|
16
|
+
from wbcore.contrib.color.enums import WBColor
|
|
17
|
+
from wbcore.contrib.icons import WBIcon
|
|
18
|
+
from wbcore.contrib.notifications.dispatch import send_notification
|
|
19
|
+
from wbcore.contrib.notifications.utils import create_notification_type
|
|
20
|
+
from wbcore.enums import RequestType
|
|
21
|
+
from wbcore.markdown.utils import custom_url_fetcher
|
|
22
|
+
from wbcore.metadata.configs.buttons import ActionButton, ButtonDefaultColor
|
|
23
|
+
from wbcore.models import WBModel
|
|
24
|
+
from wbcore.models.fields import YearField
|
|
25
|
+
from wbcore.utils.models import ComplexToStringMixin
|
|
26
|
+
from weasyprint import HTML
|
|
27
|
+
|
|
28
|
+
from .compliance_type import ComplianceDocumentMixin, ComplianceType, can_active_request
|
|
29
|
+
from .enums import IncidentSeverity
|
|
30
|
+
|
|
31
|
+
User = get_user_model()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def can_draft_request(instance, user: "User") -> bool:
|
|
35
|
+
if instance.is_instance:
|
|
36
|
+
return False
|
|
37
|
+
return user.has_perm("wbcompliance.administrate_compliance")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class ComplianceTaskGroup(WBModel):
|
|
41
|
+
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
|
42
|
+
order = models.PositiveIntegerField(null=True, blank=True, verbose_name=_("Order"))
|
|
43
|
+
|
|
44
|
+
class Meta:
|
|
45
|
+
verbose_name = "Compliance Task Group"
|
|
46
|
+
verbose_name_plural = "Compliance Task Groups"
|
|
47
|
+
|
|
48
|
+
def __str__(self) -> str:
|
|
49
|
+
return "{}".format(self.name)
|
|
50
|
+
|
|
51
|
+
@classmethod
|
|
52
|
+
def get_endpoint_basename(cls) -> str:
|
|
53
|
+
return "wbcompliance:compliancetaskgroup"
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def get_representation_endpoint(cls) -> str:
|
|
57
|
+
return "wbcompliance:compliancetaskgrouprepresentation-list"
|
|
58
|
+
|
|
59
|
+
@classmethod
|
|
60
|
+
def get_representation_value_key(cls) -> str:
|
|
61
|
+
return "id"
|
|
62
|
+
|
|
63
|
+
@classmethod
|
|
64
|
+
def get_representation_label_key(cls) -> str:
|
|
65
|
+
return "{{name}}"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class ComplianceTask(WBModel):
|
|
69
|
+
class Occurrence(models.TextChoices):
|
|
70
|
+
YEARLY = "YEARLY", "Yearly"
|
|
71
|
+
QUARTERLY = "QUARTERLY", "Quarterly"
|
|
72
|
+
MONTHLY = "MONTHLY", "Monthly"
|
|
73
|
+
NEVER = "NEVER", "Never"
|
|
74
|
+
|
|
75
|
+
@classmethod
|
|
76
|
+
def get_color_map(cls) -> list:
|
|
77
|
+
colors = [WBColor.GREEN_LIGHT.value, WBColor.YELLOW_LIGHT.value, WBColor.RED_LIGHT.value]
|
|
78
|
+
return [choice for choice in zip(cls, colors)]
|
|
79
|
+
|
|
80
|
+
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
|
81
|
+
description = models.TextField(default="", blank=True, verbose_name=_("Description"))
|
|
82
|
+
occurrence = models.CharField(
|
|
83
|
+
max_length=32,
|
|
84
|
+
default=Occurrence.MONTHLY,
|
|
85
|
+
choices=Occurrence.choices,
|
|
86
|
+
verbose_name=_("Occurrence"),
|
|
87
|
+
)
|
|
88
|
+
active = models.BooleanField(default=True)
|
|
89
|
+
group = models.ForeignKey(
|
|
90
|
+
to="wbcompliance.ComplianceTaskGroup",
|
|
91
|
+
blank=True,
|
|
92
|
+
null=True,
|
|
93
|
+
on_delete=models.SET_NULL,
|
|
94
|
+
related_name="tasks_related",
|
|
95
|
+
verbose_name=_("Group"),
|
|
96
|
+
)
|
|
97
|
+
review = models.ManyToManyField(
|
|
98
|
+
"wbcompliance.ReviewComplianceTask",
|
|
99
|
+
related_name="tasks",
|
|
100
|
+
blank=True,
|
|
101
|
+
verbose_name=_("Review"),
|
|
102
|
+
help_text=_("list of reviews that contain this task"),
|
|
103
|
+
)
|
|
104
|
+
risk_level = models.CharField(
|
|
105
|
+
max_length=32,
|
|
106
|
+
blank=True,
|
|
107
|
+
null=True,
|
|
108
|
+
choices=IncidentSeverity.choices,
|
|
109
|
+
verbose_name=_("Risk Level"),
|
|
110
|
+
)
|
|
111
|
+
remarks = models.TextField(null=True, blank=True, verbose_name=_("Remarks"))
|
|
112
|
+
type = models.ForeignKey(
|
|
113
|
+
to=ComplianceType, on_delete=models.PROTECT, related_name="tasks_of_type", verbose_name=_("Type")
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
class Meta:
|
|
117
|
+
verbose_name = "Compliance Task"
|
|
118
|
+
verbose_name_plural = "Compliance Tasks"
|
|
119
|
+
|
|
120
|
+
def generate_compliance_task_instance(self, occured: date | None = None, link_instance_review: bool = False):
|
|
121
|
+
kwargs = {"task": self}
|
|
122
|
+
if _instance := ComplianceTaskInstance.objects.filter(task=self).last():
|
|
123
|
+
kwargs.update({"status": _instance.status, "text": _instance.text, "summary_text": _instance.summary_text})
|
|
124
|
+
new_instance = ComplianceTaskInstance.objects.create(**kwargs)
|
|
125
|
+
|
|
126
|
+
if occured:
|
|
127
|
+
new_instance.occured = occured
|
|
128
|
+
new_instance.save()
|
|
129
|
+
|
|
130
|
+
if link_instance_review:
|
|
131
|
+
for review in self.review.filter(Q(is_instance=False) & Q(review_task=None)):
|
|
132
|
+
qs = ReviewComplianceTask.objects.filter(review_task=review, is_instance=True)
|
|
133
|
+
if review_instance := qs.filter(occured=new_instance.occured).last():
|
|
134
|
+
new_instance.review.add(review_instance)
|
|
135
|
+
return new_instance
|
|
136
|
+
|
|
137
|
+
def __str__(self):
|
|
138
|
+
return "{}".format(self.title)
|
|
139
|
+
|
|
140
|
+
@classmethod
|
|
141
|
+
def get_endpoint_basename(cls) -> str:
|
|
142
|
+
return "wbcompliance:compliancetask"
|
|
143
|
+
|
|
144
|
+
@classmethod
|
|
145
|
+
def get_representation_endpoint(cls) -> str:
|
|
146
|
+
return "wbcompliance:compliancetaskrepresentation-list"
|
|
147
|
+
|
|
148
|
+
@classmethod
|
|
149
|
+
def get_representation_value_key(cls) -> str:
|
|
150
|
+
return "id"
|
|
151
|
+
|
|
152
|
+
@classmethod
|
|
153
|
+
def get_representation_label_key(cls) -> str:
|
|
154
|
+
return "{{title}}"
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class ComplianceTaskInstance(models.Model):
|
|
158
|
+
class Status(models.TextChoices):
|
|
159
|
+
NOT_CHECKED = "NOT_CHECKED", "Not Checked"
|
|
160
|
+
WARNING = "WARNING", "Warning"
|
|
161
|
+
FOR_INFO = "FOR_INFO", "For Info"
|
|
162
|
+
NOTHING_TO_REPORT = "NOTHING_TO_REPORT", "Nothing to Report"
|
|
163
|
+
BREACH = "BREACH", "Breach"
|
|
164
|
+
|
|
165
|
+
@classmethod
|
|
166
|
+
def get_color_map(cls) -> list:
|
|
167
|
+
colors = [
|
|
168
|
+
WBColor.GREY.value,
|
|
169
|
+
WBColor.YELLOW_LIGHT.value,
|
|
170
|
+
WBColor.YELLOW.value,
|
|
171
|
+
WBColor.BLUE_LIGHT.value,
|
|
172
|
+
WBColor.RED_LIGHT.value,
|
|
173
|
+
]
|
|
174
|
+
return [choice for choice in zip(cls, colors)]
|
|
175
|
+
|
|
176
|
+
task = models.ForeignKey(
|
|
177
|
+
on_delete=models.CASCADE,
|
|
178
|
+
to="wbcompliance.ComplianceTask",
|
|
179
|
+
related_name="task_instances_related",
|
|
180
|
+
verbose_name=_("Compliance Task"),
|
|
181
|
+
)
|
|
182
|
+
occured = models.DateField(auto_now_add=True, verbose_name=_("Occured"))
|
|
183
|
+
status = models.CharField(
|
|
184
|
+
max_length=32,
|
|
185
|
+
default=Status.NOT_CHECKED,
|
|
186
|
+
choices=Status.choices,
|
|
187
|
+
verbose_name=_("Status"),
|
|
188
|
+
)
|
|
189
|
+
text = models.TextField(default="", blank=True, verbose_name=_("Text"))
|
|
190
|
+
summary_text = models.TextField(null=True, blank=True, verbose_name=_("Summary Text"))
|
|
191
|
+
review = models.ManyToManyField(
|
|
192
|
+
"wbcompliance.ReviewComplianceTask",
|
|
193
|
+
related_name="task_instances",
|
|
194
|
+
blank=True,
|
|
195
|
+
verbose_name=_("Review"),
|
|
196
|
+
help_text=_("list of reviews that contain this task instance"),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
class Meta:
|
|
200
|
+
verbose_name = "Compliance Task Instance"
|
|
201
|
+
verbose_name_plural = "Compliance Task Instances"
|
|
202
|
+
|
|
203
|
+
@classmethod
|
|
204
|
+
def get_max_depth(cls) -> int | None:
|
|
205
|
+
return cls.objects.all().values("task").annotate(dcount=Count("task")).aggregate(max=Max("dcount")).get("max")
|
|
206
|
+
|
|
207
|
+
@classmethod
|
|
208
|
+
def get_dict_max_count_task(cls) -> dict:
|
|
209
|
+
if cls.get_max_depth():
|
|
210
|
+
return cls.objects.all().values("task").annotate(dcount=Count("task")).latest("dcount")
|
|
211
|
+
return {}
|
|
212
|
+
|
|
213
|
+
def __str__(self) -> str:
|
|
214
|
+
return "{}".format(self.task.title)
|
|
215
|
+
|
|
216
|
+
@classmethod
|
|
217
|
+
def get_endpoint_basename(cls) -> str:
|
|
218
|
+
return "wbcompliance:compliancetaskinstance"
|
|
219
|
+
|
|
220
|
+
@classmethod
|
|
221
|
+
def get_representation_value_key(cls) -> str:
|
|
222
|
+
return "id"
|
|
223
|
+
|
|
224
|
+
@classmethod
|
|
225
|
+
def get_representation_label_key(cls) -> str:
|
|
226
|
+
return "{{task__title}} : {{id}}"
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
class ComplianceAction(models.Model):
|
|
230
|
+
class Status(models.TextChoices):
|
|
231
|
+
TO_BE_DONE = "TO_BE_DONE", "To be done"
|
|
232
|
+
WORK_IN_PROGRESS = "WORK_IN_PROGRESS", "Work in Progress"
|
|
233
|
+
DONE = "DONE", "Done"
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def get_color_map(cls) -> list:
|
|
237
|
+
colors = [WBColor.GREY.value, WBColor.YELLOW_LIGHT.value, WBColor.GREEN_LIGHT.value]
|
|
238
|
+
return [choice for choice in zip(cls, colors)]
|
|
239
|
+
|
|
240
|
+
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
|
241
|
+
description = models.TextField(
|
|
242
|
+
default="",
|
|
243
|
+
null=True,
|
|
244
|
+
blank=True,
|
|
245
|
+
verbose_name=_("Description"),
|
|
246
|
+
)
|
|
247
|
+
summary_description = models.TextField(
|
|
248
|
+
null=True,
|
|
249
|
+
blank=True,
|
|
250
|
+
verbose_name=_("Summary Description"),
|
|
251
|
+
)
|
|
252
|
+
deadline = models.DateField(null=True, blank=True, verbose_name=_("Deadline"))
|
|
253
|
+
progress = models.FloatField(
|
|
254
|
+
default=0, validators=[MinValueValidator(0.0), MaxValueValidator(1.0)], verbose_name=_("Progress")
|
|
255
|
+
)
|
|
256
|
+
status = models.CharField(
|
|
257
|
+
max_length=32,
|
|
258
|
+
default=Status.TO_BE_DONE,
|
|
259
|
+
choices=Status.choices,
|
|
260
|
+
verbose_name=_("Status"),
|
|
261
|
+
)
|
|
262
|
+
type = models.ForeignKey(
|
|
263
|
+
to=ComplianceType, on_delete=models.PROTECT, related_name="actions_of_type", verbose_name=_("Type")
|
|
264
|
+
)
|
|
265
|
+
active = models.BooleanField(default=True, verbose_name=_("Active"))
|
|
266
|
+
creator = models.ForeignKey(
|
|
267
|
+
to="directory.Person",
|
|
268
|
+
null=True,
|
|
269
|
+
blank=True,
|
|
270
|
+
related_name="compliance_actions",
|
|
271
|
+
verbose_name=_("Creator"),
|
|
272
|
+
on_delete=models.SET_NULL,
|
|
273
|
+
)
|
|
274
|
+
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
275
|
+
changer = models.ForeignKey(
|
|
276
|
+
to="directory.Person",
|
|
277
|
+
null=True,
|
|
278
|
+
blank=True,
|
|
279
|
+
verbose_name=_("Changer"),
|
|
280
|
+
related_name="updated_actions",
|
|
281
|
+
on_delete=models.SET_NULL,
|
|
282
|
+
)
|
|
283
|
+
last_modified = models.DateTimeField(auto_now=True, verbose_name=_("Last modified"))
|
|
284
|
+
|
|
285
|
+
class Meta:
|
|
286
|
+
verbose_name = "Compliance Action"
|
|
287
|
+
verbose_name_plural = "Compliance Actions"
|
|
288
|
+
|
|
289
|
+
def __str__(self) -> str:
|
|
290
|
+
return "{}".format(self.title)
|
|
291
|
+
|
|
292
|
+
@classmethod
|
|
293
|
+
def get_endpoint_basename(cls) -> str:
|
|
294
|
+
return "wbcompliance:complianceaction"
|
|
295
|
+
|
|
296
|
+
@classmethod
|
|
297
|
+
def get_representation_value_key(cls) -> str:
|
|
298
|
+
return "id"
|
|
299
|
+
|
|
300
|
+
@classmethod
|
|
301
|
+
def get_representation_label_key(cls) -> str:
|
|
302
|
+
return "{{title}}"
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
class ComplianceEvent(models.Model):
|
|
306
|
+
class Type(models.TextChoices):
|
|
307
|
+
INCIDENT = "INCIDENT", "Incident"
|
|
308
|
+
INFO = "INFO", "Info"
|
|
309
|
+
|
|
310
|
+
@classmethod
|
|
311
|
+
def get_color_map(cls) -> list:
|
|
312
|
+
colors = [WBColor.GREY.value, WBColor.BLUE_LIGHT.value]
|
|
313
|
+
return [choice for choice in zip(cls, colors)]
|
|
314
|
+
|
|
315
|
+
type_event = models.CharField(
|
|
316
|
+
max_length=32,
|
|
317
|
+
default=Type.INCIDENT,
|
|
318
|
+
choices=Type.choices,
|
|
319
|
+
verbose_name=_("Type Event"),
|
|
320
|
+
)
|
|
321
|
+
level = models.CharField(
|
|
322
|
+
max_length=32,
|
|
323
|
+
default=IncidentSeverity.LOW,
|
|
324
|
+
choices=IncidentSeverity.choices,
|
|
325
|
+
verbose_name=_("Level"),
|
|
326
|
+
)
|
|
327
|
+
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
|
328
|
+
exec_summary = models.TextField(
|
|
329
|
+
null=True,
|
|
330
|
+
blank=True,
|
|
331
|
+
verbose_name=_("Executive Summary"),
|
|
332
|
+
)
|
|
333
|
+
exec_summary_board = models.TextField(
|
|
334
|
+
null=True,
|
|
335
|
+
blank=True,
|
|
336
|
+
verbose_name=_("Executive Summary for the Board"),
|
|
337
|
+
)
|
|
338
|
+
description = models.TextField(default="", blank=True, verbose_name=_("Description"))
|
|
339
|
+
actions_taken = models.TextField(default="", blank=True, verbose_name=_("Actions Taken"))
|
|
340
|
+
consequences = models.TextField(default="", blank=True, verbose_name=_("Consequences"))
|
|
341
|
+
future_suggestions = models.TextField(default="", blank=True, verbose_name=_("Future Suggestions"))
|
|
342
|
+
type = models.ForeignKey(
|
|
343
|
+
to=ComplianceType, on_delete=models.PROTECT, related_name="events_of_type", verbose_name=_("Type")
|
|
344
|
+
)
|
|
345
|
+
active = models.BooleanField(default=True)
|
|
346
|
+
creator = models.ForeignKey(
|
|
347
|
+
to="directory.Person",
|
|
348
|
+
null=True,
|
|
349
|
+
blank=True,
|
|
350
|
+
related_name="compliance_events",
|
|
351
|
+
verbose_name=_("Creator"),
|
|
352
|
+
on_delete=models.SET_NULL,
|
|
353
|
+
)
|
|
354
|
+
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
355
|
+
changer = models.ForeignKey(
|
|
356
|
+
to="directory.Person",
|
|
357
|
+
null=True,
|
|
358
|
+
blank=True,
|
|
359
|
+
related_name="updated_events",
|
|
360
|
+
verbose_name=_("Changer"),
|
|
361
|
+
on_delete=models.SET_NULL,
|
|
362
|
+
)
|
|
363
|
+
last_modified = models.DateTimeField(auto_now=True, verbose_name=_("Last modified"))
|
|
364
|
+
confidential = models.BooleanField(default=False, verbose_name=_("Confidential"))
|
|
365
|
+
|
|
366
|
+
class Meta:
|
|
367
|
+
verbose_name = "Compliance Event"
|
|
368
|
+
verbose_name_plural = "Compliance Events"
|
|
369
|
+
|
|
370
|
+
notification_types = [
|
|
371
|
+
create_notification_type(
|
|
372
|
+
code="wbcompliance.complianceevent.notify",
|
|
373
|
+
title="Compliance Event Notification",
|
|
374
|
+
help_text="Sends out a notification when a new compliance event was created.",
|
|
375
|
+
)
|
|
376
|
+
]
|
|
377
|
+
|
|
378
|
+
def __str__(self) -> str:
|
|
379
|
+
return "{}".format(self.title)
|
|
380
|
+
|
|
381
|
+
@classmethod
|
|
382
|
+
def get_endpoint_basename(cls) -> str:
|
|
383
|
+
return "wbcompliance:complianceevent"
|
|
384
|
+
|
|
385
|
+
@classmethod
|
|
386
|
+
def get_representation_value_key(cls) -> str:
|
|
387
|
+
return "id"
|
|
388
|
+
|
|
389
|
+
@classmethod
|
|
390
|
+
def get_representation_label_key(cls) -> str:
|
|
391
|
+
return "{{title}}"
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class ReviewComplianceTask(ComplianceDocumentMixin, ComplexToStringMixin, WBModel):
|
|
395
|
+
class Occurrence(models.TextChoices):
|
|
396
|
+
YEARLY = "YEARLY", "Yearly"
|
|
397
|
+
QUARTERLY = "QUARTERLY", "Quarterly"
|
|
398
|
+
MONTHLY = "MONTHLY", "Monthly"
|
|
399
|
+
NEVER = "NEVER", "Never"
|
|
400
|
+
|
|
401
|
+
class Status(models.TextChoices):
|
|
402
|
+
DRAFT = "DRAFT", "Draft"
|
|
403
|
+
VALIDATION_REQUESTED = "VALIDATION_REQUESTED", "Validation Requested"
|
|
404
|
+
VALIDATED = "VALIDATED", "Validated"
|
|
405
|
+
|
|
406
|
+
@classmethod
|
|
407
|
+
def get_color_map(cls) -> list:
|
|
408
|
+
colors = [
|
|
409
|
+
WBColor.BLUE_LIGHT.value,
|
|
410
|
+
WBColor.YELLOW_LIGHT.value,
|
|
411
|
+
WBColor.GREEN_LIGHT.value,
|
|
412
|
+
]
|
|
413
|
+
return [choice for choice in zip(cls, colors)]
|
|
414
|
+
|
|
415
|
+
class Meta:
|
|
416
|
+
verbose_name = "Review Compliance Task"
|
|
417
|
+
verbose_name_plural = "Review Compliance Tasks"
|
|
418
|
+
|
|
419
|
+
notification_types = [
|
|
420
|
+
create_notification_type(
|
|
421
|
+
code="wbcompliance.reviewcompliancetask.notify",
|
|
422
|
+
title="Compliance Task Review Notification",
|
|
423
|
+
help_text="Notifies you when a compliance task can be reviewed",
|
|
424
|
+
)
|
|
425
|
+
]
|
|
426
|
+
|
|
427
|
+
title = models.CharField(max_length=255, verbose_name=_("Title"))
|
|
428
|
+
from_date = models.DateField(null=True, blank=True, verbose_name=_("From"))
|
|
429
|
+
to_date = models.DateField(null=True, blank=True, verbose_name=_("To"))
|
|
430
|
+
description = models.TextField(default="", blank=True, verbose_name=_("Description"))
|
|
431
|
+
year = YearField(
|
|
432
|
+
validators=[MinValueValidator(1000), MaxValueValidator(9999)], null=True, blank=True, verbose_name=_("Year")
|
|
433
|
+
)
|
|
434
|
+
creator = models.ForeignKey(
|
|
435
|
+
to="directory.Person",
|
|
436
|
+
null=True,
|
|
437
|
+
blank=True,
|
|
438
|
+
verbose_name=_("Creator"),
|
|
439
|
+
related_name="author_review_compliane_tasks",
|
|
440
|
+
on_delete=models.CASCADE,
|
|
441
|
+
)
|
|
442
|
+
created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
|
|
443
|
+
changer = models.ForeignKey(
|
|
444
|
+
"directory.Person", null=True, blank=True, verbose_name=_("Changer"), on_delete=models.deletion.SET_NULL
|
|
445
|
+
)
|
|
446
|
+
changed = models.DateTimeField(auto_now=True, verbose_name=_("Changed"))
|
|
447
|
+
status = FSMField(default=Status.DRAFT, choices=Status.choices, verbose_name=_("Status"))
|
|
448
|
+
occurrence = models.CharField(
|
|
449
|
+
max_length=32,
|
|
450
|
+
default=Occurrence.MONTHLY,
|
|
451
|
+
choices=Occurrence.choices,
|
|
452
|
+
verbose_name=_("Occurrence"),
|
|
453
|
+
)
|
|
454
|
+
|
|
455
|
+
is_instance = models.BooleanField(default=False, verbose_name=_("Is occurrence"))
|
|
456
|
+
review_task = models.ForeignKey(
|
|
457
|
+
to="wbcompliance.ReviewComplianceTask",
|
|
458
|
+
blank=True,
|
|
459
|
+
null=True,
|
|
460
|
+
on_delete=models.SET_NULL,
|
|
461
|
+
related_name="review_tasks",
|
|
462
|
+
verbose_name=_("Parent Review"),
|
|
463
|
+
)
|
|
464
|
+
occured = models.DateField(null=True, blank=True, verbose_name=_("Occured Instance"))
|
|
465
|
+
type = models.ForeignKey(
|
|
466
|
+
to=ComplianceType,
|
|
467
|
+
on_delete=models.PROTECT,
|
|
468
|
+
related_name="reviewtask_of_type",
|
|
469
|
+
verbose_name=_("Type"),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
def notify(self, title, msg, recipients: QuerySet["User"]) -> None:
|
|
473
|
+
for user in recipients:
|
|
474
|
+
send_notification(
|
|
475
|
+
code="wbcompliance.reviewcompliancetask.notify",
|
|
476
|
+
title=title,
|
|
477
|
+
body=msg,
|
|
478
|
+
user=user,
|
|
479
|
+
reverse_name="wbcompliance:reviewcompliancetask-detail",
|
|
480
|
+
reverse_args=[self.id],
|
|
481
|
+
)
|
|
482
|
+
|
|
483
|
+
def _remove_styled_paragraph(self, html_content: str) -> str:
|
|
484
|
+
tags = ["p", "span"]
|
|
485
|
+
for tag in tags:
|
|
486
|
+
reg_str = "<" + tag + "(.*?)" + ">"
|
|
487
|
+
reg_style = '(style=".*?;")'
|
|
488
|
+
_attributs = re.findall(reg_str, html_content)
|
|
489
|
+
dict_attrs = {}
|
|
490
|
+
for attribut in _attributs:
|
|
491
|
+
dict_attrs[attribut] = re.sub(reg_style, "", attribut)
|
|
492
|
+
|
|
493
|
+
for _key, _value in dict_attrs.items():
|
|
494
|
+
html_content = re.sub(_key, _value, html_content)
|
|
495
|
+
|
|
496
|
+
html_content = re.sub("<p> </p>", "", html_content)
|
|
497
|
+
return html_content
|
|
498
|
+
|
|
499
|
+
def generate_pdf(self) -> bytes:
|
|
500
|
+
html = get_template("compliance/review_compliance_task_report.html")
|
|
501
|
+
table = {}
|
|
502
|
+
tasks = ComplianceTask.objects.filter(Q(review=self) & Q(active=True)).order_by("group__order")
|
|
503
|
+
group_ids = tasks.values_list("group", flat=True).distinct()
|
|
504
|
+
for group_id in group_ids:
|
|
505
|
+
if group_id:
|
|
506
|
+
group = ComplianceTaskGroup.objects.get(id=group_id)
|
|
507
|
+
table[group.id] = {"name": group.name, "tasks": {}}
|
|
508
|
+
else:
|
|
509
|
+
group_id = ""
|
|
510
|
+
group = None
|
|
511
|
+
table[""] = {"name": "", "tasks": {}}
|
|
512
|
+
|
|
513
|
+
for task in tasks.filter(group=group):
|
|
514
|
+
table[group_id]["tasks"][task.id] = {
|
|
515
|
+
"title": task.title,
|
|
516
|
+
"description": self._remove_styled_paragraph(task.description),
|
|
517
|
+
"risk_level": task.risk_level,
|
|
518
|
+
"remarks": task.remarks,
|
|
519
|
+
}
|
|
520
|
+
html_content = html.render(
|
|
521
|
+
{
|
|
522
|
+
"today": timezone.now(),
|
|
523
|
+
"review": self,
|
|
524
|
+
"table": table,
|
|
525
|
+
}
|
|
526
|
+
)
|
|
527
|
+
return HTML(
|
|
528
|
+
string=html_content, base_url=settings.BASE_ENDPOINT_URL, url_fetcher=custom_url_fetcher
|
|
529
|
+
).write_pdf()
|
|
530
|
+
|
|
531
|
+
def get_period_date(self, today: datetime | None = None):
|
|
532
|
+
if today is None:
|
|
533
|
+
today = timezone.now()
|
|
534
|
+
if self.occurrence == self.Occurrence.YEARLY:
|
|
535
|
+
from_date = date(today.year - 1, 1, 1)
|
|
536
|
+
to_date = from_date + YearEnd(1)
|
|
537
|
+
elif self.occurrence == self.Occurrence.QUARTERLY:
|
|
538
|
+
from_date = (today - MonthEnd(4)).date() + timedelta(days=1)
|
|
539
|
+
to_date = (today - MonthEnd(1)).date()
|
|
540
|
+
elif self.occurrence == self.Occurrence.MONTHLY:
|
|
541
|
+
from_date = (today - MonthEnd(2)).date() + timedelta(days=1)
|
|
542
|
+
to_date = (today - MonthEnd(1)).date()
|
|
543
|
+
else:
|
|
544
|
+
return None, None
|
|
545
|
+
return from_date, to_date
|
|
546
|
+
|
|
547
|
+
def generate_review_compliance_task_instance(
|
|
548
|
+
self, current_date: datetime | None = None, link_instance: bool = False, notify_admin: bool = False
|
|
549
|
+
) -> None:
|
|
550
|
+
"""
|
|
551
|
+
allow to generate the occurrence of the Indicators Report.
|
|
552
|
+
current_date: allow to find the year and the period date corresponding to the occurrence. we use today by default
|
|
553
|
+
"""
|
|
554
|
+
if current_date is None:
|
|
555
|
+
current_date = timezone.now()
|
|
556
|
+
from_date, to_date = self.get_period_date(current_date)
|
|
557
|
+
date_title = to_date if to_date else current_date
|
|
558
|
+
kwargs = {
|
|
559
|
+
"review_task": self,
|
|
560
|
+
"year": current_date.year,
|
|
561
|
+
"occured": current_date.date(),
|
|
562
|
+
"is_instance": True,
|
|
563
|
+
"status": ReviewComplianceTask.Status.DRAFT,
|
|
564
|
+
"creator": self.creator,
|
|
565
|
+
"changer": self.changer,
|
|
566
|
+
"occurrence": ReviewComplianceTask.Occurrence.NEVER,
|
|
567
|
+
"description": self.description,
|
|
568
|
+
"title": "{} - {}".format(self.title, date_title.strftime("%b %Y")),
|
|
569
|
+
"from_date": from_date,
|
|
570
|
+
"to_date": to_date,
|
|
571
|
+
"type": self.type,
|
|
572
|
+
}
|
|
573
|
+
if _instance := ReviewComplianceTask.objects.filter(Q(review_task=self) & Q(is_instance=True)).last():
|
|
574
|
+
kwargs.update({"creator": _instance.changer, "description": _instance.description})
|
|
575
|
+
new_review = ReviewComplianceTask.objects.create(**kwargs)
|
|
576
|
+
|
|
577
|
+
if link_instance:
|
|
578
|
+
for task in ComplianceTask.objects.filter(review__in=[self]):
|
|
579
|
+
if instance := ComplianceTaskInstance.objects.filter(task=task).last():
|
|
580
|
+
instance.review.add(new_review)
|
|
581
|
+
|
|
582
|
+
if notify_admin and (compliance_type := self.type):
|
|
583
|
+
recipients = ComplianceType.get_administrators(compliance_type)
|
|
584
|
+
msg = _("<b>{}</b> is available. You can now complete and validate it.").format(self.title)
|
|
585
|
+
title = _("Instance Indicator Report: {}").format(self.title)
|
|
586
|
+
self.notify(title, msg, recipients)
|
|
587
|
+
|
|
588
|
+
def get_task_group_ids_from_review(self, through_task: bool = True, task_with_group: bool = True) -> set:
|
|
589
|
+
if through_task:
|
|
590
|
+
return set(
|
|
591
|
+
self.tasks.filter(group__isnull=not (task_with_group))
|
|
592
|
+
.order_by("group__order")
|
|
593
|
+
.values_list("group", flat=True)
|
|
594
|
+
)
|
|
595
|
+
else:
|
|
596
|
+
return set(
|
|
597
|
+
self.task_instances.filter(task__group__isnull=not (task_with_group))
|
|
598
|
+
.order_by("task__group__order")
|
|
599
|
+
.values_list("task__group", flat=True)
|
|
600
|
+
)
|
|
601
|
+
|
|
602
|
+
@transition(
|
|
603
|
+
field=status,
|
|
604
|
+
source=Status.DRAFT,
|
|
605
|
+
target=Status.VALIDATION_REQUESTED,
|
|
606
|
+
permission=lambda _, user: user.has_perm("wbcompliance.administrate_compliance"),
|
|
607
|
+
custom={
|
|
608
|
+
"_transition_button": ActionButton(
|
|
609
|
+
method=RequestType.PATCH,
|
|
610
|
+
color=ButtonDefaultColor.WARNING,
|
|
611
|
+
identifiers=("wbcompliance:reviewcompliancetask",),
|
|
612
|
+
icon=WBIcon.SEND.icon,
|
|
613
|
+
key="validationrequested",
|
|
614
|
+
label="Request Validation",
|
|
615
|
+
action_label=_("Request Validation"),
|
|
616
|
+
description_fields=_(
|
|
617
|
+
"<p>Title: <b>{{title}}</b></p>\
|
|
618
|
+
<p>Status: <b>{{status}}</b></p> <p>From: <b>{{from_date}}</b></p>\
|
|
619
|
+
<p>To: <b>{{to_date}}</b></p> <p>Do you want to send this request for validation ?</p>"
|
|
620
|
+
),
|
|
621
|
+
)
|
|
622
|
+
},
|
|
623
|
+
)
|
|
624
|
+
def validationrequested(self, by=None, description=None, **kwargs):
|
|
625
|
+
if compliance_type := self.type:
|
|
626
|
+
# notify the compliance team without the current user
|
|
627
|
+
if by:
|
|
628
|
+
self.changer = by.profile
|
|
629
|
+
current_user = self.changer if self.changer else self.creator
|
|
630
|
+
recipients = ComplianceType.get_administrators(compliance_type).exclude(profile=current_user)
|
|
631
|
+
msg = _("Validation Request from {} to validate a ompliance Risk Review: <b>{}</b>").format(
|
|
632
|
+
str(current_user), self.title
|
|
633
|
+
)
|
|
634
|
+
title = _("Validation Requested Compliance Risk Review: {}").format(self.title)
|
|
635
|
+
self.notify(title, msg, recipients)
|
|
636
|
+
|
|
637
|
+
@transition(
|
|
638
|
+
field=status,
|
|
639
|
+
source=Status.VALIDATION_REQUESTED,
|
|
640
|
+
target=Status.DRAFT,
|
|
641
|
+
permission=lambda _, user: user.has_perm("wbcompliance.administrate_compliance"),
|
|
642
|
+
custom={
|
|
643
|
+
"_transition_button": ActionButton(
|
|
644
|
+
method=RequestType.PATCH,
|
|
645
|
+
color=ButtonDefaultColor.WARNING,
|
|
646
|
+
identifiers=("wbcompliance:reviewcompliancetask",),
|
|
647
|
+
icon=WBIcon.EDIT.icon,
|
|
648
|
+
key="draft",
|
|
649
|
+
label="Return to Draft Mode",
|
|
650
|
+
action_label=_("Return to Draft Mode"),
|
|
651
|
+
description_fields=_(
|
|
652
|
+
"""
|
|
653
|
+
<p>Title: <b> {{title}} </b></p>
|
|
654
|
+
<p>Status: <b>{{status}}</b></p> <p>From: <b>{{from_date}}</b></p> <p>To: <b>{{to_date}}</b></p>
|
|
655
|
+
<p>Do you want to return to draft ?</p>
|
|
656
|
+
"""
|
|
657
|
+
),
|
|
658
|
+
)
|
|
659
|
+
},
|
|
660
|
+
)
|
|
661
|
+
def draft(self, by=None, description=None, **kwargs):
|
|
662
|
+
if compliance_type := self.type:
|
|
663
|
+
if by:
|
|
664
|
+
self.changer = by.profile
|
|
665
|
+
current_user = self.changer if self.changer else self.creator
|
|
666
|
+
msg = _("{} has changed a Compliance Risk Review to Draft : <b>{}</b>").format(
|
|
667
|
+
str(current_user), self.title
|
|
668
|
+
)
|
|
669
|
+
title = _("Compliance Risk Review : {}").format(self.title)
|
|
670
|
+
recipients = ComplianceType.get_administrators(compliance_type).exclude(profile=current_user)
|
|
671
|
+
self.notify(title, msg, recipients)
|
|
672
|
+
|
|
673
|
+
@transition(
|
|
674
|
+
field=status,
|
|
675
|
+
source=Status.VALIDATION_REQUESTED,
|
|
676
|
+
target=Status.VALIDATED,
|
|
677
|
+
permission=can_active_request,
|
|
678
|
+
custom={
|
|
679
|
+
"_transition_button": ActionButton(
|
|
680
|
+
method=RequestType.PATCH,
|
|
681
|
+
color=ButtonDefaultColor.WARNING,
|
|
682
|
+
identifiers=("wbcompliance:reviewcompliancetask",),
|
|
683
|
+
icon=WBIcon.SEND.icon,
|
|
684
|
+
key="validation",
|
|
685
|
+
label="Validate",
|
|
686
|
+
action_label=_("Validate"),
|
|
687
|
+
description_fields=_(
|
|
688
|
+
"""
|
|
689
|
+
<p>Title: <b> {{title}} </b></p>
|
|
690
|
+
<p>Status: <b>{{status}}</b></p> <p>From: <b>{{from_date}}</b></p> <p>To: <b>{{to_date}}</b></p>
|
|
691
|
+
<p>Do you want to validate?</p>
|
|
692
|
+
"""
|
|
693
|
+
),
|
|
694
|
+
)
|
|
695
|
+
},
|
|
696
|
+
)
|
|
697
|
+
def validation(self, by=None, description=None, **kwargs):
|
|
698
|
+
if compliance_type := self.type:
|
|
699
|
+
if by:
|
|
700
|
+
self.changer = by.profile
|
|
701
|
+
current_user = self.changer if self.changer else self.creator
|
|
702
|
+
msg = _(
|
|
703
|
+
"""
|
|
704
|
+
{} has validated a Compliance Risk Review : <b>{}</b>
|
|
705
|
+
"""
|
|
706
|
+
).format(str(current_user), self.title)
|
|
707
|
+
title = _("Validation - Compliance Risk Review : {}").format(self.title)
|
|
708
|
+
recipients = ComplianceType.get_administrators(compliance_type).exclude(profile=current_user)
|
|
709
|
+
self.notify(title, msg, recipients)
|
|
710
|
+
|
|
711
|
+
@transition(
|
|
712
|
+
field=status,
|
|
713
|
+
source=Status.VALIDATED,
|
|
714
|
+
target=Status.DRAFT,
|
|
715
|
+
permission=can_draft_request,
|
|
716
|
+
custom={
|
|
717
|
+
"_transition_button": ActionButton(
|
|
718
|
+
method=RequestType.PATCH,
|
|
719
|
+
color=ButtonDefaultColor.WARNING,
|
|
720
|
+
identifiers=("wbcompliance:reviewcompliancetask",),
|
|
721
|
+
icon=WBIcon.EDIT.icon,
|
|
722
|
+
key="backtodraft",
|
|
723
|
+
label="Back to draft",
|
|
724
|
+
action_label=_("Back to draft"),
|
|
725
|
+
description_fields=_(
|
|
726
|
+
"""
|
|
727
|
+
<p>Title: <b> {{title}} </b></p>
|
|
728
|
+
<p>Status: <b>{{status}}</b></p> <p>From: <b>{{from_date}}</b></p> <p>To: <b>{{to_date}}</b></p>
|
|
729
|
+
<p>Do you want to return to the draft?</p>
|
|
730
|
+
"""
|
|
731
|
+
),
|
|
732
|
+
)
|
|
733
|
+
},
|
|
734
|
+
)
|
|
735
|
+
def backtodraft(self, by=None, description=None, **kwargs):
|
|
736
|
+
if compliance_type := self.type:
|
|
737
|
+
if by:
|
|
738
|
+
self.changer = by.profile
|
|
739
|
+
current_user = self.changer if self.changer else self.creator
|
|
740
|
+
msg = _("{} has drafted a Compliance Indicators Report : <b>{}</b>").format(str(current_user), self.title)
|
|
741
|
+
title = _("Compliance Indicators Report drafted: {}").format(self.title)
|
|
742
|
+
recipients = ComplianceType.get_administrators(compliance_type)
|
|
743
|
+
self.notify(title, msg, recipients)
|
|
744
|
+
|
|
745
|
+
def compute_str(self) -> str:
|
|
746
|
+
_str = "{}".format(self.title)
|
|
747
|
+
if self.from_date or self.to_date:
|
|
748
|
+
_str += " - ({} - {})".format(self.from_date, self.to_date)
|
|
749
|
+
return _str
|
|
750
|
+
|
|
751
|
+
def __str__(self) -> str:
|
|
752
|
+
return self.computed_str
|
|
753
|
+
|
|
754
|
+
def save(self, *args, **kwargs):
|
|
755
|
+
self.computed_str = self.compute_str()
|
|
756
|
+
super().save(*args, **kwargs)
|
|
757
|
+
|
|
758
|
+
@classmethod
|
|
759
|
+
def get_endpoint_basename(cls) -> str:
|
|
760
|
+
return "wbcompliance:reviewcompliancetask"
|
|
761
|
+
|
|
762
|
+
@classmethod
|
|
763
|
+
def get_representation_endpoint(cls) -> str:
|
|
764
|
+
return "wbcompliance:reviewcompliancetaskrepresentation-list"
|
|
765
|
+
|
|
766
|
+
@classmethod
|
|
767
|
+
def get_representation_value_key(cls) -> str:
|
|
768
|
+
return "id"
|
|
769
|
+
|
|
770
|
+
@classmethod
|
|
771
|
+
def get_representation_label_key(cls) -> str:
|
|
772
|
+
return "{{computed_str}}"
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
@receiver(post_save, sender=ComplianceEvent)
|
|
776
|
+
def post_save_compliance_event(sender, instance, created, **kwargs):
|
|
777
|
+
"""
|
|
778
|
+
Send notification to administrators
|
|
779
|
+
"""
|
|
780
|
+
if created and (compliance_type := instance.type):
|
|
781
|
+
current_profile = instance.changer if instance.changer else instance.creator
|
|
782
|
+
recipients = ComplianceType.get_administrators(compliance_type).exclude(profile=current_profile)
|
|
783
|
+
title = "{}: {}".format(ComplianceEvent.Type[instance.type_event].label, instance.title)
|
|
784
|
+
msg = _("<p>An {} Event was created by {} {} at {}</p>").format(
|
|
785
|
+
ComplianceEvent.Type[instance.type_event].label,
|
|
786
|
+
current_profile.first_name,
|
|
787
|
+
current_profile.last_name,
|
|
788
|
+
instance.last_modified.strftime("%d-%b-%y %H:%M:%S"),
|
|
789
|
+
)
|
|
790
|
+
if instance.exec_summary:
|
|
791
|
+
msg += _("<p> Summary : {}</p>").format(instance.exec_summary)
|
|
792
|
+
for recipient in recipients:
|
|
793
|
+
send_notification(
|
|
794
|
+
code="wbcompliance.complianceevent.notify",
|
|
795
|
+
title=title,
|
|
796
|
+
body=msg,
|
|
797
|
+
user=recipient,
|
|
798
|
+
reverse_name="wbcompliance:complianceevent-detail",
|
|
799
|
+
reverse_args=[instance.id],
|
|
800
|
+
)
|