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,133 @@
1
+ import logging
2
+ from typing import TypeVar
3
+
4
+ from celery import shared_task
5
+ from django.contrib.auth import get_user_model
6
+ from django.contrib.auth.models import Group
7
+ from django.contrib.contenttypes.models import ContentType
8
+ from django.core.files.base import ContentFile
9
+ from django.db import models
10
+ from django.db.models import Q
11
+ from django.utils.translation import gettext_lazy as _
12
+ from slugify import slugify
13
+ from wbcore.contrib.documents.models import Document, DocumentType
14
+ from wbcore.contrib.documents.models.mixins import DocumentMixin
15
+ from wbcore.models import WBModel
16
+
17
+ User = get_user_model()
18
+ SelfComplianceType = TypeVar("SelfComplianceType", bound="ComplianceType")
19
+
20
+
21
+ def can_active_request(instance, user: "User") -> bool:
22
+ if instance.changer:
23
+ current_profile = instance.changer
24
+ else:
25
+ current_profile = instance.creator
26
+
27
+ return user.is_superuser or (
28
+ user.profile != current_profile and user.has_perm("wbcompliance.administrate_compliance")
29
+ )
30
+
31
+
32
+ class ComplianceType(WBModel):
33
+ class Meta:
34
+ verbose_name = "Compliance Type"
35
+ verbose_name_plural = "Compliance Types"
36
+ permissions = [("administrate_compliance", "Can Administrate Compliance")]
37
+
38
+ name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True)
39
+ description = models.TextField(default="", blank=True, verbose_name=_("Description"))
40
+ in_charge = models.ManyToManyField(
41
+ Group,
42
+ related_name="compliance_types",
43
+ blank=True,
44
+ verbose_name=_("Group of administrators"),
45
+ help_text=_("groups responsible for managing this type of compliance"),
46
+ )
47
+
48
+ def __str__(self) -> str:
49
+ return "{}".format(self.name)
50
+
51
+ @classmethod
52
+ def get_endpoint_basename(cls) -> str:
53
+ return "wbcompliance:compliancetype"
54
+
55
+ @classmethod
56
+ def get_representation_endpoint(cls) -> str:
57
+ return "wbcompliance:compliancetyperepresentation-list"
58
+
59
+ @classmethod
60
+ def get_representation_value_key(cls) -> str:
61
+ return "id"
62
+
63
+ @classmethod
64
+ def get_representation_label_key(cls) -> str:
65
+ return "{{name}}"
66
+
67
+ @classmethod
68
+ def get_administrators(cls, type: SelfComplianceType | None = None) -> models.QuerySet["User"]:
69
+ administrators = (
70
+ get_user_model()
71
+ .objects.filter(
72
+ Q(groups__permissions__codename="administrate_compliance")
73
+ | Q(user_permissions__codename="administrate_compliance")
74
+ )
75
+ .distinct()
76
+ )
77
+ if type:
78
+ administrators = administrators.filter(groups__in=type.in_charge.all())
79
+ return administrators
80
+
81
+ @classmethod
82
+ def is_administrator(cls, user) -> bool:
83
+ return user.has_perm("wbcompliance.administrate_compliance")
84
+
85
+
86
+ @shared_task
87
+ def update_or_create_compliance_document(user_id: int, content_type_id: int, object_id: int, send_email: bool = True):
88
+ user = get_user_model().objects.get(id=user_id)
89
+ content_type = ContentType.objects.get(id=content_type_id)
90
+ content_object = content_type.model_class().objects.get(id=object_id)
91
+
92
+ filename = "{}.pdf".format(slugify(str(content_object)))
93
+ logging.getLogger("weasyprint").setLevel(logging.CRITICAL)
94
+ pdf_content = content_object.generate_pdf()
95
+ logging.getLogger("weasyprint").setLevel(logging.INFO)
96
+ content_file = ContentFile(pdf_content, name=filename)
97
+
98
+ document_type, _ = DocumentType.objects.get_or_create(name="Compliance")
99
+ document, _ = Document.objects.update_or_create(
100
+ document_type=document_type,
101
+ system_created=True,
102
+ system_key="{}-{}-{}".format(content_type.model, content_object.id, filename),
103
+ defaults={
104
+ "file": content_file,
105
+ "name": filename,
106
+ "permission_type": Document.PermissionType.PRIVATE,
107
+ "creator": user,
108
+ },
109
+ )
110
+ document.link(content_object)
111
+ if send_email:
112
+ document.send_email(
113
+ to_emails=user.email,
114
+ as_link=True,
115
+ subject="Compliance PDF - {}".format(str(content_object)),
116
+ )
117
+
118
+
119
+ class ComplianceDocumentMixin(DocumentMixin):
120
+ def get_permissions_for_user_and_document(self, user, view, created):
121
+ """
122
+ allows user to access the document associated with this object
123
+ :return: The tuple list permission for the corresponding user
124
+
125
+ :note: Core implements a signal that automatically calls this function when instantiating the view
126
+ """
127
+ if ComplianceType.is_administrator(user):
128
+ return [
129
+ ("document.view_document", False),
130
+ ("document.change_document", False),
131
+ ("document.delete_document", False),
132
+ ]
133
+ return []
@@ -0,0 +1,13 @@
1
+ from django.db import models
2
+ from wbcore.contrib.color.enums import WBColor
3
+
4
+
5
+ class IncidentSeverity(models.TextChoices):
6
+ LOW = "LOW", "Low"
7
+ MEDIUM = "MEDIUM", "Medium"
8
+ HIGH = "HIGH", "High"
9
+
10
+ @classmethod
11
+ def get_color_map(cls) -> list:
12
+ colors = [WBColor.GREEN_LIGHT.value, WBColor.YELLOW_LIGHT.value, WBColor.RED_LIGHT.value]
13
+ return [choice for choice in zip(cls, colors)]
@@ -0,0 +1,4 @@
1
+ from .backend import AbstractRuleBackend
2
+ from .checks import RiskCheck
3
+ from .incidents import CheckedObjectIncidentRelationship, RiskIncident, RiskIncidentType
4
+ from .rules import RiskRule, RuleBackend, RuleCheckedObjectRelationship, RuleThreshold
@@ -0,0 +1,139 @@
1
+ from dataclasses import dataclass
2
+ from datetime import date
3
+ from typing import TYPE_CHECKING, Any, Dict, Generator, Iterable, Optional
4
+
5
+ from django.contrib.contenttypes.models import ContentType
6
+ from django.db import models
7
+ from wbcore import serializers as wb_serializers
8
+
9
+ if TYPE_CHECKING:
10
+ from wbcompliance.models.risk_management.incidents import RiskIncidentType
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class IncidentResult:
15
+ breached_object: models.Model | None
16
+ breached_object_repr: str
17
+ breached_value: str | None
18
+ report_details: dict[str, Any]
19
+ severity: "RiskIncidentType"
20
+
21
+
22
+ class AbstractRuleBackend:
23
+ OBJECT_FIELD_NAME = "evaluated_object"
24
+ evaluated_object: Any
25
+ evaluation_date: date
26
+ parameters: dict[str, Any]
27
+
28
+ def __init__(
29
+ self,
30
+ evaluation_date: date,
31
+ evaluated_object: Any,
32
+ json_parameters: Optional[Dict[str, Any]] = None,
33
+ thresholds: Optional[
34
+ Iterable
35
+ ] = None, # TODO refactor threshold to be a dataclass (DTO) to remove indirect dependency to the module
36
+ **kwargs
37
+ ):
38
+ if not json_parameters:
39
+ json_parameters = {}
40
+ self.evaluation_date = evaluation_date
41
+ setattr(self, self.OBJECT_FIELD_NAME, evaluated_object)
42
+ self.thresholds = thresholds if thresholds else []
43
+ self.parameters = self._deserialize_parameters(json_parameters)
44
+ for k, v in self.parameters.items():
45
+ setattr(self, k, v)
46
+
47
+ @classmethod
48
+ def get_all_active_relationships(cls) -> Iterable:
49
+ raise NotImplementedError()
50
+
51
+ @classmethod
52
+ def get_serializer_class(cls) -> wb_serializers.Serializer:
53
+ """
54
+ Return the serializer to deserialize the parameters given as json
55
+ Returns:
56
+ A serializer class
57
+ Raises:
58
+ NotImplementedError
59
+ """
60
+ raise NotImplementedError()
61
+
62
+ @classmethod
63
+ def _deserialize_parameters(cls, json_parameters) -> Dict[str, Any]:
64
+ """
65
+ The parameters are stored in the rule as a json field.
66
+
67
+ E.g. As such, model are stored with their id.
68
+
69
+ This allows for the implementing backend to override the default deserialization behavior
70
+ Args:
71
+ json_parameters: The serialized dictionary
72
+
73
+ Returns:
74
+ The deserializer dictionary. Default to returning the json parameters untouched
75
+ """
76
+ # If parameters are not empty, we expect the backend to define `get_serializer_class. This will crash otherwise
77
+ serializer = cls.get_serializer_class()(data=json_parameters)
78
+ if serializer.is_valid():
79
+ return serializer.validated_data
80
+ else:
81
+ raise ValueError(serializer.errors)
82
+
83
+ @classmethod
84
+ def get_allowed_content_type(cls) -> "ContentType":
85
+ """
86
+ This function is called upon backend registration in order to determine the allowed checked_object content type
87
+
88
+ Returns:
89
+ The allowed content type
90
+
91
+ Raises:
92
+ NotImplementedError
93
+ """
94
+ raise NotImplementedError()
95
+
96
+ def _build_dto_args(self):
97
+ """
98
+ Can be overrided to define what the default DataTransferObject is going to be
99
+ It defaults to calling _build_dto on the evaluated object
100
+ Returns:
101
+ An object holding the DTO representation
102
+ """
103
+ if hasattr(self.evaluated_object, "_build_dto"):
104
+ return self.evaluated_object._build_dto(self.evaluation_date)
105
+ return tuple()
106
+
107
+ def _process_dto(self, *dtos) -> Generator[IncidentResult, None, None]:
108
+ """
109
+ Check the rule against the instantiated backend given the DTO
110
+
111
+ Returns:
112
+ yield the breach object, its string representation, the incident report and the severity as IncidentResult
113
+
114
+ Raises:
115
+ NotImplementedError
116
+ """
117
+ raise NotImplementedError()
118
+
119
+ def check_rule(self, *dto_args, **kwargs) -> Generator[IncidentResult, None, None]:
120
+ """
121
+ Build and Check the rule against the instantiated backend given its attributes
122
+
123
+ Returns:
124
+ yield the breach object, its string representation, the incident report and the severity as IncidentResult
125
+ """
126
+ if not dto_args:
127
+ # We build the data transfer object if it is not provided.
128
+ dto_args = self._build_dto_args()
129
+ if any(dto_args):
130
+ yield from self._process_dto(*dto_args, **kwargs)
131
+
132
+ def is_passive_evaluation_valid(self) -> bool:
133
+ """
134
+ Determine if the instantiated backend can be evaluated given its attributes
135
+
136
+ Returns:
137
+ True if the backend rule can be evaluated
138
+ """
139
+ return True
@@ -0,0 +1,194 @@
1
+ from contextlib import suppress
2
+ from types import DynamicClassAttribute
3
+ from typing import Self
4
+
5
+ from celery import shared_task
6
+ from django.contrib.contenttypes.fields import GenericForeignKey
7
+ from django.contrib.contenttypes.models import ContentType
8
+ from django.db import models
9
+ from django.template import Context, Template
10
+ from django.utils.translation import gettext_lazy as _
11
+ from wbcore.contrib.color.enums import WBColor
12
+ from wbcore.contrib.icons import WBIcon
13
+ from wbcore.models import WBModel
14
+ from wbcore.utils.models import ComplexToStringMixin
15
+
16
+ from .incidents import CheckedObjectIncidentRelationship
17
+
18
+
19
+ class RiskCheck(ComplexToStringMixin, WBModel):
20
+ class CheckStatus(models.TextChoices):
21
+ PENDING = "PENDING", "Pending"
22
+ RUNNING = "RUNNING", "Running"
23
+ FAILED = "FAILED", "Failed"
24
+ SUCCESS = "SUCCESS", "Success"
25
+ WARNING = "WARNING", "Warning"
26
+
27
+ @DynamicClassAttribute
28
+ def icon(self):
29
+ return {
30
+ "PENDING": WBIcon.PENDING.icon,
31
+ "RUNNING": WBIcon.RUNNING.icon,
32
+ "FAILED": WBIcon.REJECT.icon,
33
+ "SUCCESS": WBIcon.CONFIRM.icon,
34
+ "WARNING": WBIcon.WARNING.icon,
35
+ }[self.value]
36
+
37
+ @DynamicClassAttribute
38
+ def color(self):
39
+ return {
40
+ "PENDING": WBColor.YELLOW_LIGHT.value,
41
+ "RUNNING": WBColor.BLUE_LIGHT.value,
42
+ "FAILED": WBColor.RED_LIGHT.value,
43
+ "SUCCESS": WBColor.GREEN_LIGHT.value,
44
+ "WARNING": WBColor.YELLOW_DARK.value,
45
+ }[self.value]
46
+
47
+ rule_checked_object_relationship = models.ForeignKey(
48
+ "wbcompliance.RuleCheckedObjectRelationship",
49
+ on_delete=models.CASCADE,
50
+ verbose_name=_("Rule-Checked Object Relationship"),
51
+ related_name="checks",
52
+ )
53
+
54
+ creation_datetime = models.DateTimeField(
55
+ auto_now_add=True,
56
+ verbose_name=_("Creation Date"),
57
+ help_text=_("Time at which the check was created/triggered"),
58
+ )
59
+ evaluation_date = models.DateField(
60
+ verbose_name=_("Evaluation Date"), help_text=_("The date at which the rule was evaluated")
61
+ )
62
+
63
+ trigger_content_type = models.ForeignKey(
64
+ ContentType, on_delete=models.CASCADE, related_name="triggered_checks", blank=True, null=True
65
+ )
66
+ trigger_id = models.PositiveIntegerField(blank=True, null=True)
67
+ trigger = GenericForeignKey("trigger_content_type", "trigger_id")
68
+
69
+ status = models.CharField(
70
+ max_length=32, default=CheckStatus.PENDING, choices=CheckStatus.choices, verbose_name=_("Status")
71
+ )
72
+
73
+ @property
74
+ def is_active(self) -> bool:
75
+ return self.trigger is not None
76
+
77
+ @property
78
+ def rule(self):
79
+ return self.rule_checked_object_relationship.rule
80
+
81
+ @property
82
+ def checked_object(self) -> models.Model:
83
+ """
84
+ Return the object from which the rule needs to be check against
85
+ """
86
+ return self.rule_checked_object_relationship.checked_object
87
+
88
+ @property
89
+ def previous_check(self) -> Self | None:
90
+ """
91
+ Property holding the last valid check
92
+
93
+ Returns:
94
+ The last valid check
95
+ """
96
+ with suppress(RiskCheck.DoesNotExist):
97
+ return (
98
+ RiskCheck.objects.filter(
99
+ evaluation_date__lt=self.evaluation_date,
100
+ rule_checked_object_relationship=self.rule_checked_object_relationship,
101
+ )
102
+ .order_by("-evaluation_date", "-creation_datetime")
103
+ .first()
104
+ )
105
+
106
+ def compute_str(self) -> str:
107
+ return _("{} - {}").format(
108
+ self.rule_checked_object_relationship.checked_object,
109
+ self.evaluation_date,
110
+ )
111
+
112
+ def evaluate(
113
+ self, *explicit_dto, override_incident: bool = False, ignore_informational_threshold: bool = False
114
+ ) -> list[models.Model]:
115
+ """
116
+ Evaluate the check and returns tuple of incidents information
117
+ Args:
118
+ override_incident: True if the existing incident needs to be overriden
119
+
120
+ Returns:
121
+
122
+ """
123
+ self.status = self.CheckStatus.RUNNING
124
+ self.save()
125
+ rule_backend = self.rule.rule_backend.backend(
126
+ self.evaluation_date, self.checked_object, self.rule.parameters, self.rule.thresholds.all()
127
+ )
128
+ self.status = self.CheckStatus.SUCCESS
129
+ incidents = []
130
+ report_template = Template(self.rule.rule_backend.incident_report_template)
131
+ for incident_result in rule_backend.check_rule(*explicit_dto):
132
+ if (
133
+ ignore_informational_threshold
134
+ and incident_result.severity.is_ignorable
135
+ and incident_result.severity.is_informational
136
+ ):
137
+ self.status = self.CheckStatus.WARNING
138
+ else:
139
+ self.status = self.CheckStatus.FAILED
140
+ # If the check is passive, we aggregated incident per breached object and return it for further processing
141
+ report = report_template.render(Context({"report_details": incident_result.report_details}))
142
+ if not self.is_active:
143
+ incident, created = self.rule.get_or_create_incident(
144
+ self.evaluation_date,
145
+ incident_result.severity,
146
+ incident_result.breached_object,
147
+ incident_result.breached_object_repr,
148
+ )
149
+ incident.update_or_create_relationship(
150
+ self,
151
+ report,
152
+ incident_result.report_details,
153
+ incident_result.breached_value,
154
+ incident_result.severity,
155
+ override_incident=override_incident or created,
156
+ )
157
+ incidents.append(incident)
158
+ else:
159
+ # If the check is active, the only thing that matter is whether the check led to incident or not
160
+ CheckedObjectIncidentRelationship.objects.create(
161
+ rule_check=self,
162
+ report=report,
163
+ report_details=incident_result.report_details,
164
+ breached_value=incident_result.breached_value,
165
+ severity=incident_result.severity,
166
+ )
167
+ self.save()
168
+ return incidents
169
+
170
+ class Meta:
171
+ verbose_name = "Risk Check"
172
+ verbose_name_plural = "Risk Checks"
173
+
174
+ @classmethod
175
+ def get_endpoint_basename(cls) -> str:
176
+ return "wbcompliance:riskcheck"
177
+
178
+ @classmethod
179
+ def get_representation_endpoint(cls) -> str:
180
+ return "wbcompliance:riskcheckrepresentation-list"
181
+
182
+ @classmethod
183
+ def get_representation_value_key(cls) -> str:
184
+ return "id"
185
+
186
+
187
+ @shared_task
188
+ def evaluate_as_task(
189
+ check_id: int, *dto, override_incident: bool = False, ignore_informational_threshold: bool = False
190
+ ):
191
+ check = RiskCheck.objects.get(id=check_id)
192
+ check.evaluate(
193
+ *dto, override_incident=override_incident, ignore_informational_threshold=ignore_informational_threshold
194
+ )
@@ -0,0 +1,41 @@
1
+ from contextlib import suppress
2
+
3
+ from django.core.exceptions import MultipleObjectsReturned
4
+ from django.db.utils import ProgrammingError
5
+
6
+ from .rules import RuleBackend, RuleGroup
7
+
8
+
9
+ def register(backend_name: str | None, incident_report_template: str | None = None, rule_group_key: str | None = None):
10
+ """
11
+ Decorator to include when a backend need automatic registration
12
+ Args:
13
+ backend_name:
14
+
15
+ Returns:
16
+
17
+ """
18
+ if not backend_name:
19
+ raise ValueError("At least one name must be passed to register.")
20
+
21
+ def _decorator(backend_class):
22
+ with suppress(RuntimeError, MultipleObjectsReturned, ProgrammingError):
23
+ defaults = {
24
+ "name": backend_name,
25
+ "allowed_checked_object_content_type": backend_class.get_allowed_content_type(),
26
+ }
27
+ if incident_report_template:
28
+ defaults["incident_report_template"] = incident_report_template
29
+ if rule_group_key:
30
+ defaults["rule_group"] = RuleGroup.objects.get_or_create(
31
+ key=rule_group_key, defaults={"name": rule_group_key.title()}
32
+ )[0]
33
+
34
+ RuleBackend.objects.update_or_create(
35
+ backend_class_path=backend_class.__module__,
36
+ backend_class_name=backend_class.__name__,
37
+ defaults=defaults,
38
+ )
39
+ return backend_class
40
+
41
+ return _decorator