Coverage for backend/django/core/managers.py: 91%
73 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-12-18 04:00 +0000
« prev ^ index » next coverage.py v7.10.7, created at 2025-12-18 04:00 +0000
1from django.db import models
2from authentication.user.models import User
3from core.auxiliary.enums.FlowsheetTemplateType import FlowsheetTemplateType
4from core.auxiliary.models.Flowsheet import Flowsheet
5from core.validation import get_current_flowsheet, cache_result
6from authentication.user.AccessTable import AccessTable
7from typing import TypeVar
8t = TypeVar('t', bound=models.Model, covariant=True)
11def verify_flowsheet_access(user: User, flowsheet_id: int) -> bool:
12 """
13 Verify if the user has access to the flowsheet either directly or through public template edit access.
15 :param user: User object
16 :param flowsheet_id: Flowsheet ID to check access for
18 :return: True if the user has access, False otherwise
19 """
20 user_has_direct_access = AccessTable.objects.filter(
21 user_id=user.id,
22 flowsheet_id=flowsheet_id
23 ).exists()
25 user_can_edit_template = False
27 # Check if this is a public flowsheet template that the user can edit (only admins can edit public templates)
28 if user.is_staff and not user_has_direct_access: 28 ↛ 29line 28 didn't jump to line 29 because the condition on line 28 was never true
29 flowsheet: Flowsheet = Flowsheet.objects.get(id=flowsheet_id)
30 user_can_edit_template = flowsheet.flowsheet_template_type == FlowsheetTemplateType.PublicTemplate
32 access_granted = user_has_direct_access or user_can_edit_template
34 cache_result(has_access=access_granted)
36 return access_granted
39class AccessControlManager(models.Manager):
40 """
41 Custom object manager to enforce access control based on the current flowsheet and user context.
42 This manager overrides the default queryset to filter objects based on the flowsheet.
44 By default, object modification methods (update, delete, etc.) will be scoped only to the
45 active flowsheet that the user has access to. Creation methods check explicitly if the user has
46 access to the flowsheet before allowing creation.
47 """
49 def __init__(self):
50 super().__init__()
51 self.flowsheet_related_name = "flowsheet"
53 def create(self, **kwargs):
54 ctx = get_current_flowsheet() or {}
56 flowsheet = ctx.get("flowsheet")
57 user = ctx.get("user")
58 has_access = ctx.get("has_access")
60 # This is only the case when tests uses the objects directly without going through a view
61 if flowsheet == None or user == None:
62 return super().create(**kwargs)
64 if has_access is None: 64 ↛ 66line 64 didn't jump to line 66 because the condition on line 64 was never true
65 # Check access and cache the result
66 has_access = verify_flowsheet_access(user=user, flowsheet_id=flowsheet)
68 kwargs[f"{self.flowsheet_related_name}_id"] = flowsheet
70 if has_access: 70 ↛ 73line 70 didn't jump to line 73 because the condition on line 70 was always true
71 return super().create(**kwargs)
72 else:
73 raise PermissionError("User does not have access to this flowsheet.")
75 def bulk_create(self, objs, **kwargs):
76 ctx = get_current_flowsheet() or {}
78 flowsheet = ctx.get("flowsheet")
79 user = ctx.get("user")
80 has_access = ctx.get("has_access")
82 # This is only the case when tests uses the objects directly without going through a view
83 if flowsheet == None or user == None:
84 return super().bulk_create(objs, **kwargs)
86 if has_access is None:
87 # Check access and cache the result
88 has_access = verify_flowsheet_access(user=user, flowsheet_id=flowsheet)
90 # Set flowsheet ID on each object before bulk create
91 for obj in objs:
92 setattr(obj, f"{self.flowsheet_related_name}_id", flowsheet)
94 if has_access: 94 ↛ 97line 94 didn't jump to line 97 because the condition on line 94 was always true
95 return super().bulk_create(objs, **kwargs)
96 else:
97 raise PermissionError("User does not have access to this flowsheet.")
98 def get_queryset(self):
99 """
100 Override the get_queryset method to filter objects based on the current flowsheet and user access.
101 """
102 qs = super().get_queryset()
103 ctx = get_current_flowsheet() or {}
105 flowsheet = ctx.get("flowsheet")
106 user = ctx.get("user")
107 has_access = ctx.get("has_access")
109 # This is only the case when tests uses the objects directly without going through a view
110 if flowsheet == None or user == None:
111 return qs
113 # Use cached access result if present
114 if has_access is None:
115 # Check access and cache the result
116 has_access = verify_flowsheet_access(user=user, flowsheet_id=flowsheet)
118 filter_kwargs = {f"{self.flowsheet_related_name}_id": flowsheet}
120 if has_access:
121 return qs.filter(**filter_kwargs)
122 else:
123 return qs.none()
125class SoftDeleteManager(AccessControlManager):
126 def __init__(self):
127 super().__init__()
128 def get_queryset(self):
129 # filter out deleted objects
130 return super().get_queryset().filter(is_deleted=False)
132 def include_deleted(self):
133 # include deleted objects
134 return super().get_queryset()
138def include_soft_deleted[t](objects: models.Manager[t]) -> models.QuerySet[t]:
139 """
140 In some operations, including copying a flowsheet,
141 we need to include the soft deleted objects
142 Example usage:
143 include_soft_deleted(SimulationObject.objects).filter(flowsheet_id=1)
144 """
145 if objects is not None and isinstance(objects, SoftDeleteManager):
146 return objects.include_deleted().all()
147 else:
148 return objects.all()