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.
Files changed (129) hide show
  1. wbcompliance/__init__.py +1 -0
  2. wbcompliance/admin/__init__.py +16 -0
  3. wbcompliance/admin/compliance_form.py +56 -0
  4. wbcompliance/admin/compliance_task.py +135 -0
  5. wbcompliance/admin/compliance_type.py +8 -0
  6. wbcompliance/admin/risk_management/__init__.py +3 -0
  7. wbcompliance/admin/risk_management/checks.py +7 -0
  8. wbcompliance/admin/risk_management/incidents.py +50 -0
  9. wbcompliance/admin/risk_management/rules.py +63 -0
  10. wbcompliance/admin/utils.py +46 -0
  11. wbcompliance/apps.py +14 -0
  12. wbcompliance/factories/__init__.py +21 -0
  13. wbcompliance/factories/compliance.py +246 -0
  14. wbcompliance/factories/risk_management/__init__.py +12 -0
  15. wbcompliance/factories/risk_management/backends.py +42 -0
  16. wbcompliance/factories/risk_management/checks.py +12 -0
  17. wbcompliance/factories/risk_management/incidents.py +84 -0
  18. wbcompliance/factories/risk_management/rules.py +100 -0
  19. wbcompliance/filters/__init__.py +2 -0
  20. wbcompliance/filters/compliances.py +189 -0
  21. wbcompliance/filters/risk_management/__init__.py +3 -0
  22. wbcompliance/filters/risk_management/checks.py +22 -0
  23. wbcompliance/filters/risk_management/incidents.py +113 -0
  24. wbcompliance/filters/risk_management/rules.py +110 -0
  25. wbcompliance/filters/risk_management/tables.py +112 -0
  26. wbcompliance/filters/risk_management/utils.py +3 -0
  27. wbcompliance/management/__init__.py +10 -0
  28. wbcompliance/migrations/0001_initial_squashed_squashed_0010_alter_checkedobjectincidentrelationship_resolved_by_and_more.py +1744 -0
  29. wbcompliance/migrations/0011_alter_riskrule_parameters.py +21 -0
  30. wbcompliance/migrations/0012_alter_compliancetype_options.py +20 -0
  31. wbcompliance/migrations/0013_alter_riskrule_unique_together.py +16 -0
  32. wbcompliance/migrations/0014_alter_reviewcompliancetask_year.py +27 -0
  33. wbcompliance/migrations/0015_auto_20240103_0957.py +43 -0
  34. wbcompliance/migrations/0016_checkedobjectincidentrelationship_report_details_and_more.py +37 -0
  35. wbcompliance/migrations/0017_alter_rulebackend_incident_report_template.py +20 -0
  36. wbcompliance/migrations/0018_alter_rulecheckedobjectrelationship_unique_together.py +39 -0
  37. wbcompliance/migrations/0019_rulegroup_riskrule_activation_date_and_more.py +60 -0
  38. wbcompliance/migrations/__init__.py +0 -0
  39. wbcompliance/models/__init__.py +20 -0
  40. wbcompliance/models/compliance_form.py +626 -0
  41. wbcompliance/models/compliance_task.py +800 -0
  42. wbcompliance/models/compliance_type.py +133 -0
  43. wbcompliance/models/enums.py +13 -0
  44. wbcompliance/models/risk_management/__init__.py +4 -0
  45. wbcompliance/models/risk_management/backend.py +139 -0
  46. wbcompliance/models/risk_management/checks.py +194 -0
  47. wbcompliance/models/risk_management/dispatch.py +41 -0
  48. wbcompliance/models/risk_management/incidents.py +619 -0
  49. wbcompliance/models/risk_management/mixins.py +115 -0
  50. wbcompliance/models/risk_management/rules.py +654 -0
  51. wbcompliance/permissions.py +32 -0
  52. wbcompliance/serializers/__init__.py +30 -0
  53. wbcompliance/serializers/compliance_form.py +320 -0
  54. wbcompliance/serializers/compliance_task.py +463 -0
  55. wbcompliance/serializers/compliance_type.py +26 -0
  56. wbcompliance/serializers/risk_management/__init__.py +19 -0
  57. wbcompliance/serializers/risk_management/checks.py +53 -0
  58. wbcompliance/serializers/risk_management/incidents.py +227 -0
  59. wbcompliance/serializers/risk_management/rules.py +158 -0
  60. wbcompliance/tasks.py +112 -0
  61. wbcompliance/tests/__init__.py +0 -0
  62. wbcompliance/tests/conftest.py +63 -0
  63. wbcompliance/tests/disable_signals.py +82 -0
  64. wbcompliance/tests/mixins.py +17 -0
  65. wbcompliance/tests/risk_management/__init__.py +0 -0
  66. wbcompliance/tests/risk_management/models/__init__.py +0 -0
  67. wbcompliance/tests/risk_management/models/test_backends.py +0 -0
  68. wbcompliance/tests/risk_management/models/test_checks.py +55 -0
  69. wbcompliance/tests/risk_management/models/test_incidents.py +327 -0
  70. wbcompliance/tests/risk_management/models/test_rules.py +255 -0
  71. wbcompliance/tests/signals.py +89 -0
  72. wbcompliance/tests/test_filters.py +23 -0
  73. wbcompliance/tests/test_models.py +57 -0
  74. wbcompliance/tests/test_serializers.py +48 -0
  75. wbcompliance/tests/test_views.py +377 -0
  76. wbcompliance/tests/tests.py +21 -0
  77. wbcompliance/urls.py +238 -0
  78. wbcompliance/viewsets/__init__.py +40 -0
  79. wbcompliance/viewsets/buttons/__init__.py +9 -0
  80. wbcompliance/viewsets/buttons/compliance_form.py +78 -0
  81. wbcompliance/viewsets/buttons/compliance_task.py +149 -0
  82. wbcompliance/viewsets/buttons/risk_managment/__init__.py +3 -0
  83. wbcompliance/viewsets/buttons/risk_managment/checks.py +11 -0
  84. wbcompliance/viewsets/buttons/risk_managment/incidents.py +51 -0
  85. wbcompliance/viewsets/buttons/risk_managment/rules.py +35 -0
  86. wbcompliance/viewsets/compliance_form.py +425 -0
  87. wbcompliance/viewsets/compliance_task.py +513 -0
  88. wbcompliance/viewsets/compliance_type.py +38 -0
  89. wbcompliance/viewsets/display/__init__.py +22 -0
  90. wbcompliance/viewsets/display/compliance_form.py +317 -0
  91. wbcompliance/viewsets/display/compliance_task.py +453 -0
  92. wbcompliance/viewsets/display/compliance_type.py +22 -0
  93. wbcompliance/viewsets/display/risk_managment/__init__.py +11 -0
  94. wbcompliance/viewsets/display/risk_managment/checks.py +46 -0
  95. wbcompliance/viewsets/display/risk_managment/incidents.py +155 -0
  96. wbcompliance/viewsets/display/risk_managment/rules.py +146 -0
  97. wbcompliance/viewsets/display/risk_managment/tables.py +51 -0
  98. wbcompliance/viewsets/endpoints/__init__.py +27 -0
  99. wbcompliance/viewsets/endpoints/compliance_form.py +207 -0
  100. wbcompliance/viewsets/endpoints/compliance_task.py +193 -0
  101. wbcompliance/viewsets/endpoints/compliance_type.py +9 -0
  102. wbcompliance/viewsets/endpoints/risk_managment/__init__.py +12 -0
  103. wbcompliance/viewsets/endpoints/risk_managment/checks.py +16 -0
  104. wbcompliance/viewsets/endpoints/risk_managment/incidents.py +36 -0
  105. wbcompliance/viewsets/endpoints/risk_managment/rules.py +32 -0
  106. wbcompliance/viewsets/endpoints/risk_managment/tables.py +14 -0
  107. wbcompliance/viewsets/menu/__init__.py +17 -0
  108. wbcompliance/viewsets/menu/compliance_form.py +49 -0
  109. wbcompliance/viewsets/menu/compliance_task.py +130 -0
  110. wbcompliance/viewsets/menu/compliance_type.py +17 -0
  111. wbcompliance/viewsets/menu/risk_management.py +56 -0
  112. wbcompliance/viewsets/risk_management/__init__.py +21 -0
  113. wbcompliance/viewsets/risk_management/checks.py +49 -0
  114. wbcompliance/viewsets/risk_management/incidents.py +204 -0
  115. wbcompliance/viewsets/risk_management/mixins.py +52 -0
  116. wbcompliance/viewsets/risk_management/rules.py +179 -0
  117. wbcompliance/viewsets/risk_management/tables.py +96 -0
  118. wbcompliance/viewsets/titles/__init__.py +17 -0
  119. wbcompliance/viewsets/titles/compliance_form.py +101 -0
  120. wbcompliance/viewsets/titles/compliance_task.py +60 -0
  121. wbcompliance/viewsets/titles/compliance_type.py +13 -0
  122. wbcompliance/viewsets/titles/risk_managment/__init__.py +1 -0
  123. wbcompliance/viewsets/titles/risk_managment/checks.py +0 -0
  124. wbcompliance/viewsets/titles/risk_managment/incidents.py +0 -0
  125. wbcompliance/viewsets/titles/risk_managment/rules.py +0 -0
  126. wbcompliance/viewsets/titles/risk_managment/tables.py +7 -0
  127. wbcompliance-2.2.1.dist-info/METADATA +7 -0
  128. wbcompliance-2.2.1.dist-info/RECORD +129 -0
  129. 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>&nbsp;</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
+ )