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
edc_export/auths.py CHANGED
@@ -4,34 +4,38 @@ from edc_auth.utils import remove_default_model_permissions_from_edc_permissions
4
4
  from .auth_objects import export_codenames
5
5
  from .constants import DATA_EXPORTER_ROLE, EXPORT, EXPORT_PII
6
6
 
7
- site_auths.add_post_update_func(
8
- "edc_export",
9
- remove_default_model_permissions_from_edc_permissions,
10
- )
11
7
 
12
- site_auths.add_custom_permissions_tuples(
13
- model="edc_export.edcpermissions",
14
- codename_tuples=[
15
- (
16
- "edc_export.export_visitschedule",
17
- "Can export the visit schedule",
18
- ),
19
- (
20
- "edc_export.view_export_dashboard",
21
- "Can view export dashboard",
22
- ),
23
- (
24
- "edc_export.export_subjectschedulehistory",
25
- "Can export subject schedule history",
26
- ),
27
- (
28
- "edc_export.export_pii",
29
- "Can export PII",
30
- ),
31
- ],
32
- )
8
+ def update_site_auths():
9
+ site_auths.add_post_update_func(
10
+ "edc_export",
11
+ remove_default_model_permissions_from_edc_permissions,
12
+ )
33
13
 
14
+ site_auths.add_custom_permissions_tuples(
15
+ model="edc_export.edcpermissions",
16
+ codename_tuples=[
17
+ (
18
+ "edc_export.export_visitschedule",
19
+ "Can export the visit schedule",
20
+ ),
21
+ (
22
+ "edc_export.view_export_dashboard",
23
+ "Can view export dashboard",
24
+ ),
25
+ (
26
+ "edc_export.export_subjectschedulehistory",
27
+ "Can export subject schedule history",
28
+ ),
29
+ (
30
+ "edc_export.export_pii",
31
+ "Can export PII",
32
+ ),
33
+ ],
34
+ )
34
35
 
35
- site_auths.add_group(*export_codenames, name=EXPORT)
36
- site_auths.add_role(EXPORT, name=DATA_EXPORTER_ROLE)
37
- site_auths.add_group("edc_export.export_pii", name=EXPORT_PII)
36
+ site_auths.add_group(*export_codenames, name=EXPORT)
37
+ site_auths.add_role(EXPORT, name=DATA_EXPORTER_ROLE)
38
+ site_auths.add_group("edc_export.export_pii", name=EXPORT_PII)
39
+
40
+
41
+ update_site_auths()
@@ -102,7 +102,10 @@ class ModelExporter:
102
102
  if f in self.export_fields or f in self.audit_fields or f in self.required_fields:
103
103
  self._field_names.pop(self._field_names.index(f))
104
104
  self._field_names = (
105
- self.export_fields + self.required_fields + self.field_names + self.audit_fields
105
+ *self.export_fields,
106
+ *self.required_fields,
107
+ *self.field_names,
108
+ *self.audit_fields,
106
109
  )
107
110
 
108
111
  @property
edc_facility/auths.py CHANGED
@@ -2,6 +2,11 @@ from edc_auth.site_auths import site_auths
2
2
 
3
3
  from .auth_objects import EDC_FACILITY, EDC_FACILITY_SUPER, EDC_FACILITY_VIEW, codenames
4
4
 
5
- site_auths.add_group(*codenames, name=EDC_FACILITY_VIEW, view_only=True)
6
- site_auths.add_group(*codenames, name=EDC_FACILITY, no_delete=True)
7
- site_auths.add_group(*codenames, name=EDC_FACILITY_SUPER)
5
+
6
+ def update_site_auths():
7
+ site_auths.add_group(*codenames, name=EDC_FACILITY_VIEW, view_only=True)
8
+ site_auths.add_group(*codenames, name=EDC_FACILITY, no_delete=True)
9
+ site_auths.add_group(*codenames, name=EDC_FACILITY_SUPER)
10
+
11
+
12
+ update_site_auths()
edc_facility/facility.py CHANGED
@@ -7,8 +7,7 @@ from zoneinfo import ZoneInfo
7
7
 
8
8
  import arrow
9
9
  from arrow import Arrow
10
- from dateutil._common import weekday
11
- from dateutil.relativedelta import relativedelta
10
+ from dateutil.relativedelta import relativedelta, weekday
12
11
  from django.conf import settings
13
12
  from django.utils import timezone
14
13
 
@@ -36,19 +35,18 @@ class Facility:
36
35
  def __init__(
37
36
  self,
38
37
  name: str | None = None,
39
- days: list | None = None,
38
+ days: list[weekday] | None = None,
40
39
  slots: list[int] | None = None,
41
40
  best_effort_available_datetime: datetime | None = None,
42
41
  ):
43
- self.days = []
42
+ self.days = days
44
43
  self.name = name
45
44
  if not name:
46
45
  raise FacilityError(f"Name cannot be None. See {self!r}")
47
46
  self.best_effort_available_datetime = (
48
47
  True if best_effort_available_datetime is None else best_effort_available_datetime
49
48
  )
50
- for day in days:
51
- self.days.append(getattr(day, "weekday", weekday(day)))
49
+ self.weekdays = [d.weekday for d in self.days]
52
50
  self.slots = slots or [99999 for _ in self.days]
53
51
  self.config = dict(zip([str(d) for d in self.days], self.slots, strict=False))
54
52
  self.holidays = self.holiday_cls()
@@ -69,9 +67,9 @@ class Facility:
69
67
  slots_per_day = 0
70
68
  return slots_per_day
71
69
 
72
- @property
73
- def weekdays(self) -> list[int]:
74
- return [d.weekday for d in self.days]
70
+ # @property
71
+ # def weekdays(self) -> list[int]:
72
+ # return [d.weekday for d in self.days]
75
73
 
76
74
  @staticmethod
77
75
  def open_slot_on(arr) -> Arrow:
@@ -132,6 +130,7 @@ class Facility:
132
130
  reverse_delta=None,
133
131
  taken_datetimes=None,
134
132
  schedule_on_holidays=None,
133
+ **kwargs, # noqa: ARG002
135
134
  ):
136
135
  """Returns an arrow object for a datetime equal to or
137
136
  close to the suggested datetime.
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from typing import TYPE_CHECKING
4
5
 
5
6
  from django.apps import apps as django_apps
@@ -28,20 +29,24 @@ class CustomLabelCondition:
28
29
  """
29
30
  return
30
31
 
31
- def get_additional_options(self, request=None, obj=None, model=None):
32
+ def get_additional_options(self, request=None, obj=None, model=None): # noqa: ARG002
32
33
  return {}
33
34
 
34
35
  @property
35
36
  def appointment(self) -> Appointment | None:
36
37
  """Returns the appointment instance for this request or None."""
37
- return django_apps.get_model(self.appointment_model).objects.get(
38
- pk=self.request.GET.get("appointment")
39
- )
38
+ with contextlib.suppress(ObjectDoesNotExist):
39
+ return django_apps.get_model(self.appointment_model).objects.get(
40
+ pk=self.request.GET.get("appointment")
41
+ )
42
+ return None
40
43
 
41
44
  @property
42
45
  def previous_appointment(self) -> Appointment | None:
43
46
  """Returns the previous appointment for this request or None."""
44
- return self.appointment.previous_by_timepoint
47
+ with contextlib.suppress(ObjectDoesNotExist):
48
+ return self.appointment.previous_by_timepoint
49
+ return None
45
50
 
46
51
  @property
47
52
  def previous_visit(self):
@@ -73,10 +78,8 @@ class CustomLabelCondition:
73
78
  """
74
79
  previous_obj = None
75
80
  if self.previous_visit:
76
- try:
81
+ with contextlib.suppress(ObjectDoesNotExist):
77
82
  previous_obj = self.model.objects.get(
78
83
  **{f"{self.model.related_visit_model_attr()}": self.previous_visit}
79
84
  )
80
- except ObjectDoesNotExist:
81
- pass
82
85
  return previous_obj
@@ -41,5 +41,5 @@ class FormLabel:
41
41
  except KeyError as e:
42
42
  raise CustomFormLabelError(
43
43
  f"Custom label template has invalid keys. See {label}. Got {e}."
44
- )
44
+ ) from e
45
45
  return label
edc_form_runners/auths.py CHANGED
@@ -9,10 +9,15 @@ from .auth_objects import (
9
9
  codenames,
10
10
  )
11
11
 
12
- site_auths.add_group(*codenames, name=EDC_FORM_RUNNERS_VIEW, view_only=True)
13
- site_auths.add_group(*codenames, name=EDC_FORM_RUNNERS, no_delete=True)
14
- site_auths.add_group(*codenames, name=EDC_FORM_RUNNERS_SUPER)
15
12
 
16
- site_auths.update_role(EDC_FORM_RUNNERS_VIEW, name=AUDITOR_ROLE)
17
- site_auths.update_role(EDC_FORM_RUNNERS, name=CLINICIAN_ROLE)
18
- site_auths.update_role(EDC_FORM_RUNNERS_SUPER, name=DATA_MANAGER_ROLE)
13
+ def update_site_auths():
14
+ site_auths.add_group(*codenames, name=EDC_FORM_RUNNERS_VIEW, view_only=True)
15
+ site_auths.add_group(*codenames, name=EDC_FORM_RUNNERS, no_delete=True)
16
+ site_auths.add_group(*codenames, name=EDC_FORM_RUNNERS_SUPER)
17
+
18
+ site_auths.update_role(EDC_FORM_RUNNERS_VIEW, name=AUDITOR_ROLE)
19
+ site_auths.update_role(EDC_FORM_RUNNERS, name=CLINICIAN_ROLE)
20
+ site_auths.update_role(EDC_FORM_RUNNERS_SUPER, name=DATA_MANAGER_ROLE)
21
+
22
+
23
+ update_site_auths()
@@ -1,6 +1,6 @@
1
1
  from typing import Any
2
2
 
3
- from edc_constants.constants import NOT_APPLICABLE
3
+ from edc_constants.constants import NOT_APPLICABLE, NULL_STRING
4
4
 
5
5
  from .base_form_validator import (
6
6
  APPLICABLE_ERROR,
@@ -33,8 +33,8 @@ class ApplicableFieldValidator(BaseFormValidator):
33
33
  def applicable_if(
34
34
  self,
35
35
  *responses: Any,
36
- field: str = None,
37
- field_applicable: str = None,
36
+ field: str,
37
+ field_applicable: str,
38
38
  inverse: bool | None = None,
39
39
  is_instance_field: bool | None = None,
40
40
  msg: str | None = None,
@@ -95,8 +95,8 @@ class ApplicableFieldValidator(BaseFormValidator):
95
95
  def applicable(
96
96
  self,
97
97
  *responses: Any,
98
- field: str = None,
99
- field_applicable: str = None,
98
+ field: str,
99
+ field_applicable: str,
100
100
  inverse: bool | None = None,
101
101
  is_instance_field: bool | None = None,
102
102
  msg: str | None = None,
@@ -117,7 +117,8 @@ class ApplicableFieldValidator(BaseFormValidator):
117
117
  field_applicable_value = self.get(field_applicable)
118
118
 
119
119
  if field_value in responses and (
120
- field_applicable_value is None or field_applicable_value == not_applicable
120
+ field_applicable_value in (NULL_STRING, not_applicable)
121
+ or field_applicable_value is None
121
122
  ):
122
123
  self.raise_applicable(field_applicable, msg=msg, applicable_msg=applicable_msg)
123
124
  elif (
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import contextlib
3
4
  from copy import copy
4
5
  from typing import Any
5
6
 
6
7
  from django.core.exceptions import NON_FIELD_ERRORS
7
- from django.forms import ValidationError, forms
8
+ from django.forms import ValidationError
8
9
 
9
10
  APPLICABLE_ERROR = "applicable"
10
11
  INVALID_ERROR = "invalid"
@@ -14,7 +15,7 @@ REQUIRED_ERROR = "required"
14
15
  OUT_OF_RANGE_ERROR = "out_of_range"
15
16
 
16
17
 
17
- class InvalidModelFormFieldValidator(Exception):
18
+ class InvalidModelFormFieldValidator(Exception): # noqa: N818
18
19
  def __init__(self, message, code=None):
19
20
  message = f"Invalid field validator. Got '{message}'"
20
21
  super().__init__(message)
@@ -119,13 +120,13 @@ class BaseFormValidator:
119
120
  """
120
121
  try:
121
122
  self._clean()
122
- except forms.ValidationError as e:
123
+ except ValidationError as e:
123
124
  self.capture_error_message(e)
124
125
  self.capture_error_code(e)
125
- raise forms.ValidationError(e)
126
+ raise ValidationError(e) from e
126
127
  return self.cleaned_data
127
128
 
128
- def capture_error_message(self, e: forms.ValidationError) -> None:
129
+ def capture_error_message(self, e: ValidationError) -> None:
129
130
  try:
130
131
  self._errors.update(**e.error_dict)
131
132
  except AttributeError:
@@ -134,11 +135,9 @@ class BaseFormValidator:
134
135
  except AttributeError:
135
136
  self._errors.update({NON_FIELD_ERRORS: str(e)})
136
137
 
137
- def capture_error_code(self, e: forms.ValidationError) -> None:
138
- try:
138
+ def capture_error_code(self, e: ValidationError) -> None:
139
+ with contextlib.suppress(AttributeError):
139
140
  self._error_codes.append(e.code)
140
- except AttributeError:
141
- pass
142
141
 
143
142
  def get_inline_field_value(self, field=None, inline_set=None):
144
143
  """Returns the value of the first inline field that has a value."""
@@ -41,20 +41,14 @@ class OtherSpecifyFieldValidator(BaseFormValidator):
41
41
  cleaned_data.get(field), fk_stored_field_name, cleaned_data.get(field)
42
42
  )
43
43
 
44
- if (
45
- field_value is not None
46
- and field_value == other
47
- and not cleaned_data.get(other_specify_field)
48
- ):
44
+ if field_value and field_value == other and not cleaned_data.get(other_specify_field):
49
45
  ref = "" if not ref else f" ref: {ref}"
50
46
  message = {other_specify_field: required_msg or f"This field is required.{ref}"}
51
47
  self._errors.update(message)
52
48
  self._error_codes.append(REQUIRED_ERROR)
53
49
  raise ValidationError(message, code=REQUIRED_ERROR)
54
50
  if (
55
- field_value is not None
56
- and field_value != other
57
- and cleaned_data.get(other_specify_field)
51
+ field_value and field_value != other and cleaned_data.get(other_specify_field)
58
52
  ) or (field_value is None and cleaned_data.get(other_specify_field)):
59
53
  ref = "" if not ref else f" ref: {ref}"
60
54
  message = {
@@ -43,8 +43,8 @@ class RequiredFieldValidator(BaseFormValidator):
43
43
  def required_if(
44
44
  self,
45
45
  *responses: str | int | bool,
46
- field: str = None,
47
- field_required: str = None,
46
+ field: str,
47
+ field_required: str,
48
48
  required_msg: str | None = None,
49
49
  not_required_msg: str | None = None,
50
50
  optional_if_dwta: bool | None = None,
@@ -82,7 +82,7 @@ class RequiredFieldValidator(BaseFormValidator):
82
82
  self.get(field_required, inline_set=inline_set) is not None
83
83
  )
84
84
  else:
85
- field_required_has_value = self.get(field_required, inline_set=inline_set)
85
+ field_required_has_value = bool(self.get(field_required, inline_set=inline_set))
86
86
 
87
87
  if field in self.cleaned_data:
88
88
  if (DWTA in responses and optional_if_dwta and field_value == DWTA) or (
@@ -117,7 +117,7 @@ class RequiredFieldValidator(BaseFormValidator):
117
117
  def required_if_true(
118
118
  self,
119
119
  condition: bool,
120
- field_required: str = None,
120
+ field_required: str,
121
121
  required_msg: str | None = None,
122
122
  not_required_msg: str | None = None,
123
123
  inverse: bool | None = None,
@@ -145,7 +145,7 @@ class RequiredFieldValidator(BaseFormValidator):
145
145
  def not_required_if_true(
146
146
  self,
147
147
  condition: bool,
148
- field: str = None,
148
+ field: str,
149
149
  msg: str | None = None,
150
150
  is_instance_field: bool | None = None,
151
151
  ) -> bool:
@@ -170,8 +170,8 @@ class RequiredFieldValidator(BaseFormValidator):
170
170
 
171
171
  def required_if_not_none(
172
172
  self,
173
- field: str = None,
174
- field_required: str = None,
173
+ field: str,
174
+ field_required: str,
175
175
  required_msg: str | None = None,
176
176
  not_required_msg: str | None = None,
177
177
  optional_if_dwta: bool | None = None,
@@ -182,6 +182,9 @@ class RequiredFieldValidator(BaseFormValidator):
182
182
  """Raises an exception or returns False.
183
183
 
184
184
  If field is not none, field_required is "required".
185
+
186
+ Note: CharFields usually default to an empty string and not NULL.
187
+ For IntegerFields, zero is a value.
185
188
  """
186
189
  inverse = True if inverse is None else inverse
187
190
  if is_instance_field:
@@ -197,7 +200,7 @@ class RequiredFieldValidator(BaseFormValidator):
197
200
  if field_required_evaluate_as_int:
198
201
  field_required_has_value = self.cleaned_data.get(field_required) is not None
199
202
  else:
200
- field_required_has_value = self.cleaned_data.get(field_required)
203
+ field_required_has_value = bool(self.cleaned_data.get(field_required))
201
204
 
202
205
  if field_value is not None and not field_required_has_value:
203
206
  self.raise_required(field=field_required, msg=required_msg)
@@ -221,8 +224,8 @@ class RequiredFieldValidator(BaseFormValidator):
221
224
  def not_required_if(
222
225
  self,
223
226
  *responses: str | int | bool,
224
- field: str = None,
225
- field_required: str = None,
227
+ field: str,
228
+ field_required: str | None = None,
226
229
  field_not_required: str | None = None,
227
230
  required_msg: str | None = None,
228
231
  not_required_msg: str | None = None,
@@ -259,8 +262,8 @@ class RequiredFieldValidator(BaseFormValidator):
259
262
 
260
263
  def require_together(
261
264
  self,
262
- field: str = None,
263
- field_required: str = None,
265
+ field: str,
266
+ field_required: str,
264
267
  required_msg: str | None = None,
265
268
  is_instance_field: bool | None = None,
266
269
  ) -> bool:
@@ -280,15 +283,15 @@ class RequiredFieldValidator(BaseFormValidator):
280
283
  return False
281
284
 
282
285
  @staticmethod
283
- def _inspect_params(
284
- *responses: str | int | bool, field: str = None, field_required: str = None
285
- ) -> bool:
286
+ def _inspect_params(*responses: str | int | bool, field: str, field_required: str) -> bool:
286
287
  """Inspects params and raises if any are None"""
287
288
  if not field:
288
289
  errmsg = _("`field` cannot be `None`")
289
290
  raise InvalidModelFormFieldValidator(f"{errmsg}.")
290
291
  if not responses:
291
- errmsg = _(f"At least one valid response for field '{field}' must be provided.")
292
+ errmsg = _(
293
+ "At least one valid response for field '{field}' must be provided."
294
+ ).format(field=field)
292
295
  raise InvalidModelFormFieldValidator(errmsg)
293
296
  if not field_required:
294
297
  raise InvalidModelFormFieldValidator('"field_required" cannot be None.')
@@ -16,7 +16,7 @@ if TYPE_CHECKING:
16
16
  from .models import IdentifierModel
17
17
 
18
18
 
19
- class IdentifierMissingTemplateValue(Exception):
19
+ class IdentifierMissingTemplateValue(Exception): # noqa: N818
20
20
  pass
21
21
 
22
22
 
@@ -38,6 +38,7 @@ class ResearchIdentifier:
38
38
  identifier: str | None = None,
39
39
  ) -> None:
40
40
  self._identifier = None
41
+ self._sequence_number = None
41
42
  self.requesting_model = requesting_model
42
43
  if not self.requesting_model:
43
44
  raise IdentifierError("Invalid requesting_model. Got None")
@@ -121,10 +122,10 @@ class ResearchIdentifier:
121
122
  for key in keys:
122
123
  try:
123
124
  value = getattr(self, key)
124
- except AttributeError:
125
+ except AttributeError as e:
125
126
  raise IdentifierMissingTemplateValue(
126
127
  f"Required option not provided. Got '{key}'."
127
- )
128
+ ) from e
128
129
  else:
129
130
  if value:
130
131
  template_opts.update({key: value})
@@ -141,15 +142,15 @@ class ResearchIdentifier:
141
142
  @property
142
143
  def sequence_number(self) -> int:
143
144
  """Returns the next sequence number to use."""
144
- try:
145
- identifier_model = (
145
+ if self._sequence_number is None:
146
+ if identifier_model := (
146
147
  self.identifier_model_cls.objects.filter(
147
148
  name=self.label, device_id=self.device_id, site=self.site
148
149
  )
149
150
  .order_by("-sequence_number")
150
151
  .first()
151
- )
152
- sequence_number = identifier_model.sequence_number + 1
153
- except AttributeError:
154
- sequence_number = 1
155
- return sequence_number
152
+ ):
153
+ self._sequence_number = identifier_model.sequence_number + 1
154
+ else:
155
+ self._sequence_number = 1
156
+ return self._sequence_number
@@ -1,5 +1,7 @@
1
+ from __future__ import annotations
2
+
1
3
  from secrets import choice
2
- from typing import Any
4
+ from typing import TYPE_CHECKING, Any
3
5
 
4
6
  from django.apps import apps as django_apps
5
7
  from django.core.exceptions import ObjectDoesNotExist
@@ -8,6 +10,9 @@ from django.utils import timezone
8
10
 
9
11
  from .utils import convert_to_human_readable
10
12
 
13
+ if TYPE_CHECKING:
14
+ from edc_identifier.models import IdentifierModel
15
+
11
16
 
12
17
  class DuplicateIdentifierError(Exception):
13
18
  pass
@@ -195,7 +200,7 @@ class SimpleUniqueIdentifier:
195
200
  def model_cls(self) -> type[models.Model]:
196
201
  return django_apps.get_model(self.model)
197
202
 
198
- def update_identifier_model(self, **kwargs) -> bool:
203
+ def update_identifier_model(self, **kwargs) -> bool | IdentifierModel:
199
204
  """Attempts to update identifier_model and returns True (or instance)
200
205
  if successful else False if identifier already exists.
201
206
  """
@@ -210,5 +215,6 @@ class SimpleUniqueIdentifier:
210
215
  try:
211
216
  self.model_cls.objects.get(identifier=self.identifier)
212
217
  except ObjectDoesNotExist:
218
+ opts = {k: v for k, v in opts.items() if v is not None}
213
219
  return self.model_cls.objects.create(**opts)
214
220
  return False
edc_lab/auths.py CHANGED
@@ -9,6 +9,7 @@ from edc_auth.constants import (
9
9
  PII_VIEW,
10
10
  )
11
11
  from edc_auth.site_auths import site_auths
12
+ from edc_export.constants import EXPORT
12
13
 
13
14
  from .auth_objects import (
14
15
  LAB,
@@ -18,26 +19,28 @@ from .auth_objects import (
18
19
  lab_view_codenames,
19
20
  )
20
21
 
21
- site_auths.add_group(*lab_codenames, name=LAB)
22
- site_auths.add_group(*lab_view_codenames, name=LAB_VIEW)
23
-
24
- if django_apps.is_installed("edc_export"):
25
- from edc_export.constants import EXPORT
26
-
27
- site_auths.update_group(
28
- "edc_lab.export_aliquot",
29
- "edc_lab.export_box",
30
- "edc_lab.export_boxitem",
31
- "edc_lab.export_boxtype",
32
- "edc_lab.export_order",
33
- "edc_lab.export_panel",
34
- "edc_lab.export_result",
35
- "edc_lab.export_resultitem",
36
- name=EXPORT,
37
- )
38
-
39
-
40
- site_auths.add_role(ADMINISTRATION, EVERYONE, LAB, PII_VIEW, name=LAB_TECHNICIAN_ROLE)
41
- site_auths.update_role(LAB, name=CLINICIAN_ROLE)
42
- site_auths.update_role(LAB, name=NURSE_ROLE)
43
- site_auths.update_role(LAB_VIEW, name=AUDITOR_ROLE)
22
+
23
+ def update_site_auths():
24
+ site_auths.add_group(*lab_codenames, name=LAB)
25
+ site_auths.add_group(*lab_view_codenames, name=LAB_VIEW)
26
+
27
+ if django_apps.is_installed("edc_export"):
28
+ site_auths.update_group(
29
+ "edc_lab.export_aliquot",
30
+ "edc_lab.export_box",
31
+ "edc_lab.export_boxitem",
32
+ "edc_lab.export_boxtype",
33
+ "edc_lab.export_order",
34
+ "edc_lab.export_panel",
35
+ "edc_lab.export_result",
36
+ "edc_lab.export_resultitem",
37
+ name=EXPORT,
38
+ )
39
+
40
+ site_auths.add_role(ADMINISTRATION, EVERYONE, LAB, PII_VIEW, name=LAB_TECHNICIAN_ROLE)
41
+ site_auths.update_role(LAB, name=CLINICIAN_ROLE)
42
+ site_auths.update_role(LAB, name=NURSE_ROLE)
43
+ site_auths.update_role(LAB_VIEW, name=AUDITOR_ROLE)
44
+
45
+
46
+ update_site_auths()
@@ -90,18 +90,15 @@ class AliquotCreator:
90
90
  parent_segment=self.parent_segment,
91
91
  )
92
92
 
93
- parent_identifier = parent_identifier or aliquot_identifier_obj.identifier
94
-
95
- aliquot = self.aliquot_model_cls.objects.create(
93
+ return self.aliquot_model_cls.objects.create(
96
94
  aliquot_identifier=aliquot_identifier_obj.identifier,
97
95
  aliquot_type=aliquot_type.name,
98
96
  alpha_code=aliquot_type.alpha_code,
99
97
  count=count,
100
98
  numeric_code=aliquot_type.numeric_code,
101
- parent_identifier=parent_identifier,
99
+ parent_identifier=parent_identifier or aliquot_identifier_obj.identifier,
102
100
  identifier_prefix=self.identifier_prefix,
103
- is_primary=True if self.is_primary else False,
104
- requisition_identifier=self.requisition_identifier,
105
- subject_identifier=self.subject_identifier,
101
+ is_primary=bool(self.is_primary),
102
+ requisition_identifier=self.requisition_identifier or "",
103
+ subject_identifier=self.subject_identifier or "",
106
104
  )
107
- return aliquot
@@ -1,3 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ if TYPE_CHECKING:
6
+ from edc_lab import AliquotCreator, AliquotType
7
+
8
+
1
9
  class PrimaryAliquotError(Exception):
2
10
  pass
3
11
 
@@ -11,11 +19,12 @@ class PrimaryAliquot:
11
19
 
12
20
  def __init__(
13
21
  self,
14
- aliquot_type=None,
15
- subject_identifier=None,
16
- requisition_identifier=None,
17
- identifier_prefix=None,
18
- aliquot_creator_cls=None,
22
+ *,
23
+ aliquot_creator_cls: type[AliquotCreator],
24
+ aliquot_type: AliquotType | None = None,
25
+ subject_identifier: str | None = None,
26
+ requisition_identifier: str | None = None,
27
+ identifier_prefix: str | None = None,
19
28
  ):
20
29
  self._object = None
21
30
  self.aliquot_creator_cls = aliquot_creator_cls