clinicedc 2.0.7__py3-none-any.whl → 2.0.8__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.

Potentially problematic release.


This version of clinicedc might be problematic. Click here for more details.

Files changed (140) hide show
  1. {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.dist-info}/METADATA +1 -1
  2. {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.dist-info}/RECORD +134 -137
  3. {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.dist-info}/WHEEL +1 -1
  4. edc_action_item/auths.py +37 -32
  5. edc_action_item/models/action_model_mixin.py +1 -2
  6. edc_action_item/models/signals.py +22 -23
  7. edc_action_item/site_action_items.py +5 -9
  8. edc_action_item/utils.py +3 -3
  9. edc_adverse_event/auths.py +55 -51
  10. edc_adverse_event/model_mixins/ae_tmg/ae_tmg_methods_model_mixin.py +2 -4
  11. edc_appointment/auths.py +14 -10
  12. edc_appointment/creators/appointment_creator.py +1 -1
  13. edc_appointment/creators/appointments_creator.py +1 -1
  14. edc_appointment/model_mixins/appointment_methods_model_mixin.py +2 -3
  15. edc_appointment/model_mixins/appointment_model_mixin.py +31 -28
  16. edc_appointment/models/appointment.py +1 -1
  17. edc_appointment/utils.py +19 -24
  18. edc_auth/auth_objects/__init__.py +2 -20
  19. edc_auth/auth_objects/default_groups.py +13 -11
  20. edc_auth/auth_objects/default_roles.py +26 -24
  21. edc_auth/auth_updater/auth_updater.py +13 -2
  22. edc_auth/auth_updater/group_updater.py +12 -10
  23. edc_auth/auth_updater/role_updater.py +2 -2
  24. edc_auth/constants.py +10 -0
  25. edc_auth/import_users.py +3 -3
  26. edc_auth/models/user_profile.py +14 -11
  27. edc_auth/site_auths.py +80 -67
  28. edc_consent/auths.py +18 -12
  29. edc_constants/constants.py +1 -0
  30. edc_crf/auths.py +5 -0
  31. edc_dashboard/auths.py +10 -6
  32. edc_dashboard/url_config.py +92 -83
  33. edc_dashboard/url_names.py +4 -4
  34. edc_dashboard/view_mixins/url_request_context_mixin.py +6 -5
  35. edc_data_manager/admin/data_query_admin.py +12 -11
  36. edc_data_manager/auths.py +37 -34
  37. edc_data_manager/rule/query_rule_wrapper.py +7 -7
  38. edc_export/archive_exporter.py +3 -2
  39. edc_export/auths.py +32 -28
  40. edc_export/model_exporter/model_exporter.py +4 -1
  41. edc_facility/auths.py +8 -3
  42. edc_facility/facility.py +8 -9
  43. edc_form_label/custom_label_condition.py +11 -8
  44. edc_form_label/form_label.py +1 -1
  45. edc_form_runners/auths.py +11 -6
  46. edc_form_validators/applicable_field_validator.py +7 -6
  47. edc_form_validators/base_form_validator.py +8 -9
  48. edc_form_validators/other_specify_field_validator.py +2 -8
  49. edc_form_validators/required_field_validator.py +19 -16
  50. edc_identifier/research_identifier.py +11 -10
  51. edc_identifier/simple_identifier.py +8 -2
  52. edc_lab/auths.py +26 -23
  53. edc_lab/lab/aliquot_creator.py +5 -8
  54. edc_lab/lab/primary_aliquot.py +14 -5
  55. edc_lab/model_mixins/requisition/requisition_model_mixin.py +6 -8
  56. edc_lab/models/aliquot.py +2 -2
  57. edc_lab/models/manifest/manifest.py +2 -2
  58. edc_lab/models/manifest/manifest_item.py +1 -1
  59. edc_lab_dashboard/auths.py +16 -11
  60. edc_lab_results/calculate_missing.py +8 -8
  61. edc_lab_results/form_validator_mixins/blood_results_form_validator_mixin.py +2 -2
  62. edc_lab_results/get_summary.py +26 -25
  63. edc_lab_results/model_mixins/blood_result_model_mixin.py +2 -0
  64. edc_label/auths.py +6 -1
  65. edc_label/label_template.py +8 -8
  66. edc_list_data/load_model_data.py +3 -3
  67. edc_list_data/post_migrate_signals.py +1 -1
  68. edc_list_data/preload_data.py +2 -2
  69. edc_list_data/row.py +1 -1
  70. edc_list_data/site_list_data.py +6 -5
  71. edc_locator/auths.py +18 -13
  72. edc_metadata/auths.py +11 -7
  73. edc_metadata/metadata/metadata.py +1 -1
  74. edc_metadata/metadata_rules/crf/crf_rule.py +1 -1
  75. edc_metadata/metadata_rules/metadata_rule_evaluator.py +5 -3
  76. edc_metadata/metadata_rules/rule.py +2 -3
  77. edc_metadata/metadata_rules/rule_evaluator.py +1 -1
  78. edc_metadata/model_mixins/updates/updates_crf_metadata_model_mixin.py +7 -4
  79. edc_metadata/model_mixins/updates/updates_requisition_metadata_model_mixin.py +5 -2
  80. edc_metadata/models/signals.py +10 -11
  81. edc_navbar/auths.py +18 -13
  82. edc_notification/auths.py +9 -4
  83. edc_notification/notification/graded_event_notification.py +2 -2
  84. edc_notification/notification/model_notification.py +3 -30
  85. edc_notification/notification/new_model_notification.py +1 -1
  86. edc_notification/notification/notification.py +1 -1
  87. edc_notification/notification/updated_model_notification.py +2 -2
  88. edc_offstudy/auths.py +12 -7
  89. edc_pdutils/df_exporters/csv_model_exporter.py +5 -2
  90. edc_pharmacy/auths.py +19 -15
  91. edc_pharmacy/models/medication/formulation.py +5 -7
  92. edc_pharmacy/prescribe/create_prescription.py +3 -3
  93. edc_pharmacy/utils/confirm_stock.py +1 -1
  94. edc_pharmacy/utils/confirm_stock_at_site.py +1 -1
  95. edc_pharmacy/views/confirmation_at_site_view.py +6 -9
  96. edc_prn/admin_site.py +5 -0
  97. edc_prn/prn.py +10 -11
  98. edc_prn/urls.py +11 -0
  99. edc_protocol_incident/action_items.py +4 -4
  100. edc_protocol_incident/auths.py +27 -20
  101. edc_pylabels/auths.py +6 -1
  102. edc_qareports/auths.py +11 -7
  103. edc_randomization/admin.py +30 -24
  104. edc_randomization/auths.py +12 -7
  105. edc_randomization/randomizer.py +22 -20
  106. edc_randomization/utils.py +17 -16
  107. edc_refusal/auths.py +7 -2
  108. edc_refusal/model_mixins.py +1 -1
  109. edc_registration/auths.py +28 -23
  110. edc_registration/model_mixins/updates_or_creates_registered_subject_model_mixin.py +13 -4
  111. edc_registration/models/registered_subject.py +1 -1
  112. edc_reportable/utils/get_reference_range_collection.py +2 -3
  113. edc_reportable/utils/load_data.py +1 -1
  114. edc_review_dashboard/auths.py +23 -18
  115. edc_screening/age_evaluator.py +3 -3
  116. edc_screening/auths.py +35 -30
  117. edc_screening/eligibility.py +1 -1
  118. edc_screening/gender_evaluator.py +1 -1
  119. edc_screening/model_mixins/eligibility_model_mixin.py +0 -2
  120. edc_screening/model_mixins/screening_methods_model_mixin.py +1 -1
  121. edc_screening/screening_eligibility.py +2 -4
  122. edc_screening/utils.py +9 -9
  123. edc_search/generate_slug.py +26 -0
  124. edc_search/model_mixins.py +10 -21
  125. edc_sites/auths.py +8 -3
  126. edc_subject_dashboard/auths.py +27 -22
  127. edc_timepoint/apps.py +0 -21
  128. edc_unblinding/auths.py +9 -4
  129. edc_utils/__init__.py +3 -1
  130. edc_utils/show_urls.py +29 -2
  131. edc_visit_schedule/auths.py +6 -1
  132. edc_visit_schedule/site_visit_schedules.py +2 -2
  133. edc_visit_tracking/models/signals.py +2 -2
  134. edc_form_label/models.py +0 -0
  135. edc_search/constants.py +0 -1
  136. edc_search/models.py +0 -0
  137. edc_search/search_slug.py +0 -51
  138. edc_search/updater.py +0 -30
  139. edc_search/wsgi.py +0 -7
  140. {clinicedc-2.0.7.dist-info → clinicedc-2.0.8.dist-info}/licenses/LICENSE +0 -0
@@ -1,21 +1,3 @@
1
- from ..constants import (
2
- ACCOUNT_MANAGER,
3
- ACCOUNT_MANAGER_ROLE,
4
- ADMINISTRATION,
5
- AUDITOR,
6
- AUDITOR_ROLE,
7
- CELERY_MANAGER,
8
- CLINIC,
9
- CLINIC_SUPER,
10
- CLINICIAN_ROLE,
11
- CLINICIAN_SUPER_ROLE,
12
- CUSTOM_ROLE,
13
- EVERYONE,
14
- NURSE_ROLE,
15
- PII,
16
- PII_VIEW,
17
- STAFF_ROLE,
18
- )
19
1
  from .codenames import (
20
2
  account_manager,
21
3
  administration,
@@ -25,6 +7,6 @@ from .codenames import (
25
7
  clinic_super,
26
8
  everyone,
27
9
  )
28
- from .default_groups import default_groups
10
+ from .default_groups import get_default_groups
29
11
  from .default_pii_models import default_pii_models
30
- from .default_roles import default_roles
12
+ from .default_roles import get_default_roles
@@ -11,14 +11,16 @@ from ..constants import (
11
11
  )
12
12
  from .codenames import account_manager, administration, celery_manager, everyone
13
13
 
14
- default_groups = {
15
- ACCOUNT_MANAGER: account_manager,
16
- ADMINISTRATION: administration,
17
- AUDITOR: [],
18
- CELERY_MANAGER: celery_manager,
19
- CLINIC: [],
20
- CLINIC_SUPER: [],
21
- EVERYONE: everyone,
22
- PII: [],
23
- PII_VIEW: [],
24
- }
14
+
15
+ def get_default_groups():
16
+ return {
17
+ ACCOUNT_MANAGER: account_manager,
18
+ ADMINISTRATION: administration,
19
+ AUDITOR: [],
20
+ CELERY_MANAGER: celery_manager,
21
+ CLINIC: [],
22
+ CLINIC_SUPER: [],
23
+ EVERYONE: everyone,
24
+ PII: [],
25
+ PII_VIEW: [],
26
+ }
@@ -17,27 +17,29 @@ from ..constants import (
17
17
  STATISTICIAN,
18
18
  )
19
19
 
20
- # Format {ROLE_NAME: [GROUP_NAME, GROUP_NAME, ...]
21
- default_roles: dict[str, list[str]] = {
22
- ACCOUNT_MANAGER_ROLE: [ACCOUNT_MANAGER],
23
- AUDITOR_ROLE: [
24
- AUDITOR,
25
- PII_VIEW,
26
- ],
27
- CLINICIAN_ROLE: [
28
- CLINIC,
29
- PII,
30
- ],
31
- CLINICIAN_SUPER_ROLE: [
32
- CLINIC_SUPER,
33
- CLINIC,
34
- PII,
35
- ],
36
- CUSTOM_ROLE: [],
37
- NURSE_ROLE: [
38
- CLINIC,
39
- PII,
40
- ],
41
- STAFF_ROLE: [ADMINISTRATION, EVERYONE],
42
- STATISTICIAN: [AUDITOR],
43
- }
20
+
21
+ def get_default_roles() -> dict[str, list[str]]:
22
+ """Format {ROLE_NAME: [GROUP_NAME, GROUP_NAME, ...]"""
23
+ return {
24
+ ACCOUNT_MANAGER_ROLE: [ACCOUNT_MANAGER],
25
+ AUDITOR_ROLE: [
26
+ AUDITOR,
27
+ PII_VIEW,
28
+ ],
29
+ CLINICIAN_ROLE: [
30
+ CLINIC,
31
+ PII,
32
+ ],
33
+ CLINICIAN_SUPER_ROLE: [
34
+ CLINIC_SUPER,
35
+ CLINIC,
36
+ PII,
37
+ ],
38
+ CUSTOM_ROLE: [],
39
+ NURSE_ROLE: [
40
+ CLINIC,
41
+ PII,
42
+ ],
43
+ STAFF_ROLE: [ADMINISTRATION, EVERYONE],
44
+ STATISTICIAN: [AUDITOR],
45
+ }
@@ -9,6 +9,7 @@ from django.contrib.auth import get_user_model
9
9
  from django.core.exceptions import ObjectDoesNotExist
10
10
  from django.core.management.color import color_style
11
11
 
12
+ from ..constants import POST_UPDATE_FUNCS_KEY, PRE_UPDATE_FUNCS_KEY
12
13
  from ..site_auths import site_auths
13
14
  from .group_updater import GroupUpdater
14
15
  from .role_updater import RoleUpdater
@@ -17,6 +18,14 @@ style = color_style()
17
18
 
18
19
 
19
20
  class AuthUpdater:
21
+ """A class to update auth.Group, edc_auth.Role, auth.Permissions
22
+ models using the site_auth registry.
23
+
24
+ Called once on application startup. For example::
25
+
26
+ AuthUpdater(verbose=False, warn_only=True)
27
+ """
28
+
20
29
  group_updater_cls = GroupUpdater
21
30
  role_updater_cls = RoleUpdater
22
31
 
@@ -38,8 +47,10 @@ class AuthUpdater:
38
47
  )
39
48
  groups = groups or site_auths.groups
40
49
  pii_models = pii_models or site_auths.pii_models
41
- post_update_funcs = post_update_funcs or site_auths.post_update_funcs
42
- pre_update_funcs = pre_update_funcs or site_auths.pre_update_funcs
50
+ post_update_funcs = post_update_funcs or [f for f in site_auths.post_update_funcs]
51
+ site_auths.registry[POST_UPDATE_FUNCS_KEY] = []
52
+ pre_update_funcs = pre_update_funcs or [f for f in site_auths.pre_update_funcs]
53
+ site_auths.registry[PRE_UPDATE_FUNCS_KEY] = []
43
54
  roles = roles or site_auths.roles
44
55
  self.apps = apps
45
56
  if not self.edc_auth_skip_auth_updater:
@@ -11,7 +11,7 @@ from django.core.exceptions import (
11
11
  )
12
12
  from django.core.management.color import color_style
13
13
 
14
- from ..auth_objects import PII, PII_VIEW
14
+ from ..constants import PII, PII_VIEW
15
15
  from ..utils import make_view_only_group_permissions
16
16
 
17
17
  style = color_style()
@@ -27,7 +27,7 @@ class PermissionsCreatorError(ValidationError):
27
27
  pass
28
28
 
29
29
 
30
- class CodenameDoesNotExist(Exception):
30
+ class CodenameDoesNotExist(Exception): # noqa: N818
31
31
  pass
32
32
 
33
33
 
@@ -87,7 +87,7 @@ class GroupUpdater:
87
87
  group = self.group_model_cls.objects.get(name=group_name)
88
88
  except ObjectDoesNotExist as e:
89
89
  if not create_group:
90
- raise ObjectDoesNotExist(f"{e} Got {group_name}")
90
+ raise ObjectDoesNotExist(f"{e} Got {group_name}") from e
91
91
  group = self.group_model_cls.objects.create(name=group_name)
92
92
  else:
93
93
  group.permissions.clear()
@@ -122,7 +122,7 @@ class GroupUpdater:
122
122
  try:
123
123
  app_label, codename = self.get_from_dotted_codename(dotted_codename)
124
124
  except PermissionsCodenameError as e:
125
- warn(str(e))
125
+ warn(str(e), stacklevel=2)
126
126
  else:
127
127
  # if you add extra codenames you must also add custom
128
128
  # codename tuples to the Permissions model before you
@@ -138,7 +138,7 @@ class GroupUpdater:
138
138
  except ObjectDoesNotExist as e:
139
139
  errmsg = f"{e} Got codename={codename},app_label={app_label}"
140
140
  if not self.warn_only:
141
- raise CodenameDoesNotExist(errmsg)
141
+ raise CodenameDoesNotExist(errmsg) from e
142
142
  warn(style.ERROR(errmsg))
143
143
  except MultipleObjectsReturned as e:
144
144
  if not allow_multiple_objects:
@@ -166,15 +166,17 @@ class GroupUpdater:
166
166
  try:
167
167
  app_label, _codename = codename.split(".")
168
168
  except ValueError as e:
169
- raise PermissionsCodenameError(f"Invalid dotted codename. {e} Got {codename}.")
169
+ raise PermissionsCodenameError(
170
+ f"Invalid dotted codename. {e} Got {codename}."
171
+ ) from e
170
172
  else:
171
173
  try:
172
174
  self.apps.get_app_config(app_label)
173
- except LookupError:
175
+ except LookupError as e:
174
176
  raise PermissionsCodenameError(
175
177
  "Invalid app_label in codename. Expected format "
176
178
  f"'<app_label>.<some_codename>'. Got {codename}."
177
- )
179
+ ) from e
178
180
  prefix = _codename.split("_")[0]
179
181
  if prefix not in self.codename_prefixes:
180
182
  raise PermissionsCodenameError(
@@ -244,7 +246,7 @@ class GroupUpdater:
244
246
  except ObjectDoesNotExist as e:
245
247
  raise CodenameDoesNotExist(
246
248
  f"Unable to verify codename. {e} Got '{app_label}.{codename}'"
247
- )
249
+ ) from e
248
250
  except MultipleObjectsReturned as e:
249
251
  self.delete_and_raise_on_duplicate_codenames(codename, app_label, exception=e)
250
252
  return permission
@@ -267,7 +269,7 @@ class GroupUpdater:
267
269
  try:
268
270
  value, name = codename_tpl
269
271
  except ValueError as e:
270
- raise ValueError(f"{e} Got {codename_tpl}")
272
+ raise ValueError(f"{e} Got {codename_tpl}") from e
271
273
  _app_label, codename = value.split(".")
272
274
  if app_label and _app_label != app_label:
273
275
  raise PermissionsCreatorError(
@@ -56,11 +56,11 @@ class RoleUpdater:
56
56
  for group_name in groups:
57
57
  try:
58
58
  group = self.group_model_cls.objects.get(name=group_name)
59
- except ObjectDoesNotExist:
59
+ except ObjectDoesNotExist as e:
60
60
  raise RoleUpdaterError(
61
61
  "Invalid group specified for role. "
62
62
  f"`{group_name}` is not a group. See role `{role}`."
63
- )
63
+ ) from e
64
64
  # group = self.group_model_cls.objects.create(name=group_name)
65
65
  role.groups.add(group)
66
66
  index += 1
edc_auth/constants.py CHANGED
@@ -19,3 +19,13 @@ CUSTOM_ROLE = "custom"
19
19
  NURSE_ROLE = "research_nurse"
20
20
  STAFF_ROLE = "staff"
21
21
  STATISTICIAN = "statistician"
22
+
23
+ # keys for the site_auths registry
24
+ UPDATE_ROLES_KEY = "update_roles"
25
+ UPDATE_GROUPS_KEY = "update_groups"
26
+ PRE_UPDATE_FUNCS_KEY = "pre_update_funcs"
27
+ POST_UPDATE_FUNCS_KEY = "post_update_funcs"
28
+ GROUPS_KEY = "groups"
29
+ ROLES_KEY = "roles"
30
+ PII_MODELS_KEY = "pii_models"
31
+ CUSTOM_PERMISSIONS_TUPLES_KEY = "custom_permissions_tuples"
edc_auth/import_users.py CHANGED
@@ -183,9 +183,9 @@ class UserImporter:
183
183
  self.update_user_sites(site_names or [])
184
184
  self.update_user_roles(role_names or [STAFF_ROLE])
185
185
  self.user.save()
186
- self.user.userprofile.job_title = self.job_title
187
- self.user.userprofile.mobile = self.mobile
188
- self.user.userprofile.alternate_email = self.alternate_email
186
+ self.user.userprofile.job_title = self.job_title or ""
187
+ self.user.userprofile.mobile = self.mobile or ""
188
+ self.user.userprofile.alternate_email = self.alternate_email or ""
189
189
  self.user.userprofile.save()
190
190
 
191
191
  self.site_names = (
@@ -11,7 +11,7 @@ from edc_export.choices import EXPORT_FORMATS
11
11
  from edc_export.constants import CSV
12
12
  from edc_notification.model_mixins import NotificationUserProfileModelMixin
13
13
 
14
- from ..auth_objects import CUSTOM_ROLE, STAFF_ROLE
14
+ from ..constants import CUSTOM_ROLE, STAFF_ROLE
15
15
  from .role import Role
16
16
 
17
17
 
@@ -32,21 +32,21 @@ class UserProfile(NotificationUserProfileModelMixin, models.Model):
32
32
 
33
33
  sites = models.ManyToManyField(Site, blank=True)
34
34
 
35
- job_title = models.CharField(max_length=100, null=True, blank=True)
35
+ job_title = models.CharField(max_length=100, default="", blank=True)
36
36
 
37
- alternate_email = models.EmailField(_("Alternate email address"), blank=True, null=True)
37
+ alternate_email = models.EmailField(_("Alternate email address"), blank=True, default="")
38
38
 
39
39
  mobile = models.CharField(
40
40
  max_length=25,
41
41
  validators=[RegexValidator(regex=r"^\+\d+")],
42
- null=True,
42
+ default="",
43
43
  blank=True,
44
44
  help_text="e.g. +1234567890",
45
45
  )
46
46
 
47
47
  clinic_label_printer = models.CharField(
48
48
  max_length=100,
49
- null=True,
49
+ default="",
50
50
  blank=True,
51
51
  help_text=format_html(
52
52
  'Change in <a href="{href}">{label}</a>',
@@ -57,7 +57,7 @@ class UserProfile(NotificationUserProfileModelMixin, models.Model):
57
57
 
58
58
  lab_label_printer = models.CharField(
59
59
  max_length=100,
60
- null=True,
60
+ default="",
61
61
  blank=True,
62
62
  help_text=format_html(
63
63
  'Change in <a href="{href}">{label}</a>',
@@ -68,7 +68,7 @@ class UserProfile(NotificationUserProfileModelMixin, models.Model):
68
68
 
69
69
  print_server = models.CharField(
70
70
  max_length=100,
71
- null=True,
71
+ default="",
72
72
  blank=True,
73
73
  help_text=format_html(
74
74
  'Change in <a href="{href}">{label}</a>',
@@ -82,7 +82,6 @@ class UserProfile(NotificationUserProfileModelMixin, models.Model):
82
82
  max_length=25,
83
83
  choices=EXPORT_FORMATS,
84
84
  default=CSV,
85
- null=True,
86
85
  blank=True,
87
86
  help_text="Note: requires export permissions",
88
87
  )
@@ -101,9 +100,13 @@ class UserProfile(NotificationUserProfileModelMixin, models.Model):
101
100
  group_names = [group.name for group in self.user.groups.all()]
102
101
  add_group_names = []
103
102
  for role in self.roles.all():
104
- for group in role.groups.all():
105
- if group.name not in group_names:
106
- add_group_names.append(group.name)
103
+ add_group_names.extend(
104
+ [
105
+ group.name
106
+ for group in role.groups.all()
107
+ if group.name not in group_names
108
+ ]
109
+ )
107
110
  add_group_names = list(set(add_group_names))
108
111
  for name in add_group_names:
109
112
  self.user.groups.add(Group.objects.get(name=name))
edc_auth/site_auths.py CHANGED
@@ -9,30 +9,40 @@ from django.apps import apps as django_apps
9
9
  from django.conf import settings
10
10
  from django.utils.module_loading import import_module, module_has_submodule
11
11
 
12
- from .auth_objects import default_groups, default_pii_models, default_roles
13
-
14
-
15
- class AlreadyRegistered(Exception):
12
+ from .auth_objects import default_pii_models, get_default_groups, get_default_roles
13
+ from .constants import (
14
+ CUSTOM_PERMISSIONS_TUPLES_KEY,
15
+ GROUPS_KEY,
16
+ PII_MODELS_KEY,
17
+ POST_UPDATE_FUNCS_KEY,
18
+ PRE_UPDATE_FUNCS_KEY,
19
+ ROLES_KEY,
20
+ UPDATE_GROUPS_KEY,
21
+ UPDATE_ROLES_KEY,
22
+ )
23
+
24
+
25
+ class AlreadyRegistered(Exception): # noqa: N818
16
26
  pass
17
27
 
18
28
 
19
- class InvalidGroup(Exception):
29
+ class InvalidGroup(Exception): # noqa: N818
20
30
  pass
21
31
 
22
32
 
23
- class InvalidRole(Exception):
33
+ class InvalidRole(Exception): # noqa: N818
24
34
  pass
25
35
 
26
36
 
27
- class RoleAlreadyExists(Exception):
37
+ class RoleAlreadyExists(Exception): # noqa: N818
28
38
  pass
29
39
 
30
40
 
31
- class GroupAlreadyExists(Exception):
41
+ class GroupAlreadyExists(Exception): # noqa: N818
32
42
  pass
33
43
 
34
44
 
35
- class PiiModelAlreadyExists(Exception):
45
+ class PiiModelAlreadyExists(Exception): # noqa: N818
36
46
  pass
37
47
 
38
48
 
@@ -96,39 +106,39 @@ class SiteAuths:
96
106
 
97
107
  def initialize(self):
98
108
  self.registry = {
99
- "groups": default_groups,
100
- "roles": default_roles,
101
- "update_groups": {},
102
- "update_roles": {},
103
- "custom_permissions_tuples": {},
104
- "pre_update_funcs": [],
105
- "post_update_funcs": [],
106
- "pii_models": default_pii_models,
109
+ GROUPS_KEY: get_default_groups(),
110
+ ROLES_KEY: get_default_roles(),
111
+ UPDATE_GROUPS_KEY: {},
112
+ UPDATE_ROLES_KEY: {},
113
+ CUSTOM_PERMISSIONS_TUPLES_KEY: {},
114
+ PRE_UPDATE_FUNCS_KEY: [],
115
+ POST_UPDATE_FUNCS_KEY: [],
116
+ PII_MODELS_KEY: default_pii_models,
107
117
  }
108
118
 
109
119
  def clear(self):
110
120
  self.registry = {
111
- "groups": {},
112
- "roles": {},
113
- "update_groups": {},
114
- "update_roles": {},
115
- "custom_permissions_tuples": {},
116
- "pre_update_funcs": [],
117
- "post_update_funcs": [],
118
- "pii_models": [],
121
+ GROUPS_KEY: {},
122
+ ROLES_KEY: {},
123
+ UPDATE_GROUPS_KEY: {},
124
+ UPDATE_ROLES_KEY: {},
125
+ CUSTOM_PERMISSIONS_TUPLES_KEY: {},
126
+ PRE_UPDATE_FUNCS_KEY: [],
127
+ POST_UPDATE_FUNCS_KEY: [],
128
+ PII_MODELS_KEY: [],
119
129
  }
120
130
 
121
131
  def clear_values(self):
122
132
  registry = deepcopy(self.registry)
123
133
  self.registry = {
124
- "groups": {k: [] for k in registry.get("groups")},
125
- "roles": {k: [] for k in self.registry.get("roles")},
126
- "update_groups": {},
127
- "update_roles": {},
128
- "custom_permissions_tuples": {},
129
- "pre_update_funcs": [],
130
- "post_update_funcs": [],
131
- "pii_models": [],
134
+ GROUPS_KEY: {k: [] for k in registry.get(GROUPS_KEY)},
135
+ ROLES_KEY: {k: [] for k in self.registry.get(ROLES_KEY)},
136
+ UPDATE_GROUPS_KEY: {},
137
+ UPDATE_ROLES_KEY: {},
138
+ CUSTOM_PERMISSIONS_TUPLES_KEY: {},
139
+ PRE_UPDATE_FUNCS_KEY: [],
140
+ POST_UPDATE_FUNCS_KEY: [],
141
+ PII_MODELS_KEY: [],
132
142
  }
133
143
 
134
144
  @property
@@ -136,15 +146,15 @@ class SiteAuths:
136
146
  return getattr(settings, "EDC_AUTH_SKIP_SITE_AUTHS", False)
137
147
 
138
148
  def add_pre_update_func(self, func):
139
- self.registry["pre_update_funcs"].append(func)
149
+ self.registry[PRE_UPDATE_FUNCS_KEY].append(func)
140
150
 
141
151
  def add_post_update_func(self, app_label: str, func: Callable):
142
- self.registry["post_update_funcs"].append((app_label, func))
152
+ self.registry[POST_UPDATE_FUNCS_KEY].append((app_label, func))
143
153
 
144
154
  def add_pii_model(self, model_name):
145
- if model_name in self.registry["pii_models"]:
155
+ if model_name in self.registry[PII_MODELS_KEY]:
146
156
  raise PiiModelAlreadyExists(f"PII model already exists. Got {model_name}")
147
- self.registry["pii_models"].append(model_name)
157
+ self.registry[PII_MODELS_KEY].append(model_name)
148
158
 
149
159
  def add_groups(self, data: dict):
150
160
  for name, codenames in data.items():
@@ -162,7 +172,7 @@ class SiteAuths:
162
172
  convert_to_export=None,
163
173
  no_delete=None,
164
174
  ):
165
- if name in self.registry["groups"]:
175
+ if name in self.registry[GROUPS_KEY]:
166
176
  raise GroupAlreadyExists(f"Group name already exists. Got {name}.")
167
177
  if no_delete:
168
178
  codenames_or_func = self.remove_delete_codenames(codenames_or_func)
@@ -170,18 +180,18 @@ class SiteAuths:
170
180
  codenames_or_func = self.get_view_only_codenames(codenames_or_func)
171
181
  if convert_to_export:
172
182
  codenames_or_func = self.convert_to_export_codenames(codenames_or_func)
173
- self.registry["groups"].update({name: codenames_or_func})
183
+ self.registry[GROUPS_KEY].update({name: codenames_or_func})
174
184
 
175
185
  def add_role(self, *group_names, name=None):
176
- if name in self.registry["roles"]:
186
+ if name in self.registry[ROLES_KEY]:
177
187
  raise RoleAlreadyExists(f"Role name already exists. Got {name}.")
178
188
  group_names = list(set(group_names))
179
- self.registry["roles"].update({name: group_names})
189
+ self.registry[ROLES_KEY].update({name: group_names})
180
190
 
181
191
  def update_group(
182
192
  self, *codenames_or_func, name=None, key=None, view_only=None, no_delete=None
183
193
  ) -> None:
184
- key = key or "update_groups"
194
+ key = key or UPDATE_GROUPS_KEY
185
195
  if no_delete:
186
196
  codenames_or_func = self.remove_delete_codenames(codenames_or_func)
187
197
  if view_only:
@@ -191,30 +201,30 @@ class SiteAuths:
191
201
  try:
192
202
  existing_codenames = list(set(existing_codenames))
193
203
  except TypeError as e:
194
- raise TypeError(f"{e}. Got {name}")
204
+ raise TypeError(f"{e}. Got {name}") from e
195
205
  existing_codenames.extend(codenames_or_func)
196
206
  existing_codenames = list(set(existing_codenames))
197
207
  self.registry[key].update({name: existing_codenames})
198
208
 
199
209
  def update_role(self, *group_names, name=None, key=None) -> None:
200
- key = key or "update_roles"
210
+ key = key or UPDATE_ROLES_KEY
201
211
  group_names = list(set(group_names))
202
- existing_group_names = deepcopy(self.registry[key].get(name)) or []
203
- existing_group_names = list(set(existing_group_names))
212
+ existing_group_names = [
213
+ name for name in self.registry[key].get(name) or [] if name not in group_names
214
+ ]
204
215
  existing_group_names.extend(group_names)
205
- existing_group_names = list(set(existing_group_names))
206
216
  self.registry[key].update({name: existing_group_names})
207
217
 
208
218
  def add_custom_permissions_tuples(
209
219
  self, model: str, codename_tuples: tuple[tuple[str, str], ...]
210
220
  ):
211
221
  try:
212
- self.registry["custom_permissions_tuples"][model]
222
+ self.registry[CUSTOM_PERMISSIONS_TUPLES_KEY][model]
213
223
  except KeyError:
214
- self.registry["custom_permissions_tuples"].update({model: []})
224
+ self.registry[CUSTOM_PERMISSIONS_TUPLES_KEY].update({model: []})
215
225
  for codename_tuple in codename_tuples:
216
- if codename_tuple not in self.registry["custom_permissions_tuples"][model]:
217
- self.registry["custom_permissions_tuples"][model].append(codename_tuple)
226
+ if codename_tuple not in self.registry[CUSTOM_PERMISSIONS_TUPLES_KEY][model]:
227
+ self.registry[CUSTOM_PERMISSIONS_TUPLES_KEY][model].append(codename_tuple)
218
228
 
219
229
  @staticmethod
220
230
  def get_view_only_codenames(codenames):
@@ -225,7 +235,8 @@ class SiteAuths:
225
235
  Does not remove `edc_navbar`, 'nav_' or `edc_dashboard`
226
236
  codenames.
227
237
  """
228
- callables = [lambda: view_only_wrapper(c) for c in codenames if callable(c)]
238
+ # callables = [lambda: view_only_wrapper(c) for c in codenames if callable(c)]
239
+ callables = [lambda c=c: view_only_wrapper(c) for c in codenames if callable(c)]
229
240
  view_only_codenames = [
230
241
  codename
231
242
  for codename in codenames
@@ -263,27 +274,27 @@ class SiteAuths:
263
274
 
264
275
  @property
265
276
  def roles(self):
266
- return self.registry["roles"]
277
+ return self.registry[ROLES_KEY]
267
278
 
268
279
  @property
269
280
  def groups(self):
270
- return self.registry["groups"]
281
+ return self.registry[GROUPS_KEY]
271
282
 
272
283
  @property
273
284
  def pii_models(self):
274
- return self.registry["pii_models"]
285
+ return self.registry[PII_MODELS_KEY]
275
286
 
276
287
  @property
277
288
  def pre_update_funcs(self):
278
- return self.registry["pre_update_funcs"]
289
+ return self.registry[PRE_UPDATE_FUNCS_KEY]
279
290
 
280
291
  @property
281
292
  def post_update_funcs(self) -> tuple[str, Callable]:
282
- return self.registry["post_update_funcs"]
293
+ return self.registry[POST_UPDATE_FUNCS_KEY]
283
294
 
284
295
  @property
285
296
  def custom_permissions_tuples(self):
286
- return self.registry["custom_permissions_tuples"]
297
+ return self.registry[CUSTOM_PERMISSIONS_TUPLES_KEY]
287
298
 
288
299
  def verify_and_populate(
289
300
  self, app_name: str | None = None, warn_only: bool | None = None
@@ -294,28 +305,30 @@ class SiteAuths:
294
305
  * Updates data from `update_groups` -> `groups`
295
306
  * Updates data from `update_roles` -> `roles`
296
307
  """
297
- for name, codenames in self.registry["update_groups"].items():
298
- if name not in self.registry["groups"]:
308
+ for name, codenames in self.registry[UPDATE_GROUPS_KEY].items():
309
+ if name not in self.registry[GROUPS_KEY]:
299
310
  msg = (
300
311
  f"Cannot update group. Group name does not exist. See app={app_name}"
301
312
  f"update_groups['groups']={codenames}. Got {name}"
302
313
  )
303
314
  if warn_only:
304
- warn(msg)
315
+ warn(msg, stacklevel=2)
305
316
  else:
306
317
  raise InvalidGroup(msg)
307
- self.update_group(*codenames, name=name, key="groups")
308
- for name, group_names in self.registry["update_roles"].items():
309
- if name not in self.registry["roles"]:
318
+ self.update_group(*codenames, name=name, key=GROUPS_KEY)
319
+ self.registry[UPDATE_GROUPS_KEY] = {}
320
+ for name, group_names in self.registry[UPDATE_ROLES_KEY].items():
321
+ if name not in self.registry[ROLES_KEY]:
310
322
  msg = (
311
323
  f"Cannot update role. Role name does not exist. See app={app_name}. "
312
324
  f"update_roles['groups']={group_names}. Got {name}"
313
325
  )
314
326
  if warn_only:
315
- warn(msg)
327
+ warn(msg, stacklevel=2)
316
328
  else:
317
329
  raise InvalidRole(msg)
318
- self.update_role(*group_names, name=name, key="roles")
330
+ self.update_role(*group_names, name=name, key=ROLES_KEY)
331
+ self.registry[UPDATE_ROLES_KEY] = {}
319
332
 
320
333
  def autodiscover(self, module_name=None, verbose=True):
321
334
  """Autodiscovers in the auths.py file of any INSTALLED_APP."""
@@ -335,7 +348,7 @@ class SiteAuths:
335
348
  except ImportError as e:
336
349
  site_auths.registry = before_import_registry
337
350
  if module_has_submodule(mod, module_name):
338
- raise SiteAuthError(str(e))
351
+ raise SiteAuthError(str(e)) from e
339
352
  except ImportError:
340
353
  pass
341
354
  self.verify_and_populate(app_name=app_name)