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,227 @@
1
+ from datetime import timedelta
2
+
3
+ from django.utils.translation import gettext as _
4
+ from psycopg.types.range import NumericRange
5
+ from rest_framework import serializers
6
+ from rest_framework.reverse import reverse
7
+ from wbcompliance.models.risk_management import (
8
+ CheckedObjectIncidentRelationship,
9
+ RiskIncident,
10
+ RiskIncidentType,
11
+ RuleThreshold,
12
+ )
13
+ from wbcore import serializers as wb_serializers
14
+ from wbcore.content_type.serializers import ContentTypeRepresentationSerializer
15
+ from wbcore.contrib.authentication.serializers import GroupRepresentationSerializer
16
+ from wbcore.contrib.directory.serializers import PersonRepresentationSerializer
17
+
18
+ from .checks import RiskCheckRepresentationSerializer
19
+ from .rules import RiskRuleRepresentationSerializer
20
+
21
+
22
+ class RiskIncidentTypeRepresentationSerializer(wb_serializers.RepresentationSerializer):
23
+ class Meta:
24
+ model = RiskIncidentType
25
+ fields = ("id", "name")
26
+
27
+
28
+ class RiskIncidentRepresentationSerializer(wb_serializers.RepresentationSerializer):
29
+ class Meta:
30
+ model = RiskIncident
31
+ fields = ("id", "computed_str")
32
+
33
+
34
+ class CheckedObjectIncidentRelationshipRepresentationSerializer(wb_serializers.RepresentationSerializer):
35
+ class Meta:
36
+ model = CheckedObjectIncidentRelationship
37
+ fields = ("id", "computed_str")
38
+
39
+
40
+ class RuleThresholdModelSerializer(wb_serializers.ModelSerializer):
41
+ _rule = RiskRuleRepresentationSerializer(source="rule")
42
+ _notifiable_users = PersonRepresentationSerializer(source="notifiable_users", many=True)
43
+ _notifiable_groups = GroupRepresentationSerializer(source="notifiable_groups", many=True)
44
+ _severity = RiskIncidentTypeRepresentationSerializer(source="severity")
45
+ range_lower = wb_serializers.FloatField(source="range.lower", allow_null=True, required=False, precision=4)
46
+ range_upper = wb_serializers.FloatField(source="range.upper", allow_null=True, required=False, precision=4)
47
+ range = wb_serializers.DecimalRangeField(required=False)
48
+
49
+ def validate(self, data):
50
+ range_upper, range_lower = None, None
51
+ if (range_dict := data.get("range", None)) and isinstance(range_dict, dict):
52
+ range_upper = range_dict.pop("upper", self.instance.range.upper if self.instance else None)
53
+ range_lower = range_dict.pop("lower", self.instance.range.lower if self.instance else None)
54
+ if range_upper and range_lower and range_upper < range_lower:
55
+ raise serializers.ValidationError({"range": "Lower needs to be strictly lower than upper bound"})
56
+ data["range"] = NumericRange(lower=range_lower, upper=range_upper)
57
+ return data
58
+
59
+ class Meta:
60
+ model = RuleThreshold
61
+ read_only_fields = ("computed_str",)
62
+ fields = (
63
+ "id",
64
+ "rule",
65
+ "_rule",
66
+ "range_lower",
67
+ "range_upper",
68
+ "range",
69
+ "severity",
70
+ "notifiable_users",
71
+ "_notifiable_users",
72
+ "notifiable_groups",
73
+ "_notifiable_groups",
74
+ "computed_str",
75
+ "upgradable_after_days",
76
+ "_severity",
77
+ "severity",
78
+ )
79
+
80
+
81
+ class RiskIncidentModelSerializer(wb_serializers.ModelSerializer):
82
+ # Extra fields to accommodate the multi level tree view with the CheckedIncidentRelationship class
83
+ _group_key = wb_serializers.CharField(read_only=True)
84
+ checked_date = wb_serializers.DateField(read_only=True)
85
+ object_repr = wb_serializers.CharField(read_only=True)
86
+ threshold_repr = wb_serializers.CharField(read_only=True, required=False)
87
+ breached_value = wb_serializers.TextField(read_only=True, default="Open to see details")
88
+ report = wb_serializers.TextField(read_only=True, default="Open to see details")
89
+
90
+ _resolved_by = PersonRepresentationSerializer(source="resolved_by")
91
+ _breached_content_type = ContentTypeRepresentationSerializer(source="breached_content_type")
92
+ _rule = RiskRuleRepresentationSerializer(source="rule")
93
+ _severity = RiskIncidentTypeRepresentationSerializer(source="severity")
94
+ date_range = wb_serializers.DateRangeField(outward_bounds_transform="[]")
95
+ ignore_until = wb_serializers.DateField(
96
+ read_only=True, label="Ignore Until (Included)", help_text=_("Ignore until this date (included)")
97
+ )
98
+ ignore_duration_in_days = wb_serializers.IntegerField(
99
+ required=False,
100
+ label=_("Ignore for X days"),
101
+ help_text=_(
102
+ "If set to a value different than 0, will ignore the forthcoming incidents for the specified number of days"
103
+ ),
104
+ )
105
+
106
+ @wb_serializers.register_resource()
107
+ def additional_resources(self, instance, request, user):
108
+ res = {}
109
+ if instance.checked_object_relationships.exists():
110
+ res["relationships"] = reverse(
111
+ "wbcompliance:riskincident-relationship-list",
112
+ args=[instance.id],
113
+ request=request,
114
+ )
115
+ return res
116
+
117
+ def validate(self, data):
118
+ if (ignore_duration_in_days := data.get("ignore_duration_in_days", None)) is not None:
119
+ data["ignore_duration"] = timedelta(days=ignore_duration_in_days)
120
+ return data
121
+
122
+ class Meta:
123
+ model = RiskIncident
124
+ only_fsm_transition_on_instance = True
125
+
126
+ fields = (
127
+ "id",
128
+ "date_range",
129
+ "last_ignored_date",
130
+ "ignore_until",
131
+ "rule",
132
+ "_rule",
133
+ "breached_content_type",
134
+ "_breached_content_type",
135
+ "breached_object_id",
136
+ "breached_object_repr",
137
+ "ignore_duration",
138
+ "ignore_duration_in_days",
139
+ "status",
140
+ "severity",
141
+ "comment",
142
+ "resolved_by",
143
+ "_resolved_by",
144
+ "_severity",
145
+ "severity",
146
+ "is_notified",
147
+ "_additional_resources",
148
+ "_group_key",
149
+ "checked_date",
150
+ "object_repr",
151
+ "threshold_repr",
152
+ "breached_value",
153
+ "report",
154
+ )
155
+ read_only_fields = (
156
+ "id",
157
+ "date_range",
158
+ "last_ignored_date",
159
+ "ignore_until",
160
+ "rule",
161
+ "breached_content_type",
162
+ "breached_object_id",
163
+ "breached_object_repr",
164
+ "status",
165
+ "severity",
166
+ "resolved_by",
167
+ "_severity",
168
+ "severity",
169
+ "is_notified",
170
+ )
171
+
172
+
173
+ class CheckedObjectIncidentRelationshipModelSerializer(wb_serializers.ModelSerializer):
174
+ _resolved_by = PersonRepresentationSerializer(source="resolved_by")
175
+ _incident = RiskIncidentRepresentationSerializer(source="incident")
176
+ _rule_check = RiskCheckRepresentationSerializer(source="rule_check")
177
+ _severity = RiskIncidentTypeRepresentationSerializer(source="severity")
178
+ breached_value = wb_serializers.TextField()
179
+ # extra annotation to play properly with the tree table
180
+ checked_date = wb_serializers.DateField(read_only=True, required=False)
181
+ # rule = wb_serializers.PrimaryKeyRelatedField()
182
+ # _rule = RiskRuleRepresentationSerializer(source="rule")
183
+ object_repr = wb_serializers.CharField(read_only=True, required=False)
184
+ date_range = wb_serializers.DateRangeField(read_only=True, outward_bounds_transform="[]", required=False)
185
+ threshold_repr = wb_serializers.CharField(read_only=True, required=False)
186
+
187
+ class Meta:
188
+ model = CheckedObjectIncidentRelationship
189
+ read_only_fields = (
190
+ "computed_str",
191
+ "incident",
192
+ "status",
193
+ "severity",
194
+ "rule_check",
195
+ "report",
196
+ "_severity",
197
+ "resolved_by",
198
+ "checked_date",
199
+ "rule",
200
+ "_rule",
201
+ "object_repr",
202
+ "date_range",
203
+ "breached_value",
204
+ )
205
+ fields = (
206
+ "id",
207
+ "computed_str",
208
+ "rule_check",
209
+ "_rule_check",
210
+ "incident",
211
+ "_incident",
212
+ "report",
213
+ "status",
214
+ "comment",
215
+ "resolved_by",
216
+ "_resolved_by",
217
+ "_severity",
218
+ "severity",
219
+ "_additional_resources",
220
+ "checked_date",
221
+ # "rule",
222
+ # "_rule",
223
+ "object_repr",
224
+ "date_range",
225
+ "threshold_repr",
226
+ "breached_value",
227
+ )
@@ -0,0 +1,158 @@
1
+ from django.contrib.contenttypes.models import ContentType
2
+ from rest_framework.reverse import reverse
3
+ from wbcompliance.models.risk_management import (
4
+ RiskRule,
5
+ RuleBackend,
6
+ RuleCheckedObjectRelationship,
7
+ RuleThreshold,
8
+ )
9
+ from wbcompliance.models.risk_management.rules import RuleGroup
10
+ from wbcore import serializers as wb_serializers
11
+ from wbcore.content_type.serializers import (
12
+ ContentTypeRepresentationSerializer,
13
+ DynamicObjectIDRepresentationSerializer,
14
+ )
15
+ from wbcore.contrib.authentication.serializers import UserRepresentationSerializer
16
+
17
+
18
+ class RuleGroupRepresentationSerializer(wb_serializers.RepresentationSerializer):
19
+ class Meta:
20
+ model = RuleGroup
21
+ fields = ("id", "name")
22
+
23
+
24
+ class RuleCheckedObjectRelationshipRepresentationSerializer(wb_serializers.RepresentationSerializer):
25
+ class Meta:
26
+ model = RuleCheckedObjectRelationship
27
+ fields = ("id", "computed_str", "checked_object_repr")
28
+
29
+
30
+ class RuleBackendRepresentationSerializer(wb_serializers.RepresentationSerializer):
31
+ class Meta:
32
+ model = RuleBackend
33
+ fields = ("id", "name")
34
+
35
+
36
+ class RuleThresholdRepresentationSerializer(wb_serializers.RepresentationSerializer):
37
+ class Meta:
38
+ model = RuleThreshold
39
+ fields = ("id", "computed_str")
40
+
41
+
42
+ class RiskRuleRepresentationSerializer(wb_serializers.RepresentationSerializer):
43
+ class Meta:
44
+ model = RiskRule
45
+ fields = ("id", "name")
46
+
47
+
48
+ class GetContentTypeFromKwargs:
49
+ requires_context = True
50
+
51
+ def __call__(self, serializer_instance):
52
+ if (view := serializer_instance.view) and (rule_id := view.kwargs.get("rule_id", None)):
53
+ rule = RiskRule.objects.get(id=rule_id)
54
+ if content_type := rule.rule_backend.allowed_checked_object_content_type:
55
+ return content_type.id
56
+ return None
57
+
58
+
59
+ class RuleCheckedObjectRelationshipModelSerializer(wb_serializers.ModelSerializer):
60
+ checked_object_content_type = wb_serializers.PrimaryKeyRelatedField(
61
+ queryset=ContentType.objects.all(), default=GetContentTypeFromKwargs()
62
+ )
63
+ _checked_object_content_type = ContentTypeRepresentationSerializer(source="checked_object_content_type")
64
+ _checked_object_id = DynamicObjectIDRepresentationSerializer(
65
+ content_type_field_name="checked_object_content_type",
66
+ source="checked_object_id",
67
+ optional_get_parameters={"checked_object_content_type": "content_type"},
68
+ depends_on=[{"field": "checked_object_content_type", "options": {}}],
69
+ )
70
+
71
+ class Meta:
72
+ model = RuleCheckedObjectRelationship
73
+ # dependency_map = {
74
+ # "checked_object_content_type": ["checked_object_id"],
75
+ # }
76
+ read_only_fields = ("computed_str", "checked_object_repr")
77
+ fields = (
78
+ "id",
79
+ "rule",
80
+ "checked_object_content_type",
81
+ "_checked_object_content_type",
82
+ "checked_object_id",
83
+ "_checked_object_id",
84
+ "checked_object_repr",
85
+ "computed_str",
86
+ )
87
+
88
+
89
+ class RuleBackendModelSerializer(wb_serializers.ModelSerializer):
90
+ class Meta:
91
+ model = RuleBackend
92
+ fields = ("id", "name", "backend_class_path", "backend_class_name", "allowed_checked_object_content_type")
93
+ read_only_fields = fields
94
+
95
+
96
+ class RiskRuleModelSerializer(wb_serializers.ModelSerializer):
97
+ parameters__group_by = wb_serializers.CharField(read_only=True)
98
+ _rule_backend = RuleBackendRepresentationSerializer(source="rule_backend")
99
+ _creator = UserRepresentationSerializer(source="creator")
100
+ parameters = wb_serializers.JSONTableField()
101
+ open_incidents_count = wb_serializers.IntegerField(default=0, read_only=True)
102
+ in_breach = wb_serializers.ChoiceField(
103
+ default=False,
104
+ read_only=True,
105
+ choices=[("BREACH", "In Breach"), ("PASSED", "Passed"), ("INACTIVE", "Inactive")],
106
+ )
107
+
108
+ def validate(self, data):
109
+ if (not self.instance or not self.instance.creator) and (request := self.context.get("request")):
110
+ data["creator"] = request.user
111
+ return super().validate(data)
112
+
113
+ @wb_serializers.register_resource()
114
+ def additional_resources(self, instance, request, user):
115
+ return {
116
+ "relationships": reverse(
117
+ "wbcompliance:riskrule-relationship-list",
118
+ args=[instance.id],
119
+ request=request,
120
+ ),
121
+ "thresholds": reverse(
122
+ "wbcompliance:riskrule-threshold-list",
123
+ args=[instance.id],
124
+ request=request,
125
+ ),
126
+ "incidents": reverse(
127
+ "wbcompliance:riskrule-incident-list",
128
+ args=[instance.id],
129
+ request=request,
130
+ ),
131
+ }
132
+
133
+ class Meta:
134
+ model = RiskRule
135
+ read_only_fields = ("creator",)
136
+ fields = (
137
+ "id",
138
+ "parameters__group_by",
139
+ "permission_type",
140
+ "creator",
141
+ "_creator",
142
+ "name",
143
+ "description",
144
+ "rule_backend",
145
+ "_rule_backend",
146
+ "is_enable",
147
+ "only_passive_check_allowed",
148
+ "automatically_close_incident",
149
+ "is_silent",
150
+ "is_mandatory",
151
+ "apply_to_all_active_relationships",
152
+ "parameters",
153
+ "open_incidents_count",
154
+ "in_breach",
155
+ "frequency",
156
+ "activation_date",
157
+ "_additional_resources",
158
+ )
wbcompliance/tasks.py ADDED
@@ -0,0 +1,112 @@
1
+ from collections import defaultdict
2
+ from datetime import date, datetime
3
+
4
+ from celery import shared_task
5
+ from django.contrib.contenttypes.models import ContentType
6
+ from django.db.models import Q
7
+ from tqdm import tqdm
8
+ from wbcompliance.models import ComplianceTask, ReviewComplianceTask
9
+ from wbcompliance.models.risk_management.rules import (
10
+ RiskRule,
11
+ RuleCheckedObjectRelationship,
12
+ )
13
+
14
+
15
+ @shared_task
16
+ def check_passive_rules(
17
+ from_date: date | None = None,
18
+ to_date: date | None = None,
19
+ override_incident: bool | None = False,
20
+ extra_process_kwargs: dict | None = None,
21
+ silent_notification: bool = False,
22
+ debug: bool = False,
23
+ ):
24
+ """
25
+ Periodic function that call all active passive rules and trigger the check workflow
26
+ """
27
+ if not to_date:
28
+ to_date = datetime.today().date()
29
+
30
+ # cleanup relationship before continuing
31
+ clean_dynamic_rule_relationships(debug=debug)
32
+
33
+ process_kwargs = {
34
+ "override_incident": override_incident,
35
+ }
36
+ if isinstance(extra_process_kwargs, dict):
37
+ process_kwargs.update(extra_process_kwargs)
38
+
39
+ res = []
40
+ for rule in RiskRule.objects.filter(is_enable=True):
41
+ for relationship in rule.checked_object_relationships.iterator():
42
+ res.append((rule, relationship))
43
+ gen = res
44
+ if debug:
45
+ gen = tqdm(gen, total=len(res))
46
+ date_to_notify = defaultdict(set)
47
+ for rule, relationship in gen:
48
+ for evaluation_date in relationship.get_unchecked_dates(from_date=from_date, to_date=to_date):
49
+ if relationship.process_rule(evaluation_date, **process_kwargs):
50
+ date_to_notify[rule].add(evaluation_date)
51
+
52
+ if not silent_notification:
53
+ for rule, dates in date_to_notify.items():
54
+ for checked_date in dates:
55
+ rule.notify(checked_date)
56
+
57
+
58
+ @shared_task
59
+ def clean_dynamic_rule_relationships(debug: bool = False):
60
+ """
61
+ Periodic function to check reverse generic object that don't exist anymore. Furthermore, get the queryset representing all the active relationship for all backend and ensure that every object have a relationship (e.g. new object creation)
62
+ """
63
+ gen = RiskRule.objects.filter(apply_to_all_active_relationships=True)
64
+ if debug:
65
+ gen = tqdm(gen, total=gen.count())
66
+
67
+ for rule in gen:
68
+ leftover_relationships = rule.checked_object_relationships.all()
69
+ for content_object in rule.rule_backend.get_all_active_relationships():
70
+ rel, _ = RuleCheckedObjectRelationship.objects.get_or_create(
71
+ rule=rule,
72
+ checked_object_content_type=ContentType.objects.get_for_model(content_object),
73
+ checked_object_id=content_object.id,
74
+ )
75
+ leftover_relationships = leftover_relationships.exclude(id=rel.id)
76
+
77
+ for leftover_relationship in leftover_relationships.iterator():
78
+ leftover_relationship.delete()
79
+
80
+
81
+ @shared_task
82
+ def periodic_quaterly_or_monthly_compliance_task():
83
+ today = datetime.now()
84
+ qs = ComplianceTask.objects.filter(active=True)
85
+ qs_review = ReviewComplianceTask.objects.filter(
86
+ Q(status=ReviewComplianceTask.Status.VALIDATED) & Q(is_instance=False)
87
+ )
88
+ if today.month == 1 and today.day == 1:
89
+ qs = qs.filter(
90
+ Q(occurrence=ComplianceTask.Occurrence.YEARLY) | Q(occurrence=ComplianceTask.Occurrence.MONTHLY)
91
+ )
92
+ qs_review = qs_review.filter(
93
+ Q(occurrence=ReviewComplianceTask.Occurrence.YEARLY)
94
+ | Q(occurrence=ReviewComplianceTask.Occurrence.MONTHLY)
95
+ )
96
+ elif today.month % 3 == 0:
97
+ qs = qs.filter(
98
+ Q(occurrence=ComplianceTask.Occurrence.QUARTERLY) | Q(occurrence=ComplianceTask.Occurrence.MONTHLY)
99
+ )
100
+ qs_review = qs_review.filter(
101
+ Q(occurrence=ReviewComplianceTask.Occurrence.QUARTERLY)
102
+ | Q(occurrence=ReviewComplianceTask.Occurrence.MONTHLY)
103
+ )
104
+ else:
105
+ qs = qs.filter(occurrence=ComplianceTask.Occurrence.MONTHLY)
106
+ qs_review = qs_review.filter(occurrence=ReviewComplianceTask.Occurrence.MONTHLY)
107
+
108
+ for review_task in qs_review:
109
+ review_task.generate_review_compliance_task_instance(notify_admin=True)
110
+
111
+ for task in qs:
112
+ task.generate_compliance_task_instance(link_instance_review=True)
File without changes
@@ -0,0 +1,63 @@
1
+ from django.apps import apps
2
+ from django.db.models.signals import pre_migrate
3
+ from pytest_factoryboy import register
4
+ from wbcompliance.factories import (
5
+ ComplianceActionFactory,
6
+ ComplianceEventFactory,
7
+ ComplianceFormFactory,
8
+ ComplianceFormRuleFactory,
9
+ ComplianceFormSectionFactory,
10
+ ComplianceFormSignatureFactory,
11
+ ComplianceFormTypeFactory,
12
+ ComplianceTaskFactory,
13
+ ComplianceTaskGroupFactory,
14
+ ComplianceTaskInstanceFactory,
15
+ ComplianceTypeFactory,
16
+ UnsignedComplianceFormSignatureFactory,
17
+ )
18
+ from wbcompliance.factories.risk_management import (
19
+ CheckedObjectIncidentRelationshipFactory,
20
+ RiskCheckFactory,
21
+ RiskIncidentFactory,
22
+ RiskIncidentTypeFactory,
23
+ RiskRuleFactory,
24
+ RuleBackendFactory,
25
+ RuleCheckedObjectRelationshipFactory,
26
+ RuleThresholdFactory,
27
+ )
28
+ from wbcore.contrib.authentication.factories import (
29
+ AuthenticatedPersonFactory,
30
+ UserFactory,
31
+ )
32
+ from wbcore.contrib.directory.factories import PersonFactory
33
+
34
+ from wbcore.tests.conftest import * # isort:skip
35
+
36
+ register(ComplianceFormRuleFactory)
37
+ register(ComplianceFormSectionFactory)
38
+ register(ComplianceFormTypeFactory)
39
+ register(ComplianceFormFactory)
40
+ register(ComplianceFormSignatureFactory)
41
+ register(UnsignedComplianceFormSignatureFactory)
42
+ register(ComplianceTaskFactory)
43
+ register(ComplianceTaskInstanceFactory)
44
+ register(ComplianceActionFactory)
45
+ register(ComplianceEventFactory)
46
+ register(ComplianceTypeFactory)
47
+ register(ComplianceTaskGroupFactory)
48
+ register(RiskCheckFactory)
49
+ register(RiskIncidentFactory)
50
+ register(CheckedObjectIncidentRelationshipFactory)
51
+ register(RuleBackendFactory)
52
+ register(RiskRuleFactory)
53
+ register(RuleThresholdFactory)
54
+ register(RuleCheckedObjectRelationshipFactory)
55
+ register(RiskIncidentTypeFactory)
56
+ register(PersonFactory)
57
+ register(UserFactory)
58
+ register(AuthenticatedPersonFactory)
59
+
60
+
61
+ from .signals import *
62
+
63
+ pre_migrate.connect(app_pre_migration, sender=apps.get_app_config("wbcompliance"))
@@ -0,0 +1,82 @@
1
+ from collections import defaultdict
2
+
3
+ from django.db.models.signals import *
4
+
5
+
6
+ class DisableSignals(object):
7
+ def __init__(self, disabled_signals=None):
8
+ self.stashed_signals = defaultdict(list)
9
+ self.disabled_signals = disabled_signals or [
10
+ pre_init,
11
+ post_init,
12
+ pre_save,
13
+ post_save,
14
+ pre_delete,
15
+ post_delete,
16
+ pre_migrate,
17
+ post_migrate,
18
+ ]
19
+
20
+ def __enter__(self):
21
+ for signal in self.disabled_signals:
22
+ self.disconnect(signal)
23
+
24
+ def __exit__(self, exc_type, exc_val, exc_tb):
25
+ for signal in list(self.stashed_signals):
26
+ self.reconnect(signal)
27
+
28
+ def disconnect(self, signal):
29
+ self.stashed_signals[signal] = signal.receivers
30
+ signal.receivers = []
31
+
32
+ def reconnect(self, signal):
33
+ signal.receivers = self.stashed_signals.get(signal, [])
34
+ del self.stashed_signals[signal]
35
+
36
+
37
+ # https://stackoverflow.com/questions/55578230/django-how-to-visualize-signals-and-save-overrides
38
+ # RECEIVER_MODELS = re.compile(r"sender=(\w+)\W")
39
+
40
+
41
+ # class DisableSignalsNotification_OLD_VERSION(DisableSignals):
42
+ # def __enter__(self):
43
+ # for signal in self.disabled_signals:
44
+ # if not isinstance(signal, ModelSignal):
45
+ # continue
46
+ # for _, receiver in signal.receivers:
47
+ # rcode = inspect.getsource(receiver())
48
+ # rmodel = RECEIVER_MODELS.findall(rcode)
49
+ # if "Notification" in rmodel:
50
+ # self.disconnect(signal)
51
+
52
+
53
+ class temp_disconnect_signal:
54
+ """Temporarily disconnect a model from a signal"""
55
+
56
+ def __init__(self, signal, receiver, sender, dispatch_uid=None):
57
+ self.signal = signal
58
+ self.receiver = receiver
59
+ self.sender = sender
60
+ self.dispatch_uid = dispatch_uid
61
+
62
+ def __enter__(self):
63
+ self.signal.disconnect(
64
+ receiver=self.receiver,
65
+ sender=self.sender,
66
+ dispatch_uid=self.dispatch_uid,
67
+ )
68
+
69
+ def __exit__(self, type, value, traceback):
70
+ self.signal.connect(
71
+ receiver=self.receiver,
72
+ sender=self.sender,
73
+ dispatch_uid=self.dispatch_uid,
74
+ )
75
+
76
+
77
+ class DisableSignalsNotification(temp_disconnect_signal):
78
+ def __init__(self, dispatch_uid=None):
79
+ self.signal = post_save
80
+ self.receiver = post_create_notification
81
+ self.sender = Notification
82
+ self.dispatch_uid = dispatch_uid
@@ -0,0 +1,17 @@
1
+ import pytest
2
+ from django.contrib.auth import get_user_model
3
+ from django.contrib.auth.models import Group
4
+ from faker import Faker
5
+ from wbcore.contrib.authentication.factories import InternalUserFactory
6
+
7
+ fake = Faker()
8
+ User = get_user_model()
9
+
10
+
11
+ class UserTestMixin:
12
+ @pytest.fixture()
13
+ def user(self):
14
+ user = InternalUserFactory.create()
15
+ group = Group.objects.create(name="Compliance Position")
16
+ user.groups.add(group)
17
+ return user
File without changes
File without changes