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,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])
|