clinicedc 2.0.20__py3-none-any.whl → 2.0.22__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 (64) hide show
  1. {clinicedc-2.0.20.dist-info → clinicedc-2.0.22.dist-info}/METADATA +1 -1
  2. {clinicedc-2.0.20.dist-info → clinicedc-2.0.22.dist-info}/RECORD +64 -64
  3. edc_appointment/utils.py +4 -5
  4. edc_pdf_reports/report.py +2 -2
  5. edc_reportable/reference_range_evaluator.py +1 -2
  6. edc_subject_dashboard/middleware.py +2 -3
  7. edc_subject_dashboard/requisition_labels.py +2 -2
  8. edc_subject_dashboard/requisition_report.py +10 -6
  9. edc_subject_dashboard/templatetags/edc_subject_dashboard_extras.py +33 -33
  10. edc_subject_dashboard/view_mixins/subject_visit_view_mixin.py +1 -1
  11. edc_subject_dashboard/view_utils/crf_button.py +5 -4
  12. edc_subject_dashboard/view_utils/go_to_forms_button.py +1 -2
  13. edc_subject_dashboard/view_utils/subject_screening_button.py +2 -6
  14. edc_subject_dashboard/views/base_requisition_view.py +2 -3
  15. edc_subject_dashboard/views/refresh_appointments_view.py +6 -6
  16. edc_subject_dashboard/views/requisition_print_actions_view.py +5 -5
  17. edc_subject_dashboard/views/requisition_verify_actions_view.py +1 -1
  18. edc_timepoint/form_mixin.py +3 -3
  19. edc_timepoint/timepoint.py +1 -1
  20. edc_timepoint/timepoint_collection.py +3 -3
  21. edc_transfer/action_items.py +1 -2
  22. edc_transfer/form_validators.py +2 -2
  23. edc_unblinding/action_items.py +2 -4
  24. edc_unblinding/admin/autocomplete_admin.py +2 -2
  25. edc_unblinding/admin/unblinding_request_admin.py +2 -2
  26. edc_unblinding/admin/unblinding_review_admin.py +2 -2
  27. edc_unblinding/auths.py +11 -5
  28. edc_unblinding/models/unblinding_review.py +2 -2
  29. edc_utils/age.py +7 -3
  30. edc_utils/celery.py +3 -3
  31. edc_utils/context_processors_check.py +1 -1
  32. edc_utils/get_datetime_from_env.py +1 -1
  33. edc_utils/get_static_file.py +24 -14
  34. edc_utils/logging_filters/ignore_specific_ip_disallowed_host.py +1 -1
  35. edc_utils/message_in_queue.py +1 -4
  36. edc_utils/show_urls.py +1 -1
  37. edc_utils/text.py +1 -1
  38. edc_view_utils/model_button.py +2 -3
  39. edc_visit_schedule/exceptions.py +3 -3
  40. edc_visit_schedule/management/commands/find_invalid_onschedules.py +6 -4
  41. edc_visit_schedule/modelform_mixins/crf/visit_schedule_crf_modelform_mixin.py +1 -1
  42. edc_visit_schedule/modelform_mixins/off_schedule_modelform_mixin.py +3 -3
  43. edc_visit_schedule/modelform_mixins/visit_schedule_non_crf_modelform_mixin.py +1 -1
  44. edc_visit_schedule/models/signals.py +3 -8
  45. edc_visit_schedule/ordered_collection.py +4 -8
  46. edc_visit_schedule/post_migrate_signals.py +2 -2
  47. edc_visit_schedule/schedule/schedule.py +3 -7
  48. edc_visit_schedule/schedule/window.py +1 -1
  49. edc_visit_schedule/simple_model_validator.py +4 -4
  50. edc_visit_schedule/site_visit_schedules.py +12 -16
  51. edc_visit_schedule/subject_schedule.py +8 -8
  52. edc_visit_schedule/system_checks.py +3 -3
  53. edc_visit_schedule/templatetags/edc_visit_schedule_extras.py +2 -2
  54. edc_visit_schedule/utils.py +13 -14
  55. edc_visit_schedule/view_mixins.py +1 -1
  56. edc_visit_schedule/visit/crf.py +7 -6
  57. edc_visit_schedule/visit/forms_collection.py +2 -2
  58. edc_visit_schedule/visit/requisition.py +3 -3
  59. edc_visit_schedule/visit/visit.py +30 -30
  60. edc_visit_schedule/visit/window_period.py +3 -2
  61. edc_visit_schedule/visit_schedule/schedules_collection.py +5 -3
  62. edc_visit_schedule/visit_schedule/visit_schedule.py +5 -5
  63. {clinicedc-2.0.20.dist-info → clinicedc-2.0.22.dist-info}/WHEEL +0 -0
  64. {clinicedc-2.0.20.dist-info → clinicedc-2.0.22.dist-info}/licenses/LICENSE +0 -0
@@ -63,8 +63,7 @@ class GotToFormsButton(RelatedVisitButton):
63
63
  extra_kwargs=self.extra_kwargs,
64
64
  )
65
65
 
66
- url = "?".join([f"{nq.next_url}", nq.querystring])
67
- return url
66
+ return "?".join([f"{nq.next_url}", nq.querystring])
68
67
 
69
68
  @property
70
69
  def disabled(self) -> str:
@@ -48,14 +48,10 @@ class SubjectScreeningButton(DashboardModelButton):
48
48
 
49
49
  @property
50
50
  def reverse_kwargs(self) -> dict[str, str | UUID]:
51
- kwargs = dict(screening_identifier=self.model_obj.screening_identifier)
52
- return kwargs
51
+ return dict(screening_identifier=self.model_obj.screening_identifier)
53
52
 
54
53
  @property
55
54
  def title(self) -> str:
56
- if self.perms.view_only or self.model_obj.consented:
57
- title = _("View")
58
- else:
59
- title = _("Edit")
55
+ title = _("View") if self.perms.view_only or self.model_obj.consented else _("Edit")
60
56
  verbose_name = self.model_cls._meta.verbose_name.lower()
61
57
  return f"{title} {verbose_name}"
@@ -11,10 +11,9 @@ class BaseRequisitionView(LoginRequiredMixin, PrintersMixin, View):
11
11
  success_url_name = "subject_dashboard_url"
12
12
 
13
13
  def get_success_url(self):
14
- success_url = url_names.get(self.success_url_name)
15
- return success_url
14
+ return url_names.get(self.success_url_name)
16
15
 
17
- def get(self, request, *args, **kwargs):
16
+ def get(self, request, *args, **kwargs): # noqa: ARG002
18
17
  url = reverse("edc_lab_dashboard:home_url")
19
18
  return HttpResponseRedirect(url)
20
19
 
@@ -14,10 +14,10 @@ class RefreshAppointmentsView(LoginRequiredMixin, View):
14
14
 
15
15
  def refresh_appointments(
16
16
  self,
17
- subject_identifier: str = None,
18
- visit_schedule_name: str = None,
19
- schedule_name: str = None,
20
- **kwargs,
17
+ subject_identifier: str,
18
+ visit_schedule_name: str,
19
+ schedule_name: str,
20
+ **kwargs, # noqa: ARG002
21
21
  ) -> tuple[str, str]:
22
22
  return refresh_appointments(
23
23
  subject_identifier=subject_identifier,
@@ -26,8 +26,8 @@ class RefreshAppointmentsView(LoginRequiredMixin, View):
26
26
  request=self.request,
27
27
  )
28
28
 
29
- def get(self, request, *args, **kwargs):
30
- subject_identifier, status = self.refresh_appointments(**kwargs)
29
+ def get(self, request, *args, **kwargs): # noqa: ARG002
30
+ subject_identifier, _ = self.refresh_appointments(**kwargs)
31
31
  url_name = url_names.get("subject_dashboard_url")
32
32
  args = (subject_identifier,)
33
33
  url = reverse(url_name, args=args)
@@ -26,14 +26,14 @@ class RequisitionPrintActionsView(BaseRequisitionView):
26
26
  checkbox_name = "selected_panel_names"
27
27
 
28
28
  def __init__(self, **kwargs):
29
- self._appointment = None
30
- self._selected_panel_names = []
31
- self._requisition_metadata = None
29
+ self._appointment: Appointment | None = None
30
+ self._selected_panel_names: list[str] | None = []
31
+ self._requisition_metadata: RequisitionMetadata | None = None
32
32
  self._requisition_model_cls = None
33
- self.consignee = None
33
+ self.consignee: Consignee | None = None
34
34
  super().__init__(**kwargs)
35
35
 
36
- def post(self, request, *args, **kwargs):
36
+ def post(self, request, *args, **kwargs): # noqa: ARG002
37
37
  response = None
38
38
  if self.selected_panel_names:
39
39
  if self.request.POST.get("submit") in [
@@ -8,7 +8,7 @@ from .base_requisition_view import BaseRequisitionView
8
8
  class RequisitionVerifyActionsView(BaseRequisitionView):
9
9
  requisition_verifier_cls = RequisitionVerifier
10
10
 
11
- def post(self, request, *args, **kwargs):
11
+ def post(self, request, *args, **kwargs): # noqa: ARG002
12
12
  alert = 1
13
13
  error = 1
14
14
  subject_identifier = request.POST.get("subject_identifier")
@@ -7,15 +7,15 @@ from .constants import CLOSED_TIMEPOINT
7
7
 
8
8
  class TimepointFormMixin:
9
9
  def clean(self):
10
- cleaned_data = super(TimepointFormMixin, self).clean()
10
+ cleaned_data = super().clean()
11
11
  app_config = django_apps.get_app_config("edc_timepoint")
12
12
  try:
13
13
  app_config.timepoints[self._meta.model._meta.label_lower]
14
- except KeyError:
14
+ except KeyError as e:
15
15
  raise ImproperlyConfigured(
16
16
  "ModelForm uses a model that is not a timepoint. "
17
17
  f"Got {self._meta.model._meta.label_lower}."
18
- )
18
+ ) from e
19
19
  timepoint_status = cleaned_data.get("timepoint_status")
20
20
  if timepoint_status == CLOSED_TIMEPOINT:
21
21
  raise forms.ValidationError(
@@ -1,7 +1,7 @@
1
1
  from django.apps import apps as django_apps
2
2
 
3
3
 
4
- class TimepointClosed(Exception):
4
+ class TimepointClosed(Exception): # noqa: N818
5
5
  pass
6
6
 
7
7
 
@@ -3,7 +3,7 @@ from django.apps import apps as django_apps
3
3
  from .timepoint import Timepoint
4
4
 
5
5
 
6
- class TimepointDoesNotExist(Exception):
6
+ class TimepointDoesNotExist(Exception): # noqa: N818
7
7
  pass
8
8
 
9
9
 
@@ -34,10 +34,10 @@ class TimepointCollection:
34
34
  """Returns the timepoint class for this model."""
35
35
  try:
36
36
  timepoint = self._timepoints[model]
37
- except KeyError:
37
+ except KeyError as e:
38
38
  raise TimepointDoesNotExist(
39
39
  f"No timepoint has been configured with {model}. "
40
40
  "See AppConfig for edc_timepoint. Hint: Perhaps you are using a custom "
41
41
  "`Appointment` model?"
42
- )
42
+ ) from e
43
43
  return timepoint
@@ -19,5 +19,4 @@ class SubjectTransferAction(ActionWithNotification):
19
19
  singleton = True
20
20
 
21
21
  def get_next_actions(self):
22
- next_actions = [END_OF_STUDY_ACTION]
23
- return next_actions
22
+ return [END_OF_STUDY_ACTION]
@@ -48,7 +48,7 @@ class SubjectTransferFormValidatorMixin:
48
48
  subject_transfer_obj = django_apps.get_model(
49
49
  self.subject_transfer_model
50
50
  ).objects.get(subject_identifier=self.subject_identifier)
51
- except ObjectDoesNotExist:
51
+ except ObjectDoesNotExist as e:
52
52
  if (
53
53
  self.cleaned_data.get(self.offschedule_reason_field)
54
54
  and self.cleaned_data.get(self.offschedule_reason_field).name
@@ -59,7 +59,7 @@ class SubjectTransferFormValidatorMixin:
59
59
  f"`{self.subject_transfer_model_cls._meta.verbose_name}` "
60
60
  "form first."
61
61
  )
62
- raise forms.ValidationError({self.offschedule_reason_field: msg})
62
+ raise forms.ValidationError({self.offschedule_reason_field: msg}) from e
63
63
  else:
64
64
  if self.cleaned_data.get(self.subject_transfer_date_field) and (
65
65
  subject_transfer_obj.transfer_date
@@ -19,12 +19,11 @@ class UnblindingRequestAction(ActionWithNotification):
19
19
 
20
20
  def get_next_actions(self):
21
21
  next_actions = []
22
- next_actions = self.append_to_next_if_required(
22
+ return self.append_to_next_if_required(
23
23
  next_actions=next_actions,
24
24
  action_name=UNBLINDING_REVIEW_ACTION,
25
25
  required=self.reference_obj.approved == TBD,
26
26
  )
27
- return next_actions
28
27
 
29
28
 
30
29
  class UnblindingReviewAction(ActionWithNotification):
@@ -43,9 +42,8 @@ class UnblindingReviewAction(ActionWithNotification):
43
42
 
44
43
  def get_next_actions(self):
45
44
  next_actions = []
46
- next_actions = self.append_to_next_if_required(
45
+ return self.append_to_next_if_required(
47
46
  next_actions=next_actions,
48
47
  action_name=END_OF_STUDY_ACTION,
49
48
  required=self.reference_obj.approved == YES,
50
49
  )
51
- return next_actions
@@ -13,7 +13,7 @@ class UnblindingRequestorUserAdmin(BaseUserAdmin):
13
13
  ordering = ("first_name", "last_name")
14
14
  search_fields = ("first_name", "last_name", "username", "email")
15
15
 
16
- inlines = []
16
+ inlines = ()
17
17
 
18
18
  populate_data_dictionary = False
19
19
 
@@ -30,7 +30,7 @@ class UnblindingReviewerUserAdmin(BaseUserAdmin):
30
30
  ordering = ("first_name", "last_name")
31
31
  search_fields = ("first_name", "last_name", "username", "email")
32
32
 
33
- inlines = []
33
+ inlines = ()
34
34
 
35
35
  populate_data_dictionary = False
36
36
 
@@ -37,11 +37,11 @@ class UnblindingRequestAdmin(ModelAdminSubjectDashboardMixin, SimpleHistoryAdmin
37
37
  audit_fieldset_tuple,
38
38
  )
39
39
 
40
- autocomplete_fields = ["requestor"]
40
+ autocomplete_fields = ("requestor",)
41
41
 
42
42
  readonly_fields = ("approved", "approved_datetime")
43
43
 
44
- radio_fields = {"approved": admin.VERTICAL}
44
+ radio_fields = {"approved": admin.VERTICAL} # noqa: RUF012
45
45
 
46
46
  list_display = (
47
47
  "subject_identifier",
@@ -21,7 +21,7 @@ class UnblindingReviewAdmin(ModelAdminSubjectDashboardMixin, SimpleHistoryAdmin)
21
21
  audit_fieldset_tuple,
22
22
  )
23
23
 
24
- autocomplete_fields = ["reviewer"]
24
+ autocomplete_fields = ("reviewer",)
25
25
 
26
26
  list_display = (
27
27
  "subject_identifier",
@@ -33,4 +33,4 @@ class UnblindingReviewAdmin(ModelAdminSubjectDashboardMixin, SimpleHistoryAdmin)
33
33
  "created",
34
34
  )
35
35
 
36
- radio_fields = {"approved": admin.VERTICAL}
36
+ radio_fields = {"approved": admin.VERTICAL} # noqa: RUF012
edc_unblinding/auths.py CHANGED
@@ -1,4 +1,6 @@
1
- from edc_auth.site_auths import site_auths
1
+ from contextlib import suppress
2
+
3
+ from edc_auth.site_auths import GroupAlreadyExists, RoleAlreadyExists, site_auths
2
4
 
3
5
  from .auth_objects import (
4
6
  UNBLINDING_REQUESTORS,
@@ -11,10 +13,14 @@ from .auth_objects import (
11
13
 
12
14
 
13
15
  def update_site_auths() -> None:
14
- site_auths.add_group(*unblinding_requestors, name=UNBLINDING_REQUESTORS)
15
- site_auths.add_group(*unblinding_reviewers, name=UNBLINDING_REVIEWERS)
16
- site_auths.add_role(UNBLINDING_REQUESTORS, name=UNBLINDING_REQUESTORS_ROLE)
17
- site_auths.add_role(UNBLINDING_REVIEWERS, name=UNBLINDING_REVIEWERS_ROLE)
16
+ with suppress(GroupAlreadyExists):
17
+ site_auths.add_group(*unblinding_requestors, name=UNBLINDING_REQUESTORS)
18
+ with suppress(GroupAlreadyExists):
19
+ site_auths.add_group(*unblinding_reviewers, name=UNBLINDING_REVIEWERS)
20
+ with suppress(RoleAlreadyExists):
21
+ site_auths.add_role(UNBLINDING_REQUESTORS, name=UNBLINDING_REQUESTORS_ROLE)
22
+ with suppress(RoleAlreadyExists):
23
+ site_auths.add_role(UNBLINDING_REVIEWERS, name=UNBLINDING_REVIEWERS_ROLE)
18
24
 
19
25
 
20
26
  update_site_auths()
@@ -3,7 +3,7 @@ from django.utils import timezone
3
3
 
4
4
  from edc_action_item.models.action_model_mixin import ActionModelMixin
5
5
  from edc_constants.choices import YES_NO_TBD
6
- from edc_constants.constants import TBD
6
+ from edc_constants.constants import NULL_STRING, TBD
7
7
  from edc_identifier.managers import SubjectIdentifierManager
8
8
  from edc_identifier.model_mixins import NonUniqueSubjectIdentifierFieldMixin
9
9
  from edc_model.models.base_uuid_model import BaseUuidModel
@@ -36,7 +36,7 @@ class UnblindingReview(
36
36
 
37
37
  approved = models.CharField(max_length=15, default=TBD, choices=YES_NO_TBD)
38
38
 
39
- comment = models.TextField(verbose_name="Comment", default="")
39
+ comment = models.TextField(verbose_name="Comment", default=NULL_STRING)
40
40
 
41
41
  objects = SubjectIdentifierManager()
42
42
 
edc_utils/age.py CHANGED
@@ -17,6 +17,10 @@ class AgeFormatError(Exception):
17
17
  pass
18
18
 
19
19
 
20
+ TWO_MONTHS = 2
21
+ TWELVE_MONTHS = 12
22
+
23
+
20
24
  def get_dob(age_in_years: int, now: date | datetime | None = None) -> date:
21
25
  """Returns a DoB for the given age relative to now.
22
26
 
@@ -65,12 +69,12 @@ def formatted_age(
65
69
  age_delta = age(born, reference_dt or timezone.now())
66
70
  if age_delta.years == 0 and age_delta.months <= 0:
67
71
  age_as_str = f"{age_delta.days}d"
68
- elif age_delta.years == 0 and 0 < age_delta.months <= 2:
72
+ elif age_delta.years == 0 and 0 < age_delta.months <= TWO_MONTHS:
69
73
  age_as_str = f"{age_delta.months}m{age_delta.days}d"
70
- elif age_delta.years == 0 and age_delta.months > 2:
74
+ elif age_delta.years == 0 and age_delta.months > TWO_MONTHS:
71
75
  age_as_str = f"{age_delta.months}m"
72
76
  elif age_delta.years == 1:
73
- m = age_delta.months + 12
77
+ m = age_delta.months + TWELVE_MONTHS
74
78
  age_as_str = f"{m}m"
75
79
  else:
76
80
  age_as_str = f"{age_delta.years}y"
edc_utils/celery.py CHANGED
@@ -1,3 +1,5 @@
1
+ import contextlib
2
+
1
3
  from celery import current_app
2
4
  from celery.result import AsyncResult
3
5
  from django.conf import settings
@@ -33,10 +35,8 @@ def get_task_result(obj) -> AsyncResult | None:
33
35
  """
34
36
  result = None
35
37
  if obj.task_id:
36
- try:
38
+ with contextlib.suppress(TypeError, ValueError):
37
39
  result = AsyncResult(str(obj.task_id))
38
- except (TypeError, ValueError):
39
- pass
40
40
  return result
41
41
 
42
42
 
@@ -10,7 +10,7 @@ def edc_context_processors_check(
10
10
  if context_processor_name not in template_config.get("OPTIONS").get(
11
11
  "context_processors"
12
12
  ):
13
- errors.append(
13
+ errors.append( # noqa: PERF401
14
14
  Error(
15
15
  "Missing item in TEMPLATE.OPTIONS.context_processors. "
16
16
  f"Expected `{context_processor_name}`.",
@@ -10,7 +10,7 @@ def get_datetime_from_env(
10
10
  minute: int,
11
11
  second: int,
12
12
  time_zone: str,
13
- closing_date: date = None,
13
+ closing_date: date | None = None,
14
14
  ) -> datetime:
15
15
  if closing_date:
16
16
  hour = hour or 23
@@ -1,25 +1,35 @@
1
- import os
1
+ from pathlib import Path
2
2
  from urllib.error import URLError
3
+ from urllib.parse import urljoin
3
4
  from urllib.request import urlretrieve
4
5
 
5
6
  from django.conf import settings
6
7
 
7
8
 
8
9
  def get_static_file(app_label: str, filename: str) -> str:
9
- path = os.path.join(settings.STATIC_ROOT or "", app_label, filename)
10
- if os.path.isfile(path):
10
+ path = Path(settings.STATIC_ROOT or "") / app_label / filename
11
+ # path as os path
12
+ if path.is_file():
11
13
  try:
12
- with open(path):
14
+ with path.open("r"):
13
15
  pass
14
16
  except FileNotFoundError:
15
- path = os.path.join(f"https://{settings.STATIC_URL}", app_label, filename)
16
- try:
17
- urlretrieve(path) # nosec B310
18
- except URLError:
19
- raise FileNotFoundError(
20
- f"Static file not found. Tried "
21
- f"STATIC_ROOT ({settings.STATIC_ROOT}) and "
22
- f"STATIC_URL ({settings.STATIC_URL}). "
23
- f"Got {app_label}/{filename}."
24
- )
17
+ path = None
18
+ else:
19
+ path = None
20
+
21
+ # path as a url
22
+ if not path:
23
+ path = urljoin(f"https://{settings.STATIC_URL}", app_label, filename)
24
+ try:
25
+ urlretrieve(path) # noqa: S310
26
+ except URLError:
27
+ path = None
28
+ if not path:
29
+ raise FileNotFoundError(
30
+ f"Static file not found. Tried "
31
+ f"STATIC_ROOT ({settings.STATIC_ROOT}) and "
32
+ f"STATIC_URL ({settings.STATIC_URL}). "
33
+ f"Got {app_label}/{filename}."
34
+ )
25
35
  return path
@@ -7,7 +7,7 @@ class IgnoreSpecificIPDisallowedHost:
7
7
 
8
8
  def filter(self, record):
9
9
  if record.exc_info:
10
- exc_type, exc_value, _ = record.exc_info
10
+ _, exc_value, _ = record.exc_info
11
11
  if isinstance(exc_value, DisallowedHost):
12
12
  request = getattr(record, "request", None)
13
13
  if request and request.META.get("REMOTE_ADDR") == self.ip_to_ignore:
@@ -3,7 +3,4 @@ from django.contrib.messages import get_messages
3
3
 
4
4
  def message_in_queue(request, message_text):
5
5
  storage = get_messages(request)
6
- for message in storage:
7
- if message.message == message_text:
8
- return True
9
- return False
6
+ return any(message.message == message_text for message in storage)
edc_utils/show_urls.py CHANGED
@@ -110,5 +110,5 @@ def extract_views_from_urlpatterns(urlpatterns, base="", namespace=None): # noq
110
110
  )
111
111
  )
112
112
  else:
113
- raise TypeError("%s does not appear to be a urlpattern object" % p)
113
+ raise TypeError(f"{p} does not appear to be a urlpattern object")
114
114
  return views
edc_utils/text.py CHANGED
@@ -15,7 +15,7 @@ def get_safe_random_string(length=12, safe=None, allowed_chars=None):
15
15
  )
16
16
  if safe:
17
17
  allowed_chars = "ABCDEFGHKMNPRTUVWXYZ2346789"
18
- return "".join([random.choice(allowed_chars) for _ in range(length)]) # nosec B311
18
+ return "".join([random.choice(allowed_chars) for _ in range(length)]) # nosec B311 # noqa: S311
19
19
 
20
20
 
21
21
  def convert_php_dateformat(php_format_string):
@@ -85,9 +85,8 @@ class ModelButton:
85
85
  self._action = VIEW
86
86
  if not self.model_obj:
87
87
  self._action = ADD
88
- elif self.model_obj:
89
- if self.perms.change:
90
- self._action = CHANGE
88
+ elif self.model_obj and self.perms.change:
89
+ self._action = CHANGE
91
90
  return self._action
92
91
 
93
92
  @property
@@ -34,7 +34,7 @@ class UnknownSubjectError(Exception):
34
34
  pass
35
35
 
36
36
 
37
- class InvalidOffscheduleDate(Exception):
37
+ class InvalidOffscheduleDate(Exception): # noqa: N818
38
38
  pass
39
39
 
40
40
 
@@ -54,11 +54,11 @@ class SiteVisitScheduleError(Exception):
54
54
  pass
55
55
 
56
56
 
57
- class RegistryNotLoaded(Exception):
57
+ class RegistryNotLoaded(Exception): # noqa: N818
58
58
  pass
59
59
 
60
60
 
61
- class AlreadyRegisteredVisitSchedule(Exception):
61
+ class AlreadyRegisteredVisitSchedule(Exception): # noqa: N818
62
62
  pass
63
63
 
64
64
 
@@ -1,3 +1,5 @@
1
+ import sys
2
+
1
3
  from django.apps import apps as django_apps
2
4
  from django.core.exceptions import ObjectDoesNotExist
3
5
  from django.core.management.base import BaseCommand
@@ -15,12 +17,12 @@ class Command(BaseCommand):
15
17
  help="Delete invalid OnSchedule model instances",
16
18
  )
17
19
 
18
- def handle(self, *args, **options):
20
+ def handle(self, *args, **options): # noqa: ARG002
19
21
  allow_delete = False
20
22
  if options["delete"]:
21
23
  allow_delete = True
22
24
  else:
23
- print("Checking only")
25
+ sys.stdout.write("Checking only\n")
24
26
  subject_schedule_history_cls = django_apps.get_model(
25
27
  "edc_visit_schedule.subjectschedulehistory"
26
28
  )
@@ -43,6 +45,6 @@ class Command(BaseCommand):
43
45
  f"{onschedule_obj.subject_identifier} is invalid."
44
46
  )
45
47
  if allow_delete:
46
- msg = f"{msg} deleted."
48
+ msg = f"{msg} deleted.\n"
47
49
  onschedule_obj.delete()
48
- print(msg)
50
+ sys.stdout.write(msg)
@@ -58,7 +58,7 @@ class VisitScheduleCrfModelFormMixin:
58
58
  ),
59
59
  )
60
60
  except (NotOnScheduleError, NotOnScheduleForDateError) as e:
61
- raise forms.ValidationError(str(e))
61
+ raise forms.ValidationError(str(e)) from e
62
62
 
63
63
  def report_datetime_within_schedule_datetimes(self) -> None:
64
64
  if self.report_datetime:
@@ -28,7 +28,7 @@ class OffScheduleModelFormMixin(VisitScheduleNonCrfModelFormMixin):
28
28
  update=False,
29
29
  )
30
30
  except InvalidOffscheduleDate as e:
31
- raise forms.ValidationError(e)
31
+ raise forms.ValidationError(e) from e
32
32
  self.validate_visit_tracking_reports()
33
33
  return cleaned_data
34
34
 
@@ -50,11 +50,11 @@ class OffScheduleModelFormMixin(VisitScheduleNonCrfModelFormMixin):
50
50
  return True
51
51
 
52
52
  class Meta:
53
- help_text = {
53
+ help_text = { # noqa: RUF012
54
54
  "subject_identifier": "(read-only)",
55
55
  "action_identifier": "(read-only)",
56
56
  }
57
- widgets = {
57
+ widgets = { # noqa: RUF012
58
58
  "subject_identifier": forms.TextInput(attrs={"readonly": "readonly"}),
59
59
  "action_identifier": forms.TextInput(attrs={"readonly": "readonly"}),
60
60
  }
@@ -76,7 +76,7 @@ class VisitScheduleNonCrfModelFormMixin:
76
76
  compare_as_datetimes=self.offschedule_compare_dates_as_datetimes,
77
77
  )
78
78
  except (NotOnScheduleError, NotOnScheduleForDateError) as e:
79
- raise forms.ValidationError(str(e))
79
+ raise forms.ValidationError(str(e)) from e
80
80
 
81
81
  def report_datetime_within_schedule_datetimes(self) -> None:
82
82
  if self.report_datetime:
@@ -8,14 +8,9 @@ from ..site_visit_schedules import SiteVisitScheduleError, site_visit_schedules
8
8
 
9
9
  @receiver(post_save, weak=False, dispatch_uid="offschedule_model_on_post_save")
10
10
  def offschedule_model_on_post_save(sender, instance, raw, update_fields, **kwargs):
11
- if not raw and not update_fields:
12
- if isinstance(instance, (OffScheduleModelMixin,)):
13
- _, schedule = site_visit_schedules.get_by_offschedule_model(
14
- instance._meta.label_lower
15
- )
16
- schedule.take_off_schedule(
17
- instance.subject_identifier, instance.offschedule_datetime
18
- )
11
+ if not raw and not update_fields and isinstance(instance, (OffScheduleModelMixin,)):
12
+ _, schedule = site_visit_schedules.get_by_offschedule_model(instance._meta.label_lower)
13
+ schedule.take_off_schedule(instance.subject_identifier, instance.offschedule_datetime)
19
14
 
20
15
 
21
16
  @receiver(post_delete, weak=False, dispatch_uid="offschedule_model_on_post_delete")
@@ -1,18 +1,17 @@
1
1
  import itertools
2
- from collections import OrderedDict
3
2
 
4
3
 
5
- class OrderedCollection(OrderedDict):
4
+ class OrderedCollection(dict):
6
5
  key: str = None # key name in dictionary key/value pair
7
6
  ordering_attr: str = None # value.attrname to order dictionary on.
8
7
 
9
- def update(self, *args, **kwargs) -> None:
8
+ def update(self, **kwargs) -> None:
10
9
  """Updates and reorders."""
11
10
 
12
11
  def key_order(v):
13
12
  return getattr(v, self.ordering_attr)
14
13
 
15
- super().update(*args, **kwargs)
14
+ super().update(**kwargs)
16
15
  od = self.copy()
17
16
  self.clear()
18
17
  super().update(**{getattr(v, self.key): v for v in sorted(od.values(), key=key_order)})
@@ -40,10 +39,7 @@ class OrderedCollection(OrderedDict):
40
39
  return self.get(self._iter_keys(key=key))
41
40
 
42
41
  def _iter_keys(self, key=None, reverse=None):
43
- if reverse:
44
- seq = reversed(self.keys())
45
- else:
46
- seq = iter(self.keys())
42
+ seq = reversed(self.keys()) if reverse else iter(self.keys())
47
43
  keys = itertools.dropwhile(lambda x: x != key, seq)
48
44
  try:
49
45
  k = next(keys)
@@ -7,8 +7,8 @@ style = color_style()
7
7
 
8
8
 
9
9
  def populate_visit_schedule(sender=None, **kwargs):
10
- from .models import VisitSchedule
11
- from .site_visit_schedules import site_visit_schedules
10
+ from .models import VisitSchedule # noqa: PLC0415
11
+ from .site_visit_schedules import site_visit_schedules # noqa: PLC0415
12
12
 
13
13
  sys.stdout.write(style.MIGRATE_HEADING("Populating visit schedule:\n"))
14
14
  if getattr(settings, "EDC_VISIT_SCHEDULE_POPULATE_VISIT_SCHEDULE", True):