clinicedc 2.0.23__py3-none-any.whl → 2.0.25__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 (48) hide show
  1. {clinicedc-2.0.23.dist-info → clinicedc-2.0.25.dist-info}/METADATA +1 -1
  2. {clinicedc-2.0.23.dist-info → clinicedc-2.0.25.dist-info}/RECORD +48 -48
  3. edc_action_item/site_action_items.py +2 -2
  4. edc_appointment/view_utils/appointment_button.py +1 -1
  5. edc_consent/modeladmin_mixins/consent_model_admin_mixin.py +14 -12
  6. edc_consent/models/__init__.py +2 -2
  7. edc_consent/models/signals.py +26 -28
  8. edc_dashboard/view_mixins/message_view_mixin.py +3 -4
  9. edc_identifier/admin.py +3 -2
  10. edc_identifier/identifier.py +5 -5
  11. edc_identifier/model_mixins.py +1 -1
  12. edc_identifier/models.py +7 -6
  13. edc_identifier/research_identifier.py +1 -1
  14. edc_identifier/short_identifier.py +1 -1
  15. edc_identifier/simple_identifier.py +15 -5
  16. edc_identifier/subject_identifier.py +2 -2
  17. edc_identifier/utils.py +13 -14
  18. edc_listboard/view_mixins/search_listboard_view_mixin.py +6 -7
  19. edc_metadata/view_mixins/metadata_view_mixin.py +7 -4
  20. edc_randomization/randomization_list_importer.py +25 -21
  21. edc_randomization/randomization_list_verifier.py +31 -26
  22. edc_randomization/randomizer.py +1 -1
  23. edc_randomization/site_randomizers.py +25 -8
  24. edc_review_dashboard/middleware.py +4 -4
  25. edc_review_dashboard/views/subject_review_listboard_view.py +3 -3
  26. edc_screening/age_evaluator.py +1 -1
  27. edc_screening/eligibility.py +5 -5
  28. edc_screening/exceptions.py +3 -3
  29. edc_screening/gender_evaluator.py +1 -1
  30. edc_screening/modelform_mixins.py +1 -1
  31. edc_screening/screening_eligibility.py +30 -35
  32. edc_screening/utils.py +10 -12
  33. edc_sites/admin/list_filters.py +2 -2
  34. edc_sites/admin/site_model_admin_mixin.py +8 -8
  35. edc_sites/exceptions.py +1 -1
  36. edc_sites/forms.py +3 -1
  37. edc_sites/management/commands/sync_sites.py +11 -17
  38. edc_sites/models/site_profile.py +6 -4
  39. edc_sites/post_migrate_signals.py +2 -2
  40. edc_sites/site.py +8 -8
  41. edc_sites/utils/add_or_update_django_sites.py +3 -3
  42. edc_sites/utils/valid_site_for_subject_or_raise.py +7 -4
  43. edc_sites/view_mixins.py +2 -2
  44. edc_subject_dashboard/view_utils/subject_screening_button.py +5 -3
  45. edc_view_utils/dashboard_model_button.py +3 -3
  46. edc_visit_schedule/site_visit_schedules.py +2 -1
  47. {clinicedc-2.0.23.dist-info → clinicedc-2.0.25.dist-info}/WHEEL +0 -0
  48. {clinicedc-2.0.23.dist-info → clinicedc-2.0.25.dist-info}/licenses/LICENSE +0 -0
@@ -8,6 +8,8 @@ from django.core.exceptions import ObjectDoesNotExist
8
8
  from django.db import models
9
9
  from django.utils import timezone
10
10
 
11
+ from edc_constants.constants import NULL_STRING
12
+
11
13
  from .utils import convert_to_human_readable
12
14
 
13
15
  if TYPE_CHECKING:
@@ -22,10 +24,13 @@ class IdentifierError(Exception):
22
24
  pass
23
25
 
24
26
 
27
+ IDENTIFER_PREFIX_LENGTH = 2
28
+
29
+
25
30
  class SimpleIdentifier:
26
31
  random_string_length: int = 5
27
32
  template: str = "{device_id}{random_string}"
28
- identifier_prefix: str = None
33
+ identifier_prefix: str = NULL_STRING
29
34
 
30
35
  def __init__(
31
36
  self,
@@ -90,7 +95,7 @@ class SimpleSequentialIdentifier:
90
95
  random_number: int = choice(range(1000, 9999)) # nosec B311
91
96
  sequence: str = f"{sequence}{random_number}"
92
97
  chk: int = int(sequence) % 11
93
- self.identifier: str = f"{self.prefix or ''}{sequence}{chk}"
98
+ self.identifier: str = f"{self.prefix or NULL_STRING}{sequence}{chk}"
94
99
 
95
100
  def __str__(self) -> str:
96
101
  return self.identifier
@@ -111,12 +116,13 @@ class SimpleUniqueIdentifier:
111
116
  model: str = "edc_identifier.identifiermodel"
112
117
  template: str = "{device_id}{random_string}"
113
118
  identifier_prefix: str | None = None
119
+ identifier_prefix_length: int = 2
114
120
  identifier_cls = SimpleIdentifier
115
121
  make_human_readable: bool | None = None
116
122
 
117
123
  def __init__(
118
124
  self,
119
- model: str = None,
125
+ model: str | None = None,
120
126
  identifier_attr: str | None = None,
121
127
  identifier_type: str | None = None,
122
128
  identifier_prefix: str | None = None,
@@ -140,9 +146,13 @@ class SimpleUniqueIdentifier:
140
146
  self.identifier_attr = identifier_attr or self.identifier_attr
141
147
  self.identifier_type = identifier_type or self.identifier_type
142
148
  self.identifier_prefix = identifier_prefix or self.identifier_prefix
143
- if self.identifier_prefix and len(self.identifier_prefix) != 2:
149
+ if (
150
+ self.identifier_prefix
151
+ and len(self.identifier_prefix) != self.identifier_prefix_length
152
+ ):
144
153
  raise IdentifierError(
145
- f"Expected identifier_prefix of length=2. Got {len(identifier_prefix)}"
154
+ f"Expected identifier_prefix of length={self.identifier_prefix_length}. "
155
+ f"Got {len(identifier_prefix)}"
146
156
  )
147
157
  self.make_human_readable = make_human_readable or self.make_human_readable
148
158
  self.device_id = django_apps.get_app_config("edc_device").device_id
@@ -10,8 +10,8 @@ class SubjectIdentifier(ResearchIdentifier):
10
10
  label: str = "subjectidentifier"
11
11
  padding: int = 4
12
12
 
13
- def __init__(self, last_name: str = None, **kwargs):
14
- self.last_name = last_name
13
+ def __init__(self, last_name: str | None = None, **kwargs):
14
+ self.last_name = last_name or ""
15
15
  super().__init__(**kwargs)
16
16
 
17
17
  def pre_identifier(self) -> None:
edc_identifier/utils.py CHANGED
@@ -14,23 +14,22 @@ def is_subject_identifier_or_raise(subject_identifier, reference_obj=None, raise
14
14
  * If `subject_identifier` is None, does nothing, unless
15
15
  `raise_on_none` is `True`.
16
16
  """
17
- if subject_identifier or raise_on_none:
18
- if not re.match(
19
- ResearchProtocolConfig().subject_identifier_pattern,
20
- subject_identifier or "",
21
- ):
22
- reference_msg = ""
23
- if reference_obj:
24
- reference_msg = f"See {reference_obj!r}. "
25
- raise SubjectIdentifierError(
26
- f"Invalid format for subject identifier. {reference_msg}"
27
- f"Got `{subject_identifier or ''}`. "
28
- f"Expected pattern `{ResearchProtocolConfig().subject_identifier_pattern}`"
29
- )
17
+ valid_subject_identifier = subject_identifier and re.match(
18
+ ResearchProtocolConfig().subject_identifier_pattern, subject_identifier or ""
19
+ )
20
+ if not valid_subject_identifier or (not subject_identifier and raise_on_none):
21
+ reference_msg = ""
22
+ if reference_obj:
23
+ reference_msg = f"See {reference_obj!r}. "
24
+ raise SubjectIdentifierError(
25
+ f"Invalid format for subject identifier. {reference_msg}"
26
+ f"Got `{subject_identifier or ''}`. "
27
+ f"Expected pattern `{ResearchProtocolConfig().subject_identifier_pattern}`"
28
+ )
30
29
  return subject_identifier
31
30
 
32
31
 
33
- def get_human_phrase(no_hyphen: bool = None) -> str:
32
+ def get_human_phrase(no_hyphen: bool | None = None) -> str:
34
33
  """Returns 6 digits split by a '-', e.g. DEC-96E.
35
34
 
36
35
  There are 213,127,200 permutations from an unambiguous alphabet.
@@ -13,12 +13,12 @@ from edc_model_admin.utils import add_to_messages_once
13
13
 
14
14
 
15
15
  class SearchListboardMixin:
16
- search_fields = ["slug"]
16
+ search_fields = ("slug",)
17
17
 
18
18
  default_querystring_attrs: str = "q"
19
19
  alternate_search_attr: str = "subject_identifier"
20
20
  default_lookup = "icontains"
21
- operators: list[str] = [
21
+ operators: tuple[str, ...] = (
22
22
  "exact",
23
23
  "iexact",
24
24
  "contains",
@@ -33,7 +33,7 @@ class SearchListboardMixin:
33
33
  "endswith",
34
34
  "istartswith",
35
35
  "iendswith",
36
- ]
36
+ )
37
37
 
38
38
  def __init__(self, **kwargs):
39
39
  self._search_term = None
@@ -66,12 +66,11 @@ class SearchListboardMixin:
66
66
 
67
67
  @property
68
68
  def search_term(self) -> str | None:
69
- if not self._search_term:
70
- if search_term := self.raw_search_term:
71
- self._search_term = escape(search_term).strip()
69
+ if (not self._search_term) and (search_term := self.raw_search_term):
70
+ self._search_term = escape(search_term).strip()
72
71
  return self._search_term
73
72
 
74
- def get_search_fields(self) -> list[str]:
73
+ def get_search_fields(self) -> tuple[str, ...]:
75
74
  """Override to add additional search fields"""
76
75
  return self.search_fields
77
76
 
@@ -16,16 +16,19 @@ class MetadataViewError(Exception):
16
16
 
17
17
  class MetadataViewMixin:
18
18
  panel_model: str = "edc_lab.panel"
19
- metadata_show_status: list[str] = [REQUIRED, KEYED]
19
+ metadata_show_status: tuple[str] = (REQUIRED, KEYED)
20
20
 
21
21
  def get_context_data(self, **kwargs) -> dict:
22
22
  if self.appointment:
23
23
  # always refresh metadata / run rules
24
24
  refresh_metadata_for_timepoint(self.appointment, allow_create=True)
25
25
  referer = self.request.headers.get("Referer")
26
- if referer and "subject_review_listboard" in referer:
27
- if self.appointment.related_visit:
28
- update_appt_status_for_timepoint(self.appointment.related_visit)
26
+ if (
27
+ referer
28
+ and "subject_review_listboard" in referer
29
+ and self.appointment.related_visit
30
+ ):
31
+ update_appt_status_for_timepoint(self.appointment.related_visit)
29
32
  crf_qs = self.get_crf_metadata()
30
33
  requisition_qs = self.get_requisition_metadata()
31
34
  kwargs.update(crfs=crf_qs, requisitions=requisition_qs)
@@ -55,31 +55,32 @@ class RandomizationListImporter:
55
55
 
56
56
 
57
57
  Format:
58
- sid,assignment,site_name, orig_site, orig_allocation, orig_desc
59
- 1,single_dose,gaborone
60
- 2,two_doses,gaborone
58
+ sid,site_name, assignment, description, orig_site, orig_allocation, orig_desc
59
+ 1,gaborone,intervention,single_dose
60
+ 2,gaborone,control,two_doses
61
61
  ...
62
62
  """
63
63
 
64
- required_csv_fieldnames = ["sid", "assignment", "site_name"]
64
+ required_csv_fieldnames = ("sid", "assignment", "site_name", "description")
65
65
  verifier_cls = RandomizationListVerifier
66
66
 
67
67
  def __init__(
68
68
  self,
69
69
  randomizer_model_cls=None,
70
- randomizer_name: str = None,
71
- randomizationlist_path: Path | str = None,
72
- assignment_map: dict[str, int] = None,
73
- verbose: bool = None,
74
- overwrite: bool = None,
75
- add: bool = None,
76
- dryrun: bool = None,
77
- username: str = None,
78
- revision: str = None,
79
- sid_count_for_tests: int = None,
80
- extra_csv_fieldnames: list[str] | None = None,
81
- **kwargs,
70
+ randomizer_name: str | None = None,
71
+ randomizationlist_path: Path | str | None = None,
72
+ assignment_map: dict[str, int] | None = None,
73
+ verbose: bool | None = None,
74
+ overwrite: bool | None = None,
75
+ add: bool | None = None,
76
+ dryrun: bool | None = None,
77
+ username: str | None = None,
78
+ revision: str | None = None,
79
+ sid_count_for_tests: int | None = None,
80
+ extra_csv_fieldnames: tuple[str] | None = None,
81
+ **kwargs, # noqa: ARG002
82
82
  ):
83
+ extra_csv_fieldnames = extra_csv_fieldnames or ()
83
84
  self.verify_messages: str | None = None
84
85
  self.add = add
85
86
  self.overwrite = overwrite
@@ -92,7 +93,7 @@ class RandomizationListImporter:
92
93
  self.randomizer_name = randomizer_name
93
94
  self.assignment_map = assignment_map
94
95
  self.randomizationlist_path: Path = Path(randomizationlist_path).expanduser()
95
- self.required_csv_fieldnames.extend(extra_csv_fieldnames or [])
96
+ self.required_csv_fieldnames = (*self.required_csv_fieldnames, *extra_csv_fieldnames)
96
97
 
97
98
  if self.dryrun:
98
99
  sys.stdout.write(
@@ -223,12 +224,15 @@ class RandomizationListImporter:
223
224
  sid_count = self.sid_count_for_tests
224
225
  else:
225
226
  sid_count = len(self.get_sid_list())
226
- with self.randomizationlist_path.open(mode="r") as csvfile:
227
- reader = csv.DictReader(csvfile)
228
- for row in tqdm(reader, total=sid_count):
227
+ with self.randomizationlist_path.open(mode="r") as f:
228
+ reader = csv.DictReader(f)
229
+ all_rows = [{k: v.strip() for k, v in row.items() if k} for row in reader]
230
+ sorted_rows = sorted(
231
+ all_rows, key=lambda row: (row.get("site_name", ""), row.get("sid", ""))
232
+ )
233
+ for row in tqdm(sorted_rows, total=sid_count):
229
234
  if self.sid_count_for_tests and len(objs) == self.sid_count_for_tests:
230
235
  break
231
- row = {k: v.strip() for k, v in row.items()}
232
236
  try:
233
237
  self.randomizer_model_cls.objects.get(sid=row["sid"])
234
238
  except ObjectDoesNotExist:
@@ -25,13 +25,13 @@ class RandomizationListVerifier:
25
25
  def __init__(
26
26
  self,
27
27
  randomizer_name=None,
28
- randomizationlist_path: Path | str = None,
28
+ randomizationlist_path: Path | str | None = None,
29
29
  randomizer_model_cls=None,
30
30
  assignment_map=None,
31
31
  fieldnames=None,
32
32
  sid_count_for_tests=None,
33
- required_csv_fieldnames: list[str] | None = None,
34
- **kwargs,
33
+ required_csv_fieldnames: tuple[str, ...] | None = None,
34
+ **kwargs, # noqa: ARG002
35
35
  ):
36
36
  self.count: int = 0
37
37
  self.messages: list[str] = []
@@ -68,33 +68,38 @@ class RandomizationListVerifier:
68
68
  )
69
69
  elif message := self.verify():
70
70
  self.messages.append(message)
71
- if self.messages:
72
- if (
73
- "migrate" not in sys.argv
74
- and "makemigrations" not in sys.argv
75
- and "import_randomization_list" not in sys.argv
76
- ):
77
- raise RandomizationListError(", ".join(self.messages))
71
+ if self.messages and (
72
+ "migrate" not in sys.argv
73
+ and "makemigrations" not in sys.argv
74
+ and "import_randomization_list" not in sys.argv
75
+ ):
76
+ raise RandomizationListError(", ".join(self.messages))
78
77
 
79
78
  def verify(self) -> str | None:
80
79
  message = None
80
+
81
81
  with self.randomizationlist_path.open(mode="r") as f:
82
82
  reader = csv.DictReader(f)
83
- for index, row in enumerate(reader, start=1):
84
- row = {k: v.strip() for k, v in row.items() if k}
85
- message = self.inspect_row(index - 1, row)
86
- if message:
87
- break
88
- if self.sid_count_for_tests and index == self.sid_count_for_tests:
89
- break
90
- if not message:
91
- if self.count != index:
92
- message = (
93
- f"Randomization list count is off. Expected {index} (CSV). "
94
- f"Got {self.count} (model_cls). See file "
95
- f"{self.randomizationlist_path}. "
96
- f"Resolve this issue before using the system."
97
- )
83
+ all_rows = [{k: v.strip() for k, v in row.items() if k} for row in reader]
84
+ sorted_rows = sorted(
85
+ all_rows, key=lambda row: (row.get("site_name", ""), row.get("sid", ""))
86
+ )
87
+
88
+ for index, row in enumerate(sorted_rows, start=1):
89
+ sys.stdout.write(f"Index: {index}, SID: {row.get('sid')}, Row: {row}\n")
90
+ message = self.inspect_row(index - 1, row)
91
+ if message:
92
+ break
93
+ if self.sid_count_for_tests and index == self.sid_count_for_tests:
94
+ break
95
+
96
+ if not message and self.count != index:
97
+ message = (
98
+ f"Randomization list count is off. Expected {index} (CSV). "
99
+ f"Got {self.count} (model_cls). See file "
100
+ f"{self.randomizationlist_path}. "
101
+ f"Resolve this issue before using the system."
102
+ )
98
103
  return message
99
104
 
100
105
  def inspect_row(self, index: int, row) -> str | None:
@@ -103,7 +108,7 @@ class RandomizationListVerifier:
103
108
  Note:Index is zero-based
104
109
  """
105
110
  message = None
106
- obj1 = self.randomizer_model_cls.objects.all().order_by("sid")[index]
111
+ obj1 = self.randomizer_model_cls.objects.all().order_by("site_name", "sid")[index]
107
112
  try:
108
113
  obj2 = self.randomizer_model_cls.objects.get(sid=row["sid"])
109
114
  except ObjectDoesNotExist:
@@ -101,7 +101,7 @@ class Randomizer:
101
101
  )
102
102
  filename: str = "randomization_list.csv"
103
103
  randomizationlist_folder: Path | str = get_randomization_list_path()
104
- extra_csv_fieldnames: list[str] | None = None
104
+ extra_csv_fieldnames: tuple[str] | None = None
105
105
  trial_is_blinded: bool = True
106
106
  importer_cls: Any = RandomizationListImporter
107
107
  apps = None # if not using django_apps
@@ -28,7 +28,22 @@ class SiteRandomizerError(Exception):
28
28
 
29
29
 
30
30
  class SiteRandomizers:
31
- """Main controller of :class:`SiteRandomizers` objects."""
31
+ """Main controller of :class:`SiteRandomizers` objects.
32
+
33
+ To rescan the CSV file for randomizer additional slots:
34
+
35
+ from edc_randomization.site_randomizers import site_randomizers
36
+ # assuming the randimizer is "default"
37
+ site_randomizers.get("default").import_list(add=True,dryrun=True)
38
+
39
+ If all OK:
40
+
41
+ # assuming the randimizer is "default"
42
+ site_randomizers.get("default").import_list(add=True,dryrun=False)
43
+
44
+ See
45
+
46
+ """
32
47
 
33
48
  def __init__(self):
34
49
  self._registry = {}
@@ -57,12 +72,12 @@ class SiteRandomizers:
57
72
  def get(self, name):
58
73
  try:
59
74
  return self._registry[str(name)]
60
- except KeyError:
75
+ except KeyError as e:
61
76
  raise NotRegistered(
62
77
  f"A Randomizer class by this name is not registered. "
63
78
  f"Expected one of {list(self._registry.keys())}. "
64
79
  f"Got '{name}'. See site_randomizer."
65
- )
80
+ ) from e
66
81
 
67
82
  def get_by_model(self, model=None):
68
83
  """Returns the randomizer class for this model label_lower.
@@ -75,10 +90,12 @@ class SiteRandomizers:
75
90
  return None
76
91
 
77
92
  def get_as_choices(self):
78
- choices = []
79
- for randomizer_cls in self._registry.values():
80
- choices.append((randomizer_cls.name, randomizer_cls.name))
81
- return tuple(choices)
93
+ return tuple(
94
+ [
95
+ (randomizer_cls.name, randomizer_cls.name)
96
+ for randomizer_cls in self._registry.values()
97
+ ]
98
+ )
82
99
 
83
100
  def randomize(
84
101
  self,
@@ -115,7 +132,7 @@ class SiteRandomizers:
115
132
  before_import_registry = copy.copy(site_randomizers._registry)
116
133
  import_module(f"{app}.{module_name}")
117
134
  if verbose:
118
- sys.stdout.write(" * registered randomizer from " f"'{app}'\n")
135
+ sys.stdout.write(f" * registered randomizer from '{app}'\n")
119
136
  except Exception as e:
120
137
  if f"No module named '{app}.{module_name}'" not in str(e):
121
138
  raise
@@ -1,3 +1,5 @@
1
+ import contextlib
2
+
1
3
  from django.conf import settings
2
4
 
3
5
  from .dashboard_templates import dashboard_templates
@@ -10,12 +12,10 @@ class DashboardMiddleware:
10
12
  def __call__(self, request):
11
13
  return self.get_response(request)
12
14
 
13
- def process_view(self, request, *args) -> None:
15
+ def process_view(self, request, *args) -> None: # noqa: ARG002
14
16
  template_data = dashboard_templates
15
- try:
17
+ with contextlib.suppress(AttributeError):
16
18
  template_data.update(settings.REVIEW_DASHBOARD_BASE_TEMPLATES)
17
- except AttributeError:
18
- pass
19
19
  request.template_data.update(**template_data)
20
20
 
21
21
  def process_template_response(self, request, response):
@@ -49,15 +49,15 @@ class SubjectReviewListboardView(
49
49
  listboard_view_permission_codename = "edc_review_dashboard.view_subject_review_listboard"
50
50
 
51
51
  navbar_selected_item = "subject_review"
52
- ordering = ["subject_identifier", "visit_code", "visit_code_sequence"]
52
+ ordering = ("subject_identifier", "visit_code", "visit_code_sequence")
53
53
  paginate_by = 25
54
54
  search_form_url = "subject_review_listboard_url"
55
- search_fields = [
55
+ search_fields = (
56
56
  "subject_identifier",
57
57
  "visit_code",
58
58
  "user_created",
59
59
  "user_modified",
60
- ]
60
+ )
61
61
 
62
62
  # attr to call SubjectReviewListboardView.urls in urls.py
63
63
  urlconfig_getattr = "review_listboard_urls"
@@ -25,7 +25,7 @@ class AgeEvaluator(ReportableAgeEvaluator):
25
25
  self.reasons_ineligible = "Age unknown"
26
26
  return eligible
27
27
 
28
- def in_bounds_or_raise(self, age: int = None, **kwargs):
28
+ def in_bounds_or_raise(self, age: int | None = None, **kwargs): # noqa: ARG002
29
29
  self.reasons_ineligible = ""
30
30
  dob = localtime(timezone.now() - relativedelta(years=age)).date()
31
31
  age_units = "years"
@@ -18,14 +18,14 @@ class Eligibility:
18
18
  # default to eligible if >=18
19
19
  age_evaluator = AgeEvaluator(age_lower=18, age_lower_inclusive=True)
20
20
 
21
- custom_reasons_dict: dict = {}
21
+ custom_reasons_dict: dict = {} # noqa: RUF012
22
22
 
23
23
  def __init__(
24
24
  self,
25
- age: int = None,
26
- gender: str = None,
27
- pregnant: bool = None,
28
- breast_feeding: bool = None,
25
+ age: int | None = None,
26
+ gender: str | None = None,
27
+ pregnant: bool | None = None,
28
+ breast_feeding: bool | None = None,
29
29
  **additional_criteria,
30
30
  ) -> None:
31
31
  self.criteria = dict(**additional_criteria)
@@ -17,15 +17,15 @@ class ScreeningEligibilityCleanedDataKeyError(Exception):
17
17
  pass
18
18
 
19
19
 
20
- class ScreeningEligibilityInvalidCombination(Exception):
20
+ class ScreeningEligibilityInvalidCombination(Exception): # noqa: N818
21
21
  pass
22
22
 
23
23
 
24
- class RequiredFieldValueMissing(Exception):
24
+ class RequiredFieldValueMissing(Exception): # noqa: N818
25
25
  pass
26
26
 
27
27
 
28
- class InvalidScreeningIdentifierFormat(Exception):
28
+ class InvalidScreeningIdentifierFormat(Exception): # noqa: N818
29
29
  def __init__(self, *args, **kwargs):
30
30
  self.code = INVALID_SCREENING_IDENTIFIER
31
31
  super().__init__(*args, **kwargs)
@@ -2,7 +2,7 @@ from edc_constants.constants import FEMALE, MALE
2
2
 
3
3
 
4
4
  class GenderEvaluator:
5
- eligible_gender = [MALE, FEMALE]
5
+ eligible_gender = (MALE, FEMALE)
6
6
 
7
7
  def __init__(self, gender=None, **kwargs) -> None: # noqa
8
8
  self.eligible = False
@@ -27,7 +27,7 @@ class AlreadyConsentedFormMixin:
27
27
  raise forms.ValidationError(self.already_consented_validation_message(url))
28
28
  return cleaned_data
29
29
 
30
- def already_consented_validation_url(self, cleaned_data: dict | None = None) -> str:
30
+ def already_consented_validation_url(self, cleaned_data: dict | None = None) -> str: # noqa: ARG002
31
31
  url_name = url_names.get("subject_dashboard_url")
32
32
  return reverse(
33
33
  url_name,
@@ -2,7 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  from typing import TYPE_CHECKING
4
4
 
5
- from django.utils.html import format_html
6
5
  from django.utils.safestring import mark_safe
7
6
 
8
7
  from edc_constants.constants import NO, PENDING, TBD, YES
@@ -33,7 +32,7 @@ class ScreeningEligibility:
33
32
  eligible_fld_name: str = "eligible"
34
33
  eligible_value_default: str = TBD
35
34
  default_display_label = TBD
36
- eligible_values_list: list = [YES, NO, TBD]
35
+ eligible_values_list: list = (YES, NO, TBD)
37
36
  ineligible_display_label: str = "INELIGIBLE"
38
37
  is_eligible_value: str = YES
39
38
  is_ineligible_value: str = NO
@@ -44,9 +43,9 @@ class ScreeningEligibility:
44
43
  def __init__(
45
44
  self,
46
45
  model_obj: SubjectScreeningModel | EligibilityModelMixin = None,
47
- cleaned_data: dict = None,
46
+ cleaned_data: dict | None = None,
48
47
  eligible_value_default: str | None = None,
49
- eligible_values_list: list | None = None,
48
+ eligible_values_list: tuple[str, ...] | None = None,
50
49
  is_eligible_value: str | None = None,
51
50
  is_ineligible_value: str | None = None,
52
51
  eligible_display_label: str | None = None,
@@ -126,7 +125,7 @@ class ScreeningEligibility:
126
125
  @property
127
126
  def is_eligible(self) -> bool:
128
127
  """Returns True if eligible else False"""
129
- return True if self.eligible == self.is_eligible_value else False
128
+ return self.eligible == self.is_eligible_value
130
129
 
131
130
  def _assess_eligibility(self) -> None:
132
131
  self.set_fld_attrs_on_self()
@@ -136,30 +135,29 @@ class ScreeningEligibility:
136
135
  self.reasons_ineligible.update(**missing_data)
137
136
  self.eligible = self.eligible_value_default # probably TBD
138
137
  for fldattr, fc in self.get_required_fields().items():
139
- if fldattr not in missing_data:
140
- if fc and fc.value:
141
- msg = fc.msg if fc.msg else fldattr.title().replace("_", " ")
142
- is_callable = False
143
- try:
144
- value = fc.value(getattr(self, fldattr))
145
- except TypeError:
146
- value = fc.value
147
- else:
148
- is_callable = True
149
- if (
150
- (isinstance(value, str) and getattr(self, fldattr) != value)
151
- or (
152
- isinstance(value, (list, tuple))
153
- and getattr(self, fldattr) not in value
154
- )
155
- or (
156
- isinstance(value, range)
157
- and not (min(value) <= getattr(self, fldattr) <= max(value))
158
- )
159
- or (is_callable and value is False)
160
- ):
161
- self.reasons_ineligible.update({fldattr: msg})
162
- self.eligible = self.is_ineligible_value # probably NO
138
+ if fldattr not in missing_data and fc and fc.value:
139
+ msg = fc.msg if fc.msg else fldattr.title().replace("_", " ")
140
+ is_callable = False
141
+ try:
142
+ value = fc.value(getattr(self, fldattr))
143
+ except TypeError:
144
+ value = fc.value
145
+ else:
146
+ is_callable = True
147
+ if (
148
+ (isinstance(value, str) and getattr(self, fldattr) != value)
149
+ or (
150
+ isinstance(value, (list, tuple))
151
+ and getattr(self, fldattr) not in value
152
+ )
153
+ or (
154
+ isinstance(value, range)
155
+ and not (min(value) <= getattr(self, fldattr) <= max(value))
156
+ )
157
+ or (is_callable and value is False)
158
+ ):
159
+ self.reasons_ineligible.update({fldattr: msg})
160
+ self.eligible = self.is_ineligible_value # probably NO
163
161
  if self.is_eligible:
164
162
  if self.is_eligible and not self.get_required_fields():
165
163
  self.eligible = self.eligible_value_default
@@ -194,7 +192,7 @@ class ScreeningEligibility:
194
192
  "does not exist on class. "
195
193
  f"See {self.__class__.__name__}. "
196
194
  f"Got {e}"
197
- )
195
+ ) from e
198
196
  if self.model_obj:
199
197
  try:
200
198
  value = (
@@ -207,7 +205,7 @@ class ScreeningEligibility:
207
205
  "Attribute does not exist on model. "
208
206
  f"See {self.model_obj.__class__.__name__}. "
209
207
  f"Got {e}"
210
- )
208
+ ) from e
211
209
  else:
212
210
  value = self.cleaned_data.get(fldattr)
213
211
  setattr(self, fldattr, value)
@@ -230,10 +228,7 @@ class ScreeningEligibility:
230
228
 
231
229
  def formatted_reasons_ineligible(self) -> str:
232
230
  str_values = "<BR>".join([x for x in self.reasons_ineligible.values() if x])
233
- return format_html(
234
- "{}",
235
- mark_safe(str_values), # nosec B703 B308
236
- )
231
+ return mark_safe(str_values) # noqa: S308
237
232
 
238
233
  @property
239
234
  def display_label(self) -> str: