clinicedc 2.0.24__py3-none-any.whl → 2.0.26__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 (58) hide show
  1. {clinicedc-2.0.24.dist-info → clinicedc-2.0.26.dist-info}/METADATA +1 -1
  2. {clinicedc-2.0.24.dist-info → clinicedc-2.0.26.dist-info}/RECORD +58 -57
  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_navbar/navbar.py +2 -2
  21. edc_randomization/admin.py +3 -1
  22. edc_randomization/apps.py +8 -11
  23. edc_randomization/auth_objects.py +9 -0
  24. edc_randomization/blinding.py +6 -12
  25. edc_randomization/decorators.py +2 -3
  26. edc_randomization/exceptions.py +73 -0
  27. edc_randomization/model_mixins.py +2 -3
  28. edc_randomization/randomization_list_importer.py +35 -36
  29. edc_randomization/randomization_list_verifier.py +19 -21
  30. edc_randomization/randomizer.py +12 -28
  31. edc_randomization/site_randomizers.py +2 -16
  32. edc_randomization/system_checks.py +1 -1
  33. edc_randomization/utils.py +3 -10
  34. edc_review_dashboard/middleware.py +4 -4
  35. edc_review_dashboard/views/subject_review_listboard_view.py +3 -3
  36. edc_screening/age_evaluator.py +1 -1
  37. edc_screening/eligibility.py +5 -5
  38. edc_screening/exceptions.py +3 -3
  39. edc_screening/gender_evaluator.py +1 -1
  40. edc_screening/modelform_mixins.py +1 -1
  41. edc_screening/screening_eligibility.py +30 -35
  42. edc_screening/utils.py +10 -12
  43. edc_sites/admin/list_filters.py +2 -2
  44. edc_sites/admin/site_model_admin_mixin.py +8 -8
  45. edc_sites/exceptions.py +1 -1
  46. edc_sites/forms.py +3 -1
  47. edc_sites/management/commands/sync_sites.py +11 -17
  48. edc_sites/models/site_profile.py +6 -4
  49. edc_sites/post_migrate_signals.py +2 -2
  50. edc_sites/site.py +8 -8
  51. edc_sites/utils/add_or_update_django_sites.py +3 -3
  52. edc_sites/utils/valid_site_for_subject_or_raise.py +7 -4
  53. edc_sites/view_mixins.py +2 -2
  54. edc_subject_dashboard/view_utils/subject_screening_button.py +5 -3
  55. edc_view_utils/dashboard_model_button.py +3 -3
  56. edc_visit_schedule/site_visit_schedules.py +2 -1
  57. {clinicedc-2.0.24.dist-info → clinicedc-2.0.26.dist-info}/WHEEL +0 -0
  58. {clinicedc-2.0.24.dist-info → clinicedc-2.0.26.dist-info}/licenses/LICENSE +0 -0
@@ -6,17 +6,12 @@ from django.core.exceptions import ObjectDoesNotExist
6
6
  from django.core.management.color import color_style
7
7
  from django.db.utils import OperationalError, ProgrammingError
8
8
 
9
+ from .exceptions import InvalidAssignment, RandomizationListError
9
10
  from .site_randomizers import site_randomizers
10
11
 
11
12
  style = color_style()
12
13
 
13
-
14
- class RandomizationListError(Exception):
15
- pass
16
-
17
-
18
- class InvalidAssignment(Exception):
19
- pass
14
+ __all__ = ["RandomizationListVerifier"]
20
15
 
21
16
 
22
17
  class RandomizationListVerifier:
@@ -25,12 +20,12 @@ class RandomizationListVerifier:
25
20
  def __init__(
26
21
  self,
27
22
  randomizer_name=None,
28
- randomizationlist_path: Path | str = None,
23
+ randomizationlist_path: Path | str | None = None,
29
24
  randomizer_model_cls=None,
30
25
  assignment_map=None,
31
26
  fieldnames=None,
32
27
  sid_count_for_tests=None,
33
- required_csv_fieldnames: list[str] | None = None,
28
+ required_csv_fieldnames: tuple[str, ...] | None = None,
34
29
  **kwargs, # noqa: ARG002
35
30
  ):
36
31
  self.count: int = 0
@@ -78,28 +73,31 @@ class RandomizationListVerifier:
78
73
  def verify(self) -> str | None:
79
74
  message = None
80
75
 
76
+ # read and sort from CSV file
81
77
  with self.randomizationlist_path.open(mode="r") as f:
82
78
  reader = csv.DictReader(f)
83
79
  all_rows = [{k: v.strip() for k, v in row.items() if k} for row in reader]
84
80
  sorted_rows = sorted(
85
- all_rows, key=lambda row: (row.get("site_name", ""), row.get("sid", ""))
81
+ all_rows, key=lambda row: (row.get("site_name", ""), int(row.get("sid", 0)))
86
82
  )
87
83
 
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:
84
+ # compare sorted CSV length with DB
85
+ if len(sorted_rows) != self.randomizer_model_cls.objects.all().count():
86
+ expected_cnt = len(sorted_rows)
87
+ actual_cnt = self.randomizer_model_cls.objects.all().count()
97
88
  message = (
98
- f"Randomization list count is off. Expected {index} (CSV). "
99
- f"Got {self.count} (model_cls). See file "
89
+ f"Randomization list count is off. Expected {expected_cnt} (CSV). "
90
+ f"Got {actual_cnt} (model_cls). See file "
100
91
  f"{self.randomizationlist_path}. "
101
92
  f"Resolve this issue before using the system."
102
93
  )
94
+ if not message:
95
+ # compare sorted CSV data to DB
96
+ for index, row in enumerate(sorted_rows, start=1):
97
+ sys.stdout.write(f"Index: {index}, SID: {row.get('sid')}, Row: {row}\n")
98
+ message = self.inspect_row(index - 1, row)
99
+ if message:
100
+ break
103
101
  return message
104
102
 
105
103
  def inspect_row(self, index: int, row) -> str | None:
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING, Any
8
8
 
9
9
  from django.apps import apps as django_apps
10
10
  from django.conf import settings
11
- from django.core.exceptions import ObjectDoesNotExist, ValidationError
11
+ from django.core.exceptions import ObjectDoesNotExist
12
12
  from django.db.models import Q
13
13
 
14
14
  from edc_registration.utils import get_registered_subject_model_cls
@@ -18,38 +18,22 @@ from .constants import (
18
18
  DEFAULT_ASSIGNMENT_MAP,
19
19
  RANDOMIZED,
20
20
  )
21
- from .randomization_list_importer import (
21
+ from .exceptions import (
22
+ AllocationError,
23
+ AlreadyRandomized,
24
+ InvalidAssignmentDescriptionMap,
25
+ RandomizationError,
22
26
  RandomizationListAlreadyImported,
23
- RandomizationListImporter,
27
+ RandomizationListFileNotFound,
24
28
  )
29
+ from .randomization_list_importer import RandomizationListImporter
25
30
  from .utils import get_randomization_list_path
26
31
 
27
32
  if TYPE_CHECKING:
28
33
  from edc_registration.models import RegisteredSubject
29
34
 
30
35
 
31
- class InvalidAssignmentDescriptionMap(Exception): # noqa: N818
32
- pass
33
-
34
-
35
- class RandomizationListFileNotFound(Exception): # noqa: N818
36
- pass
37
-
38
-
39
- class RandomizationListNotLoaded(Exception): # noqa: N818
40
- pass
41
-
42
-
43
- class RandomizationError(Exception):
44
- pass
45
-
46
-
47
- class AlreadyRandomized(ValidationError): # noqa: N818
48
- pass
49
-
50
-
51
- class AllocationError(Exception):
52
- pass
36
+ __all__ = ["Randomizer"]
53
37
 
54
38
 
55
39
  class Randomizer:
@@ -61,8 +45,8 @@ class Randomizer:
61
45
  `site_randomizer` by default. To prevent registration set
62
46
  settings.EDC_RANDOMIZATION_REGISTER_DEFAULT_RANDOMIZER=False.
63
47
 
64
- assignment_map: {<assignment:str)>: <allocation:int>, ...}
65
- assignment_description_map: {<assignment:str)>: <description:str>, ...}
48
+ assignment_map: {<assignment:str>: <allocation:int>, ...}
49
+ assignment_description_map: {<assignment:str>: <description:str>, ...}
66
50
 
67
51
 
68
52
  Usage:
@@ -101,7 +85,7 @@ class Randomizer:
101
85
  )
102
86
  filename: str = "randomization_list.csv"
103
87
  randomizationlist_folder: Path | str = get_randomization_list_path()
104
- extra_csv_fieldnames: list[str] | None = None
88
+ extra_csv_fieldnames: tuple[str] | None = None
105
89
  trial_is_blinded: bool = True
106
90
  importer_cls: Any = RandomizationListImporter
107
91
  apps = None # if not using django_apps
@@ -7,26 +7,12 @@ from typing import TYPE_CHECKING
7
7
  from django.apps import apps as django_apps
8
8
  from django.utils.module_loading import import_module, module_has_submodule
9
9
 
10
+ from .exceptions import AlreadyRegistered, NotRegistered, RegistryNotLoaded
11
+
10
12
  if TYPE_CHECKING:
11
13
  from edc_randomization.randomizer import Randomizer
12
14
 
13
15
 
14
- class RegistryNotLoaded(Exception):
15
- pass
16
-
17
-
18
- class NotRegistered(Exception):
19
- pass
20
-
21
-
22
- class AlreadyRegistered(Exception):
23
- pass
24
-
25
-
26
- class SiteRandomizerError(Exception):
27
- pass
28
-
29
-
30
16
  class SiteRandomizers:
31
17
  """Main controller of :class:`SiteRandomizers` objects.
32
18
 
@@ -3,7 +3,7 @@ import sys
3
3
  from dataclasses import dataclass
4
4
 
5
5
  from django.conf import settings
6
- from django.core.checks import Error, Warning
6
+ from django.core.checks import Error, Warning # noqa: A004
7
7
  from django.core.exceptions import ImproperlyConfigured
8
8
  from django.core.management import color_style
9
9
 
@@ -14,17 +14,10 @@ from django_pandas.io import read_frame
14
14
  from edc_pdutils.constants import SYSTEM_COLUMNS
15
15
  from edc_sites.site import sites
16
16
 
17
+ from .exceptions import RandomizationListExporterError, SubjectNotRandomization
17
18
  from .site_randomizers import site_randomizers
18
19
 
19
20
 
20
- class RandomizationListExporterError(Exception):
21
- pass
22
-
23
-
24
- class SubjectNotRandomization(Exception): # noqa: N818
25
- pass
26
-
27
-
28
21
  def get_randomization_list_path() -> Path:
29
22
  return Path(
30
23
  getattr(
@@ -149,7 +142,7 @@ def generate_fake_randomization_list(
149
142
 
150
143
  def export_randomization_list(
151
144
  randomizer_name: str, path: str | None = None, username: str | None = None
152
- ):
145
+ ) -> Path:
153
146
  randomizer_cls = site_randomizers.get(randomizer_name)
154
147
 
155
148
  try:
@@ -184,4 +177,4 @@ def export_randomization_list(
184
177
  )
185
178
  df.to_csv(**opts)
186
179
  sys.stdout.write(f"{filename!s}\n")
187
- return filename
180
+ return Path(filename)
@@ -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:
edc_screening/utils.py CHANGED
@@ -40,10 +40,8 @@ def format_reasons_ineligible(*str_values: str | None, delimiter: str | None = N
40
40
  str_values = str_values or []
41
41
  str_values = tuple(x for x in str_values if x)
42
42
  if str_values:
43
- reasons = format_html(
44
- "{}",
45
- mark_safe(delimiter.join(str_values)), # noqa: S308
46
- )
43
+ formatted_string = delimiter.join(str_values)
44
+ reasons = mark_safe(formatted_string) # noqa: S308
47
45
  return reasons
48
46
 
49
47
 
@@ -120,15 +118,15 @@ def is_eligible_or_raise(
120
118
  if url and url_name.endswith("changelist"):
121
119
  url = f"{url}?q={subject_screening.screening_identifier}"
122
120
  if not url:
123
- msg = format_html(
124
- "{}",
121
+ safe_string = mark_safe( # noqa: S308
125
122
  "Not allowed. Subject is not eligible. "
126
- f"Got {subject_screening.screening_identifier}",
123
+ f"Got {subject_screening.screening_identifier}.",
127
124
  )
128
125
  else:
129
- msg = format_html(
130
- 'Not allowed. Subject is not eligible. See subject <A href="{}">{}</A>',
131
- mark_safe(url), # noqa: S308
132
- subject_screening.screening_identifier,
126
+ safe_string = format_html(
127
+ "Not allowed. Subject is not eligible. See subject "
128
+ '<A href="{url}">{screening_identifier}</A>',
129
+ url=url,
130
+ screening_identifier=subject_screening.screening_identifier,
133
131
  )
134
- raise forms.ValidationError(msg)
132
+ raise forms.ValidationError(safe_string)
@@ -17,10 +17,10 @@ class SiteListFilter(SimpleListFilter):
17
17
  else:
18
18
  site_ids = sites.get_site_ids_for_user(request=request)
19
19
  for site in Site.objects.filter(id__in=site_ids).order_by("id"):
20
- names.append((site.id, f"{site.id} {sites.get(site.id).description}"))
20
+ names.append((site.id, f"{site.id} {sites.get(site.id).description}")) # noqa: PERF401
21
21
  return tuple(names)
22
22
 
23
- def queryset(self, request, queryset):
23
+ def queryset(self, request, queryset): # noqa: ARG002
24
24
  if self.value() and self.value() != "none":
25
25
  queryset = queryset.filter(site__id=self.value())
26
26
  return queryset
@@ -46,7 +46,7 @@ class SiteModelAdminMixin:
46
46
  ]
47
47
  return sites.get_view_only_site_ids_for_user(request=request)
48
48
 
49
- def has_viewallsites_permission(self, request, obj=None) -> bool:
49
+ def has_viewallsites_permission(self, request, obj=None) -> bool: # noqa: ARG002
50
50
  """Checks if the user has the EDC custom codename
51
51
  "viewallsites" for this model.
52
52
 
@@ -54,7 +54,7 @@ class SiteModelAdminMixin:
54
54
  """
55
55
  opts = self.opts
56
56
  codename_allsites = get_permission_codename("viewallsites", opts)
57
- return request.user.has_perm("%s.%s" % (opts.app_label, codename_allsites))
57
+ return request.user.has_perm(f"{opts.app_label}.{codename_allsites}")
58
58
 
59
59
  @admin.display(description="Site", ordering="site__id")
60
60
  def site_code(self, obj=None):
@@ -75,7 +75,7 @@ class SiteModelAdminMixin:
75
75
  to mulitple sites.
76
76
  """
77
77
  list_filter = super().get_list_filter(request)
78
- list_filter = [x for x in list_filter if x != "site" and x != SiteListFilter]
78
+ list_filter = [x for x in list_filter if x not in ("site", SiteListFilter)]
79
79
  if self.user_may_view_other_sites(request) or self.has_viewallsites_permission(
80
80
  request
81
81
  ):
@@ -95,25 +95,25 @@ class SiteModelAdminMixin:
95
95
  or self.has_viewallsites_permission(request)
96
96
  ) and "site" not in list_display:
97
97
  list_display = tuple(list_display)
98
- list_display = list_display[:pos] + (self.site_code,) + list_display[pos:]
98
+ list_display = list_display[:pos], self.site_code, list_display[pos:]
99
99
  elif "site" in list_display:
100
100
  list_display = tuple(
101
101
  [x for x in list_display if x not in ["site", self.site_code]]
102
102
  )
103
- list_display = list_display[:pos] + (self.site_code,) + list_display[pos:]
103
+ list_display = list_display[:pos], self.site_code, list_display[pos:]
104
104
  return list_display
105
105
 
106
106
  def get_queryset(self, request) -> QuerySet:
107
107
  """Limit modeladmin queryset for the current site only"""
108
108
  qs = super().get_queryset(request)
109
- site_ids = [request.site.id] + self.get_view_only_site_ids_for_user(request=request)
109
+ site_ids = (request.site.id, *self.get_view_only_site_ids_for_user(request=request))
110
110
  try:
111
111
  qs = qs.select_related("site").filter(site_id__in=site_ids)
112
- except FieldError:
112
+ except FieldError as e:
113
113
  raise SiteModeAdminMixinError(
114
114
  f"Model missing field `site`. Model `{self.model}`. Did you mean to use "
115
115
  f"the SiteModelAdminMixin? See `{self}`."
116
- )
116
+ ) from e
117
117
  return qs
118
118
 
119
119
  def get_form(self, request, obj=None, change=False, **kwargs):
edc_sites/exceptions.py CHANGED
@@ -6,5 +6,5 @@ class InvalidSiteForSubjectError(Exception):
6
6
  pass
7
7
 
8
8
 
9
- class InvalidSiteForUser(Exception):
9
+ class InvalidSiteForUser(Exception): # noqa: N818
10
10
  pass
edc_sites/forms.py CHANGED
@@ -1 +1,3 @@
1
- from .modelform_mixins import SiteModelFormMixin # noqa
1
+ from .modelform_mixins import SiteModelFormMixin
2
+
3
+ __all__ = ["SiteModelFormMixin"]
@@ -4,6 +4,8 @@ import sys
4
4
  from django.conf import settings
5
5
  from django.core.management.base import BaseCommand
6
6
  from django.core.management.color import color_style
7
+ from multisite.models import Alias
8
+ from multisite.utils import create_or_sync_canonical_from_all_sites
7
9
 
8
10
  from edc_sites.site import sites as site_sites
9
11
  from edc_sites.utils import add_or_update_django_sites
@@ -15,7 +17,6 @@ class Command(BaseCommand):
15
17
  help = "Add / Update django Site model after changes to edc_sites"
16
18
 
17
19
  def add_arguments(self, parser):
18
-
19
20
  parser.add_argument(
20
21
  "--ping-hosts",
21
22
  default=False,
@@ -32,26 +33,19 @@ class Command(BaseCommand):
32
33
  help="Suggest ALLOWED_HOSTS",
33
34
  )
34
35
 
35
- def handle(self, *args, **options) -> None:
36
+ def handle(self, *args, **options) -> None: # noqa: ARG002
36
37
  sys.stdout.write("\n\n")
37
38
  sys.stdout.write(" Edc Sites : Adding / Updating sites ... \n")
38
39
  add_or_update_django_sites(verbose=True)
39
- if (
40
- "multisite" in settings.INSTALLED_APPS
41
- or "multisite.apps.AppConfig" in settings.INSTALLED_APPS
42
- ):
43
- from multisite.models import Alias
44
- from multisite.utils import create_or_sync_canonical_from_all_sites
45
-
46
- sys.stdout.write("\n Multisite. \n")
47
- create_or_sync_canonical_from_all_sites(verbose=True)
40
+ sys.stdout.write("\n Multisite. \n")
41
+ create_or_sync_canonical_from_all_sites(verbose=True)
48
42
 
49
- sys.stdout.write(" multisite.Alias \n")
50
- for obj in Alias.objects.all():
51
- sys.stdout.write(
52
- f" - Site model: {obj.site.id}: {obj.domain} "
53
- f"is_canonical={obj.is_canonical}.\n"
54
- )
43
+ sys.stdout.write(" multisite.Alias \n")
44
+ for obj in Alias.objects.all():
45
+ sys.stdout.write(
46
+ f" - Site model: {obj.site.id}: {obj.domain} "
47
+ f"is_canonical={obj.is_canonical}.\n"
48
+ )
55
49
  arg = [arg for arg in sys.argv if arg.startswith("--settings")]
56
50
  sys.stdout.write(f"\n Settings: reading from {arg}")
57
51
  sys.stdout.write("\n\n Current value of settings.ALLOWED_HOSTS\n")
@@ -5,17 +5,19 @@ import json
5
5
  from django.contrib.sites.models import Site
6
6
  from django.db import models
7
7
 
8
+ from edc_constants.constants import NULL_STRING
9
+
8
10
 
9
11
  class SiteProfile(models.Model):
10
12
  id = models.BigAutoField(primary_key=True)
11
13
 
12
- country = models.CharField(max_length=250, default="")
14
+ country = models.CharField(max_length=250, default=NULL_STRING)
13
15
 
14
- country_code = models.CharField(max_length=15, default="")
16
+ country_code = models.CharField(max_length=15, default=NULL_STRING)
15
17
 
16
- languages = models.TextField(default="")
18
+ languages = models.TextField(default=NULL_STRING)
17
19
 
18
- title = models.CharField(max_length=250, default="")
20
+ title = models.CharField(max_length=250, default=NULL_STRING)
19
21
 
20
22
  site = models.OneToOneField(Site, on_delete=models.PROTECT)
21
23