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
edc_identifier/models.py CHANGED
@@ -1,6 +1,7 @@
1
1
  from django.db import models
2
2
  from django.db.models import UniqueConstraint
3
3
 
4
+ from edc_constants.constants import NULL_STRING
4
5
  from edc_model.models import BaseUuidModel
5
6
  from edc_sites.model_mixins import SiteModelMixin
6
7
 
@@ -25,21 +26,21 @@ class IdentifierModel(SiteModelMixin, BaseUuidModel):
25
26
 
26
27
  name = models.CharField(max_length=100)
27
28
 
28
- subject_identifier = models.CharField(max_length=50, default="")
29
+ subject_identifier = models.CharField(max_length=50, default=NULL_STRING)
29
30
 
30
31
  sequence_number = models.IntegerField(default=1)
31
32
 
32
- linked_identifier = models.CharField(max_length=50, default="")
33
+ linked_identifier = models.CharField(max_length=50, default=NULL_STRING)
33
34
 
34
35
  device_id = models.IntegerField()
35
36
 
36
- protocol_number = models.CharField(max_length=25, default="")
37
+ protocol_number = models.CharField(max_length=25, default=NULL_STRING)
37
38
 
38
- model = models.CharField(max_length=100, default="")
39
+ model = models.CharField(max_length=100, default=NULL_STRING)
39
40
 
40
- identifier_type = models.CharField(max_length=100, default="")
41
+ identifier_type = models.CharField(max_length=100, default=NULL_STRING)
41
42
 
42
- identifier_prefix = models.CharField(max_length=25, default="")
43
+ identifier_prefix = models.CharField(max_length=25, default=NULL_STRING)
43
44
 
44
45
  objects = IdentifierModelManager()
45
46
 
@@ -21,7 +21,7 @@ class IdentifierMissingTemplateValue(Exception): # noqa: N818
21
21
 
22
22
 
23
23
  class ResearchIdentifier:
24
- label: str = None # e.g. subject_identifier, plot_identifier, etc
24
+ label: str | None = None # e.g. subject_identifier, plot_identifier, etc
25
25
  identifier_type: str | None = None # e.g. 'subject', 'infant', 'plot', a.k.a subject_type
26
26
  template: str | None = None
27
27
  padding: int = 5
@@ -100,7 +100,7 @@ class ShortIdentifier:
100
100
  tries += 1
101
101
  random_string = "".join(
102
102
  [
103
- random.choice(allowed_chars) # nosec B311
103
+ random.choice(allowed_chars) # nosec B311 # noqa: S311
104
104
  for _ in range(self.random_string_length)
105
105
  ]
106
106
  )
@@ -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)
edc_navbar/navbar.py CHANGED
@@ -29,7 +29,7 @@ class Navbar:
29
29
 
30
30
  def get(self, name: str) -> NavbarItem | None:
31
31
  try:
32
- navbar_item = [nb for nb in self.navbar_items if nb.name == name][0]
32
+ navbar_item = next(nb for nb in self.navbar_items if nb.name == name)
33
33
  except IndexError:
34
34
  navbar_item = None
35
35
  return navbar_item
@@ -37,7 +37,7 @@ class Navbar:
37
37
  def set_active(self, name: str) -> None:
38
38
  if name:
39
39
  for navbar_item in self.navbar_items:
40
- navbar_item.active = True if navbar_item.name == name else False
40
+ navbar_item.active = navbar_item.name == name
41
41
 
42
42
  def show_user_permissions(self, user: User = None) -> dict[str, dict[str, bool]]:
43
43
  """Returns the permissions required to access this Navbar
@@ -17,6 +17,8 @@ from .auth_objects import RANDO_UNBLINDED
17
17
  from .blinding import user_is_blinded
18
18
  from .site_randomizers import site_randomizers
19
19
 
20
+ __all__ = ["RandomizationListModelAdmin", "print_pharmacy_labels"]
21
+
20
22
 
21
23
  @admin.action(permissions=["view"], description="Print labels for pharmacy")
22
24
  def print_pharmacy_labels(modeladmin, request, queryset):
@@ -59,7 +61,7 @@ class RandomizationListModelAdmin(TemplatesModelAdminMixin, admin.ModelAdmin):
59
61
 
60
62
  search_fields = ("subject_identifier", "sid")
61
63
 
62
- def get_fieldsets(self, request, obj=None):
64
+ def get_fieldsets(self, request, obj=None): # noqa: ARG002
63
65
  return (
64
66
  (None, {"fields": self.get_fieldnames(request)}),
65
67
  audit_fieldset_tuple,
edc_randomization/apps.py CHANGED
@@ -1,8 +1,4 @@
1
- import os
2
- from warnings import warn
3
-
4
1
  from django.apps import AppConfig as DjangoAppConfig
5
- from django.conf import settings
6
2
  from django.core.checks.registry import register
7
3
 
8
4
  from .system_checks import randomizationlist_check
@@ -17,10 +13,11 @@ class AppConfig(DjangoAppConfig):
17
13
  def ready(self):
18
14
  register(randomizationlist_check, deploy=True)
19
15
 
20
- @property
21
- def randomization_list_path(self):
22
- warn(
23
- "Use of settings.RANDOMIZATION_LIST_PATH has been deprecated. "
24
- "See site_randomizers in edc_randomization"
25
- )
26
- return os.path.join(settings.RANDOMIZATION_LIST_PATH)
16
+ # @property
17
+ # def randomization_list_path(self):
18
+ # warn(
19
+ # "Use of settings.RANDOMIZATION_LIST_PATH has been deprecated. "
20
+ # "See site_randomizers in edc_randomization",
21
+ # stacklevel=2,
22
+ # )
23
+ # return os.path.join(settings.RANDOMIZATION_LIST_PATH)
@@ -2,6 +2,15 @@ import sys
2
2
 
3
3
  from .site_randomizers import site_randomizers
4
4
 
5
+ __all__ = [
6
+ "RANDO_BLINDED",
7
+ "RANDO_UNBLINDED",
8
+ "get_rando_permissions_codenames",
9
+ "get_rando_permissions_tuples",
10
+ "make_randomizationlist_view_only",
11
+ "update_rando_group_permissions",
12
+ ]
13
+
5
14
  RANDO_UNBLINDED = "RANDO_UNBLINDED"
6
15
  RANDO_BLINDED = "RANDO_BLINDED"
7
16
 
@@ -4,7 +4,6 @@ from django import forms
4
4
  from django.conf import settings
5
5
  from django.contrib.auth import get_user_model
6
6
  from django.core.exceptions import ImproperlyConfigured, ObjectDoesNotExist
7
- from django.utils.html import format_html
8
7
  from django.utils.safestring import mark_safe
9
8
 
10
9
  from .auth_objects import RANDO_UNBLINDED
@@ -43,13 +42,11 @@ def user_is_blinded(username) -> bool:
43
42
  return blinded
44
43
 
45
44
 
46
- def user_is_blinded_from_request(request):
47
- if user_is_blinded(request.user.username) or (
45
+ def user_is_blinded_from_request(request) -> bool:
46
+ return user_is_blinded(request.user.username) or (
48
47
  not user_is_blinded(request.user.username)
49
48
  and RANDO_UNBLINDED not in [g.name for g in request.user.groups.all()]
50
- ):
51
- return True
52
- return False
49
+ )
53
50
 
54
51
 
55
52
  def raise_if_prohibited_from_unblinded_rando_group(username: str, groups: Iterable) -> None:
@@ -61,12 +58,9 @@ def raise_if_prohibited_from_unblinded_rando_group(username: str, groups: Iterab
61
58
  if RANDO_UNBLINDED in [grp.name for grp in groups] and user_is_blinded(username):
62
59
  raise forms.ValidationError(
63
60
  {
64
- "groups": format_html(
65
- "{}",
66
- mark_safe(
67
- "This user is not unblinded and may not added "
68
- "to the <U>RANDO_UNBLINDED</U> group."
69
- ), # nosec B703 B308
61
+ "groups": mark_safe(
62
+ "This user is not unblinded and may not added "
63
+ "to the <U>RANDO_UNBLINDED</U> group."
70
64
  )
71
65
  }
72
66
  )
@@ -1,11 +1,10 @@
1
1
  from typing import Any
2
2
 
3
+ from .exceptions import RegisterRandomizerError
3
4
  from .randomizer import Randomizer
4
5
  from .site_randomizers import site_randomizers
5
6
 
6
-
7
- class RegisterRandomizerError(Exception):
8
- pass
7
+ __all__ = ["register"]
9
8
 
10
9
 
11
10
  def register(site=None, **kwargs) -> Any:
@@ -0,0 +1,73 @@
1
+ from django.core.exceptions import ValidationError
2
+
3
+
4
+ class InvalidAssignment(Exception): # noqa: N818
5
+ pass
6
+
7
+
8
+ class RandomizationListImportError(Exception):
9
+ pass
10
+
11
+
12
+ class RandomizationListAlreadyImported(Exception): # noqa: N818
13
+ pass
14
+
15
+
16
+ class RandomizationListError(Exception):
17
+ pass
18
+
19
+
20
+ class RegistryNotLoaded(Exception): # noqa: N818
21
+ pass
22
+
23
+
24
+ class NotRegistered(Exception): # noqa: N818
25
+ pass
26
+
27
+
28
+ class AlreadyRegistered(Exception): # noqa: N818
29
+ pass
30
+
31
+
32
+ class SiteRandomizerError(Exception):
33
+ pass
34
+
35
+
36
+ class RandomizationListExporterError(Exception):
37
+ pass
38
+
39
+
40
+ class SubjectNotRandomization(Exception): # noqa: N818
41
+ pass
42
+
43
+
44
+ class InvalidAssignmentDescriptionMap(Exception): # noqa: N818
45
+ pass
46
+
47
+
48
+ class RandomizationListFileNotFound(Exception): # noqa: N818
49
+ pass
50
+
51
+
52
+ class RandomizationListNotLoaded(Exception): # noqa: N818
53
+ pass
54
+
55
+
56
+ class RandomizationError(Exception):
57
+ pass
58
+
59
+
60
+ class AlreadyRandomized(ValidationError): # noqa: N818
61
+ pass
62
+
63
+
64
+ class AllocationError(Exception):
65
+ pass
66
+
67
+
68
+ class RegisterRandomizerError(Exception):
69
+ pass
70
+
71
+
72
+ class RandomizationListModelError(Exception):
73
+ pass
@@ -7,12 +7,11 @@ from django_crypto_fields.fields import EncryptedCharField
7
7
  from edc_model.models import HistoricalRecords
8
8
  from edc_sites.managers import CurrentSiteManager
9
9
 
10
+ from .exceptions import RandomizationListModelError
10
11
  from .randomizer import RandomizationError
11
12
  from .site_randomizers import site_randomizers
12
13
 
13
-
14
- class RandomizationListModelError(Exception):
15
- pass
14
+ __all__ = ["RandomizationListManager", "RandomizationListModelMixin"]
16
15
 
17
16
 
18
17
  class RandomizationListManager(models.Manager):
@@ -12,21 +12,16 @@ from tqdm import tqdm
12
12
 
13
13
  from edc_sites.site import sites as site_sites
14
14
 
15
+ from .exceptions import (
16
+ InvalidAssignment,
17
+ RandomizationListAlreadyImported,
18
+ RandomizationListImportError,
19
+ )
15
20
  from .randomization_list_verifier import RandomizationListVerifier
16
21
 
17
22
  style = color_style()
18
23
 
19
-
20
- class RandomizationListImportError(Exception):
21
- pass
22
-
23
-
24
- class RandomizationListAlreadyImported(Exception):
25
- pass
26
-
27
-
28
- class InvalidAssignment(Exception):
29
- pass
24
+ __all__ = ["RandomizationListImporter"]
30
25
 
31
26
 
32
27
  class RandomizationListImporter:
@@ -67,19 +62,20 @@ class RandomizationListImporter:
67
62
  def __init__(
68
63
  self,
69
64
  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,
65
+ randomizer_name: str | None = None,
66
+ randomizationlist_path: Path | str | None = None,
67
+ assignment_map: dict[str, int] | None = None,
68
+ verbose: bool | None = None,
69
+ overwrite: bool | None = None,
70
+ add: bool | None = None,
71
+ dryrun: bool | None = None,
72
+ username: str | None = None,
73
+ revision: str | None = None,
74
+ sid_count_for_tests: int | None = None,
75
+ extra_csv_fieldnames: tuple[str] | None = None,
76
+ **kwargs, # noqa: ARG002
82
77
  ):
78
+ extra_csv_fieldnames = extra_csv_fieldnames or ()
83
79
  self.verify_messages: str | None = None
84
80
  self.add = add
85
81
  self.overwrite = overwrite
@@ -92,7 +88,7 @@ class RandomizationListImporter:
92
88
  self.randomizer_name = randomizer_name
93
89
  self.assignment_map = assignment_map
94
90
  self.randomizationlist_path: Path = Path(randomizationlist_path).expanduser()
95
- self.required_csv_fieldnames.extend(extra_csv_fieldnames or [])
91
+ self.required_csv_fieldnames = (*self.required_csv_fieldnames, *extra_csv_fieldnames)
96
92
 
97
93
  if self.dryrun:
98
94
  sys.stdout.write(
@@ -156,7 +152,7 @@ class RandomizationListImporter:
156
152
  index = 0
157
153
  with self.randomizationlist_path.open(mode="r") as csvfile:
158
154
  reader = csv.DictReader(csvfile)
159
- for index, row in enumerate(reader):
155
+ for index, _ in enumerate(reader):
160
156
  if index == 0:
161
157
  continue
162
158
  if index == 0:
@@ -178,11 +174,11 @@ class RandomizationListImporter:
178
174
  elif index == 1:
179
175
  if self.dryrun:
180
176
  row_as_dict = {k: v for k, v in row.items()}
181
- print(" --> First row:")
182
- print(f" --> {list(row_as_dict.keys())}")
183
- print(f" --> {list(row_as_dict.values())}")
177
+ sys.stdout.write(" --> First row:\n")
178
+ sys.stdout.write(f" --> {list(row_as_dict.keys())}\n")
179
+ sys.stdout.write(f" --> {list(row_as_dict.values())}\n")
184
180
  obj = self.randomizer_model_cls(**self.get_import_options(row))
185
- pprint(obj.__dict__)
181
+ pprint(obj.__dict__) # noqa: T203
186
182
  else:
187
183
  break
188
184
 
@@ -223,12 +219,15 @@ class RandomizationListImporter:
223
219
  sid_count = self.sid_count_for_tests
224
220
  else:
225
221
  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):
222
+ with self.randomizationlist_path.open(mode="r") as f:
223
+ reader = csv.DictReader(f)
224
+ all_rows = [{k: v.strip() for k, v in row.items() if k} for row in reader]
225
+ sorted_rows = sorted(
226
+ all_rows, key=lambda row: (row.get("site_name", ""), row.get("sid", ""))
227
+ )
228
+ for row in tqdm(sorted_rows, total=sid_count):
229
229
  if self.sid_count_for_tests and len(objs) == self.sid_count_for_tests:
230
230
  break
231
- row = {k: v.strip() for k, v in row.items()}
232
231
  try:
233
232
  self.randomizer_model_cls.objects.get(sid=row["sid"])
234
233
  except ObjectDoesNotExist:
@@ -300,7 +299,7 @@ class RandomizationListImporter:
300
299
  **self.get_extra_import_options(row),
301
300
  )
302
301
 
303
- def get_extra_import_options(self, row):
302
+ def get_extra_import_options(self, row): # noqa: ARG002
304
303
  return {}
305
304
 
306
305
  @staticmethod
@@ -314,9 +313,9 @@ class RandomizationListImporter:
314
313
  """Returns the site name or raises"""
315
314
  try:
316
315
  site_name = self.get_site_names()[row["site_name"].lower()]
317
- except KeyError:
316
+ except KeyError as e:
318
317
  raise RandomizationListImportError(
319
318
  f"Invalid site. Got {row['site_name']}. "
320
319
  f"Expected one of {self.get_site_names().keys()}"
321
- )
320
+ ) from e
322
321
  return site_name