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,626 @@
1
+ from django.apps import apps
2
+ from django.conf import settings
3
+ from django.contrib.auth import get_user_model
4
+ from django.contrib.auth.models import Group
5
+ from django.db import models
6
+ from django.db.models import (
7
+ BooleanField,
8
+ Case,
9
+ Count,
10
+ F,
11
+ OuterRef,
12
+ QuerySet,
13
+ Subquery,
14
+ Value,
15
+ When,
16
+ )
17
+ from django.db.models.functions import Coalesce
18
+ from django.db.models.signals import post_save
19
+ from django.dispatch import receiver
20
+ from django.template.loader import get_template
21
+ from django.utils import timezone
22
+ from django.utils.translation import gettext_lazy as _
23
+ from django_fsm import FSMField, transition
24
+ from rest_framework.reverse import reverse
25
+ from wbcore.contrib.icons import WBIcon
26
+ from wbcore.contrib.notifications.dispatch import send_notification
27
+ from wbcore.contrib.notifications.utils import create_notification_type
28
+ from wbcore.enums import RequestType
29
+ from wbcore.markdown.utils import custom_url_fetcher
30
+ from wbcore.metadata.configs.buttons import ActionButton, ButtonDefaultColor
31
+ from wbcore.models import WBModel
32
+ from wbcore.permissions.shortcuts import get_internal_users
33
+ from weasyprint import HTML
34
+
35
+ from .compliance_type import ComplianceDocumentMixin, ComplianceType, can_active_request
36
+
37
+ User = get_user_model()
38
+
39
+
40
+ class ComplianceFormType(WBModel):
41
+ class Type(models.TextChoices):
42
+ TEXT = "TEXT", "Text"
43
+ FORM = "FORM", "Form"
44
+
45
+ class Meta:
46
+ verbose_name = "Compliance Form Type"
47
+ verbose_name_plural = "Compliance Form Types"
48
+
49
+ name = models.CharField(max_length=255, verbose_name=_("Name"))
50
+ type = models.CharField(
51
+ max_length=32,
52
+ default=Type.TEXT,
53
+ choices=Type.choices,
54
+ verbose_name=_("Type"),
55
+ )
56
+
57
+ def __str__(self) -> str:
58
+ return "{}".format(self.name)
59
+
60
+ @classmethod
61
+ def get_endpoint_basename(cls) -> str:
62
+ return "wbcompliance:complianceformtype"
63
+
64
+ @classmethod
65
+ def get_representation_endpoint(cls) -> str:
66
+ return "wbcompliance:complianceformtyperepresentation-list"
67
+
68
+ @classmethod
69
+ def get_representation_value_key(cls) -> str:
70
+ return "id"
71
+
72
+ @classmethod
73
+ def get_representation_label_key(cls) -> str:
74
+ return "{{name}}"
75
+
76
+
77
+ class ComplianceForm(ComplianceDocumentMixin, WBModel):
78
+ class Status(models.TextChoices):
79
+ DRAFT = "DRAFT", "Draft"
80
+ ACTIVATION_REQUESTED = "ACTIVATION_REQUESTED", "Activation Requested"
81
+ ACTIVE = "ACTIVE", "Active"
82
+
83
+ class Meta:
84
+ verbose_name = "Compliance Form"
85
+ verbose_name_plural = "Compliance Forms"
86
+
87
+ notification_types = [
88
+ create_notification_type(
89
+ code="wbcompliance.complianceform.notify",
90
+ title="Compliance Form Notification",
91
+ help_text="Sends out a notification when something happens with a compliance form",
92
+ )
93
+ ]
94
+
95
+ creator = models.ForeignKey(
96
+ to="directory.Person",
97
+ null=True,
98
+ blank=True,
99
+ verbose_name=_("Creator"),
100
+ related_name="compliance_forms",
101
+ on_delete=models.deletion.SET_NULL,
102
+ )
103
+ created = models.DateTimeField(auto_now_add=True, verbose_name=_("Created"))
104
+ changer = models.ForeignKey(
105
+ "directory.Person", null=True, blank=True, verbose_name=_("Changer"), on_delete=models.deletion.SET_NULL
106
+ )
107
+ changed = models.DateTimeField(auto_now=True, verbose_name=_("Changed"))
108
+ title = models.CharField(max_length=255, verbose_name=_("Title"))
109
+ policy = models.TextField(default="", null=True, blank=True, verbose_name=_("Policy"))
110
+ start = models.DateField(verbose_name=_("Start"))
111
+ end = models.DateField(verbose_name=_("End"), null=True, blank=True)
112
+ assigned_to = models.ManyToManyField(
113
+ Group,
114
+ related_name="forms_of_group",
115
+ blank=True,
116
+ verbose_name=_("Group to which the Form applies"),
117
+ )
118
+ only_internal = models.BooleanField(
119
+ default=True, verbose_name=_("Only internal"), help_text=_("Send the Form only to internal users")
120
+ )
121
+ form_type = models.ForeignKey(to=ComplianceFormType, on_delete=models.PROTECT, verbose_name=_("Form Type"))
122
+
123
+ compliance_type = models.ForeignKey(to=ComplianceType, on_delete=models.PROTECT, verbose_name=_("Type"))
124
+
125
+ status = FSMField(
126
+ default=Status.DRAFT,
127
+ choices=Status.choices,
128
+ verbose_name=_("Status"),
129
+ help_text=_("The Compliance Form status (default to Draft)"),
130
+ )
131
+ version = models.IntegerField(default=0)
132
+
133
+ def generate_pdf(self) -> bytes:
134
+ html_content = ""
135
+ if self.form_type.type == ComplianceFormType.Type.TEXT:
136
+ html_content = self.policy
137
+ elif self.form_type.type == ComplianceFormType.Type.FORM:
138
+ html = get_template("compliance/compliance_form.html")
139
+ html_content = html.render(
140
+ {"form_type": self.form_type.type, "today": timezone.now(), "form": self, "is_signature": False}
141
+ )
142
+ return HTML(
143
+ string=html_content, base_url=settings.BASE_ENDPOINT_URL, url_fetcher=custom_url_fetcher
144
+ ).write_pdf()
145
+
146
+ @transition(
147
+ field=status,
148
+ source=Status.DRAFT,
149
+ target=Status.ACTIVATION_REQUESTED,
150
+ permission=lambda instance, user: user.has_perm("wbcompliance.administrate_compliance"),
151
+ custom={
152
+ "_transition_button": ActionButton(
153
+ method=RequestType.PATCH,
154
+ color=ButtonDefaultColor.WARNING,
155
+ identifiers=("wbcompliance:complianceform",),
156
+ icon=WBIcon.EDIT.icon,
157
+ key="activationrequested",
158
+ label=_("Request Activation"),
159
+ action_label=_("Request Activation"),
160
+ description_fields=_(
161
+ "{{_form_type.name}} <p>Title: <b>{{title}}</b></p>\
162
+ <p>Version: <b>{{version}}</b></p> <p>Status: <b>{{status}}</b></p> <p>Start: <b>{{start}}</b></p>\
163
+ <p>End: <b>{{end}}</b></p> <p>Do you want to send this request for validation ?</p>"
164
+ ),
165
+ )
166
+ },
167
+ )
168
+ def activationrequested(self, by=None, description=None, **kwargs):
169
+ if self.compliance_type:
170
+ # notify the compliance team without the current user
171
+ if by:
172
+ self.changer = by.profile
173
+ current_profile = self.changer if self.changer else self.creator
174
+ recipients = ComplianceType.get_administrators(self.compliance_type).exclude(profile=current_profile)
175
+ msg = _("Validation Request from {} to activate a {} version <b>{}</b> : <b>{}</b>").format(
176
+ str(current_profile), self.form_type, self.version, self.title
177
+ )
178
+ title = _("Validation Request {} : {}").format(self.form_type, self.title)
179
+ self.notify(title, msg, recipients, admin_compliance=True)
180
+
181
+ @transition(
182
+ field=status,
183
+ source=Status.ACTIVATION_REQUESTED,
184
+ target=Status.ACTIVE,
185
+ permission=can_active_request,
186
+ custom={
187
+ "_transition_button": ActionButton(
188
+ method=RequestType.PATCH,
189
+ color=ButtonDefaultColor.WARNING,
190
+ identifiers=("wbcompliance:complianceform",),
191
+ icon=WBIcon.SEND.icon,
192
+ key="active",
193
+ label=_("Activate"),
194
+ action_label=_("Activate"),
195
+ description_fields=_(
196
+ """
197
+ <p> Title: <b> {{title}} </b></p> <p>Version: <b>{{version}}</b></p>
198
+ <p>Start: <b>{{start}}</b></p><p>End: <b>{{end}}</b></p>
199
+ <p>Do you want to activate this {{_form_type.name}} ?</p>
200
+ """
201
+ ),
202
+ )
203
+ },
204
+ )
205
+ def active(self, by=None, description=None, **kwargs):
206
+ self.version += 1
207
+ if by:
208
+ self.changer = by.profile
209
+ current_profile = self.changer if self.changer else self.creator
210
+ self.create_compliance_form_signature()
211
+
212
+ msg = _("<p>{} has activated a {} version <b>{}</b> : <b>{}</b></p>").format(
213
+ str(current_profile), self.form_type, self.version, self.title
214
+ )
215
+ if self.policy and self.policy != "<p></p>" and self.policy != "null":
216
+ msg += _("</br><p><b>Policy:</b></p><i>{}</i>").format(self.policy)
217
+ title = _("Applying a {} : {}").format(self.form_type, self.title)
218
+ self.notify(title, msg)
219
+ self.save()
220
+
221
+ @transition(
222
+ field=status,
223
+ source=[Status.ACTIVATION_REQUESTED, Status.ACTIVE],
224
+ target=Status.DRAFT,
225
+ permission=lambda instance, user: user.has_perm("wbcompliance.administrate_compliance"),
226
+ custom={
227
+ "_transition_button": ActionButton(
228
+ method=RequestType.PATCH,
229
+ color=ButtonDefaultColor.WARNING,
230
+ identifiers=("wbcompliance:complianceform",),
231
+ icon=WBIcon.EDIT.icon,
232
+ key="draft",
233
+ label=_("Return to Draft Mode"),
234
+ action_label=_("Return to Draft Mode"),
235
+ description_fields=_(
236
+ """
237
+ {{_form_type.name}}<p>Title: <b> {{title}} </b></p><p>Version: <b>{{version}}</b></p>
238
+ <p>Status: <b>{{status}}</b></p> <p>Start: <b>{{start}}</b></p> <p>End: <b>{{end}}</b></p>
239
+ <p>Do you want to return to draft mode this {{_form_type.name}}?</p>
240
+ """
241
+ ),
242
+ )
243
+ },
244
+ )
245
+ def draft(self, by=None, description=None, **kwargs):
246
+ if self.compliance_type:
247
+ if by:
248
+ self.changer = by.profile
249
+ current_profile = self.changer if self.changer else self.creator
250
+ if self.status == ComplianceForm.Status.ACTIVE:
251
+ msg = _("{} has disabled a {} version <b>{}</b> : <b>{}</b>").format(
252
+ str(current_profile), self.form_type, self.version, self.title
253
+ )
254
+ title = _("Disabled {} : {}").format(self.form_type, self.title)
255
+ else:
256
+ msg = _("{} has modified a {} version <b>{}</b> to Draft : <b>{}</b>").format(
257
+ str(current_profile), self.form_type, self.version, self.title
258
+ )
259
+ title = _("{} Drafted: {}").format(self.form_type, self.title)
260
+ recipients = ComplianceType.get_administrators(self.compliance_type).exclude(profile=current_profile)
261
+ self.notify(title, msg, recipients, admin_compliance=True)
262
+
263
+ def get_signing_users(self) -> QuerySet["User"]:
264
+ if self.only_internal:
265
+ users = get_internal_users()
266
+ else:
267
+ users = User.objects.filter(is_active=True)
268
+ if self.assigned_to.exists():
269
+ users = users.filter(groups__in=self.assigned_to.all())
270
+ return users.distinct()
271
+
272
+ def create_compliance_form_signature(self) -> None:
273
+ for user in self.get_signing_users():
274
+ compliance_form_signature = ComplianceFormSignature.objects.create(
275
+ compliance_form=self,
276
+ version=self.version,
277
+ policy=self.policy,
278
+ start=self.start,
279
+ end=self.end,
280
+ person=user.profile,
281
+ )
282
+ sections = ComplianceFormSection.objects.filter(compliance_form=compliance_form_signature.compliance_form)
283
+ for section in sections:
284
+ signature_section = ComplianceFormSignatureSection.objects.create(
285
+ compliance_form_signature=compliance_form_signature, name=section.name
286
+ )
287
+ rules = ComplianceFormRule.objects.filter(section=section)
288
+ for rule in rules:
289
+ ComplianceFormSignatureRule.objects.create(section=signature_section, text=rule.text)
290
+
291
+ def notify(
292
+ self, title: str, msg: str, recipients: QuerySet["User"] | None = None, admin_compliance: bool = False
293
+ ) -> None:
294
+ """
295
+ param:
296
+ recipients: list of users, if not set, we get all users assigned to the form
297
+ admin_compliance: by default is false, allow to send the signature form to recipient, if true send rather the form to admin.
298
+ """
299
+ if admin_compliance:
300
+ users = ComplianceType.get_administrators(self.compliance_type)
301
+ if recipients:
302
+ users = users.filter(id__in=[_user.id for _user in recipients])
303
+ else:
304
+ users = recipients if recipients else self.get_signing_users()
305
+ for user in users:
306
+ if admin_compliance:
307
+ endpoint = reverse("wbcompliance:complianceform-detail", args=[self.id])
308
+ else:
309
+ endpoint = None
310
+ if formsignature := (
311
+ ComplianceFormSignature.objects.filter(compliance_form=self, person=user.profile)
312
+ .order_by("version")
313
+ .last()
314
+ ):
315
+ endpoint = reverse("wbcompliance:complianceformsignature-detail", args=[formsignature.id])
316
+
317
+ if endpoint:
318
+ send_notification(
319
+ code="wbcompliance.complianceform.notify",
320
+ title=title,
321
+ body=msg,
322
+ user=user,
323
+ endpoint=endpoint,
324
+ )
325
+
326
+ @classmethod
327
+ def get_subquery_total_compliance_form_signature(cls, remaining_signed: bool = False) -> Subquery:
328
+ if remaining_signed:
329
+ compliance_form_signatures = ComplianceFormSignature.objects.filter(
330
+ compliance_form=OuterRef("pk"),
331
+ signed=None,
332
+ ).order_by("-version")
333
+ else:
334
+ compliance_form_signatures = ComplianceFormSignature.objects.filter(
335
+ compliance_form=OuterRef("pk"),
336
+ ).order_by("-version")
337
+
338
+ return Coalesce(
339
+ Subquery(
340
+ compliance_form_signatures.values("compliance_form__pk")
341
+ .annotate(total_signed=Count("compliance_form__pk"))
342
+ .values("total_signed")[:1]
343
+ ),
344
+ 0,
345
+ )
346
+
347
+ @classmethod
348
+ def get_subquery_compliance_form_signature(cls, person_signed) -> Subquery:
349
+ compliance_form_signatures = ComplianceFormSignature.objects.filter(
350
+ compliance_form=OuterRef("pk"), person=person_signed
351
+ ).order_by("-version")
352
+ return Coalesce(
353
+ Subquery(
354
+ compliance_form_signatures.values("compliance_form__pk")
355
+ .annotate(
356
+ is_signed=Case(
357
+ When(signed=None, then=Value(False)),
358
+ default=Value(True),
359
+ output_field=BooleanField(),
360
+ )
361
+ )
362
+ .values("is_signed")[:1]
363
+ ),
364
+ None,
365
+ )
366
+
367
+ def __str__(self) -> str:
368
+ return "{} {} ({} - {}) ".format(self.title, self.version, self.start, self.end)
369
+
370
+ @classmethod
371
+ def get_endpoint_basename(cls) -> str:
372
+ return "wbcompliance:complianceform"
373
+
374
+ @classmethod
375
+ def get_representation_endpoint(cls) -> str:
376
+ return "wbcompliance:complianceformrepresentation-list"
377
+
378
+ @classmethod
379
+ def get_representation_value_key(cls) -> str:
380
+ return "id"
381
+
382
+ @classmethod
383
+ def get_representation_label_key(cls) -> str:
384
+ return "{{title}}"
385
+
386
+
387
+ class ComplianceFormSignature(ComplianceDocumentMixin, models.Model):
388
+ class Meta:
389
+ verbose_name = "Compliance Form Signature"
390
+ verbose_name_plural = "Compliance Signatures"
391
+
392
+ notification_types = [
393
+ create_notification_type(
394
+ code="wbcompliance.complianceformsignature.signed",
395
+ title="Compliance Form Signature Confirmation",
396
+ help_text="Send a notification as a confirmation that a compliance form has been signed",
397
+ )
398
+ ]
399
+
400
+ compliance_form = models.ForeignKey(
401
+ to="wbcompliance.ComplianceForm",
402
+ verbose_name=_("Form"),
403
+ related_name="complianceforms",
404
+ on_delete=models.CASCADE,
405
+ )
406
+ version = models.IntegerField(default=0, verbose_name=_("Version"))
407
+ start = models.DateField(verbose_name=_("Start"))
408
+ end = models.DateField(verbose_name=_("End"), null=True, blank=True)
409
+ policy = models.TextField(default="", null=True, blank=True, verbose_name=_("Policy"))
410
+ signed = models.DateTimeField(null=True, blank=True, verbose_name=_("Signed"))
411
+ person = models.ForeignKey(
412
+ to="directory.Person",
413
+ verbose_name=_("Signer"),
414
+ related_name="signed_compliance_forms",
415
+ on_delete=models.CASCADE,
416
+ )
417
+ remark = models.TextField(null=True, blank=True, default="", verbose_name=_("Remark"))
418
+
419
+ def __str__(self) -> str:
420
+ return "{} {} ({} - {})".format(self.compliance_form.title, self.version, self.start, self.end)
421
+
422
+ def generate_pdf(self) -> bytes:
423
+ html_content = ""
424
+ html = get_template("compliance/compliance_form.html")
425
+ html_content = html.render(
426
+ {
427
+ "form_type": self.compliance_form.form_type.type,
428
+ "today": timezone.now(),
429
+ "form": self,
430
+ "is_signature": True,
431
+ }
432
+ )
433
+ return HTML(
434
+ string=html_content, base_url=settings.BASE_ENDPOINT_URL, url_fetcher=custom_url_fetcher
435
+ ).write_pdf()
436
+
437
+ @classmethod
438
+ def get_endpoint_basename(cls) -> str:
439
+ return "wbcompliance:complianceformsignature"
440
+
441
+ @classmethod
442
+ def get_representation_value_key(cls) -> str:
443
+ return "id"
444
+
445
+ @classmethod
446
+ def get_representation_label_key(cls) -> str:
447
+ return "{{compliance_form.title}} {{self.version}} ({{start}} - {{end}})"
448
+
449
+
450
+ class ComplianceFormSection(WBModel):
451
+ """Model that represents a section of the Compliance Form"""
452
+
453
+ compliance_form = models.ForeignKey(
454
+ ComplianceForm, related_name="compliance_forms", verbose_name=_("Compliance Form"), on_delete=models.CASCADE
455
+ )
456
+ name = models.CharField(max_length=255, verbose_name=_("Name section"))
457
+
458
+ class Meta:
459
+ verbose_name = "Section of the Compliance Form"
460
+ verbose_name_plural = "Sections of the Compliance Form"
461
+
462
+ def __str__(self) -> str:
463
+ return self.name
464
+
465
+ @classmethod
466
+ def get_endpoint_basename(cls) -> str:
467
+ return "wbcompliance:complianceformsection"
468
+
469
+ @classmethod
470
+ def get_representation_endpoint(cls) -> str:
471
+ return "wbcompliance:complianceformsectionrepresentation-list"
472
+
473
+ @classmethod
474
+ def get_representation_value_key(cls) -> str:
475
+ return "id"
476
+
477
+ @classmethod
478
+ def get_representation_label_key(cls) -> str:
479
+ return "{{name}}"
480
+
481
+
482
+ class ComplianceFormRule(models.Model):
483
+ """Model that represents a rule in a Section of the Compliance Form"""
484
+
485
+ section = models.ForeignKey(
486
+ ComplianceFormSection, related_name="rules", verbose_name=_("Section"), on_delete=models.CASCADE
487
+ )
488
+ text = models.TextField(default="")
489
+ ticked = models.BooleanField(
490
+ default=False,
491
+ verbose_name=_("Expected Answer"),
492
+ )
493
+
494
+ class Meta:
495
+ verbose_name = "Rule of the section of the Compliance Form"
496
+ verbose_name_plural = "Rules of the section of the Compliance Form"
497
+
498
+ def __str__(self) -> str:
499
+ return "{} {}".format(self.section, self.id)
500
+
501
+ @classmethod
502
+ def get_endpoint_basename(cls) -> str:
503
+ return "wbcompliance:complianceformrule"
504
+
505
+ @classmethod
506
+ def get_representation_value_key(cls) -> str:
507
+ return "id"
508
+
509
+ @classmethod
510
+ def get_representation_label_key(cls) -> str:
511
+ return "{{section}} {{id}}"
512
+
513
+
514
+ class ComplianceFormSignatureSection(models.Model):
515
+ """Model that represents a section of the Compliance Form Signature"""
516
+
517
+ compliance_form_signature = models.ForeignKey(
518
+ ComplianceFormSignature,
519
+ related_name="compliance_form_signatures",
520
+ verbose_name=_("Compliance Form Signature"),
521
+ on_delete=models.CASCADE,
522
+ )
523
+ name = models.CharField(max_length=255, verbose_name=_("Name section"))
524
+
525
+ class Meta:
526
+ verbose_name = "Section of the Compliance Form Signature"
527
+ verbose_name_plural = "Sections of the Compliance Form Signature"
528
+
529
+ def __str__(self) -> str:
530
+ return self.name
531
+
532
+ @classmethod
533
+ def get_representation_endpoint(cls) -> str:
534
+ return "wbcompliance:complianceformsignaturesectionrepresentation-list"
535
+
536
+ @classmethod
537
+ def get_representation_value_key(cls) -> str:
538
+ return "id"
539
+
540
+ @classmethod
541
+ def get_representation_label_key(cls) -> str:
542
+ return "{{name}}"
543
+
544
+
545
+ class ComplianceFormSignatureRule(models.Model):
546
+ """Model that represents a rule in a Section of the Compliance Form Signature"""
547
+
548
+ section = models.ForeignKey(
549
+ ComplianceFormSignatureSection, related_name="rules", verbose_name=_("Section"), on_delete=models.CASCADE
550
+ )
551
+ text = models.TextField(default="")
552
+ ticked = models.BooleanField(
553
+ default=False,
554
+ verbose_name=_("Answer"),
555
+ )
556
+ comments = models.TextField(default="")
557
+
558
+ class Meta:
559
+ verbose_name = "Rule of the section of the Compliance Form Signature"
560
+ verbose_name_plural = "Rules of the section of the Compliance Form Signature"
561
+
562
+ def __str__(self) -> str:
563
+ return "{} {}".format(self.section, self.id)
564
+
565
+ @classmethod
566
+ def get_subquery_expected_ticked(cls) -> Subquery:
567
+ return Coalesce(
568
+ Subquery(
569
+ ComplianceFormRule.objects.filter(
570
+ section__compliance_form=OuterRef("section__compliance_form_signature__compliance_form"),
571
+ section__name=OuterRef("section__name"),
572
+ text=OuterRef("text"),
573
+ )
574
+ .values("ticked")
575
+ .annotate(exp=F("ticked"))
576
+ .values("exp")[:1]
577
+ ),
578
+ None,
579
+ )
580
+
581
+ @classmethod
582
+ def get_representation_value_key(cls) -> str:
583
+ return "id"
584
+
585
+ @classmethod
586
+ def get_representation_label_key(cls) -> str:
587
+ return "{{section}} {{id}}"
588
+
589
+
590
+ if apps.is_installed("wbhuman_resources"):
591
+
592
+ @receiver(post_save, sender="wbhuman_resources.EmployeeHumanResource")
593
+ def post_save_compliance_form_employee(sender, instance, created, **kwargs):
594
+ if created:
595
+ for compliance_form in ComplianceForm.objects.filter(status=ComplianceForm.Status.ACTIVE):
596
+ if compliance_form.get_signing_users().filter(profile_id=instance.profile.id).exists():
597
+ compliance_form_signature, created = ComplianceFormSignature.objects.get_or_create(
598
+ compliance_form=compliance_form,
599
+ version=compliance_form.version,
600
+ policy=compliance_form.policy,
601
+ start=compliance_form.start,
602
+ end=compliance_form.end,
603
+ person=instance.profile,
604
+ )
605
+ sections = ComplianceFormSection.objects.filter(
606
+ compliance_form=compliance_form_signature.compliance_form
607
+ )
608
+ for section in sections:
609
+ signature_section = ComplianceFormSignatureSection.objects.create(
610
+ compliance_form_signature=compliance_form_signature, name=section.name
611
+ )
612
+ rules = ComplianceFormRule.objects.filter(section=section)
613
+ for rule in rules:
614
+ ComplianceFormSignatureRule.objects.create(section=signature_section, text=rule.text)
615
+ if _user := getattr(instance.profile, "user_account", None):
616
+ msg = _("<p>{} has activated a <b>{}</b> policy version <b>{} </b></p>").format(
617
+ str(compliance_form.creator), compliance_form.title, compliance_form.version
618
+ )
619
+ if (
620
+ compliance_form.policy
621
+ and compliance_form.policy != "<p></p>"
622
+ and compliance_form.policy != "null"
623
+ ):
624
+ msg += _("</br><p><b>Policy:</b></p><i>{}</i>").format(compliance_form.policy)
625
+ title = _("Applying a {} : {}").format(compliance_form.form_type, compliance_form.title)
626
+ compliance_form.notify(title, msg, recipients=[_user])