clinicedc 2.0.38__py3-none-any.whl → 2.0.40__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.
- {clinicedc-2.0.38.dist-info → clinicedc-2.0.40.dist-info}/METADATA +3 -12
- {clinicedc-2.0.38.dist-info → clinicedc-2.0.40.dist-info}/RECORD +146 -153
- {clinicedc-2.0.38.dist-info → clinicedc-2.0.40.dist-info}/WHEEL +1 -1
- edc_adverse_event/dashboard_urls.py +2 -0
- edc_adverse_event/middleware.py +7 -6
- edc_adverse_event/navbars.py +4 -8
- edc_adverse_event/templates/edc_adverse_event/tmg/tmg_ae_listboard_result.html +27 -23
- edc_adverse_event/templatetags/edc_adverse_event_extras.py +21 -34
- edc_adverse_event/urls.py +14 -6
- edc_adverse_event/view_mixins/ae/ae_listboard_view_mixin.py +6 -8
- edc_adverse_event/view_mixins/ae/death_report_listboard_view_mixin.py +2 -4
- edc_adverse_event/view_mixins/tmg/tmg_ae_listboard_view_mixin.py +9 -7
- edc_adverse_event/views/home_view.py +1 -2
- edc_adverse_event/views/tmg/death_listboard_view.py +8 -6
- edc_adverse_event/views/tmg/home_view.py +4 -3
- edc_adverse_event/views/tmg/summary_listboard_view.py +4 -4
- edc_appointment/utils.py +3 -6
- edc_appointment/views/unscheduled_appointment_view.py +1 -1
- edc_consent/form_validators/consent_definition_form_validator_mixin.py +5 -2
- edc_consent/model_mixins/consent_version_model_mixin.py +1 -1
- edc_consent/navbars.py +2 -1
- edc_crf/model_mixins/crf_model_mixin.py +5 -1
- edc_crf/model_mixins/crf_no_manager_model_mixin.py +2 -2
- edc_crf/model_mixins/singleton_crf_model_mixin.py +1 -1
- edc_dashboard/middleware.py +10 -16
- edc_dashboard/middleware_mixins.py +10 -0
- edc_dashboard/navbars.py +1 -1
- edc_dashboard/url_config.py +50 -31
- edc_dashboard/url_names.py +23 -17
- edc_dashboard/utils.py +4 -4
- edc_dashboard/view_mixins/template_request_context_mixin.py +5 -8
- edc_dashboard/view_mixins/url_request_context_mixin.py +38 -26
- edc_dashboard/views/administration_view.py +2 -2
- edc_dashboard/views/dashboard_view.py +5 -10
- edc_data_manager/handlers/handlers.py +17 -5
- edc_data_manager/models/query_rule.py +7 -7
- edc_data_manager/navbar_item.py +1 -1
- edc_data_manager/rule/query_rule_wrapper.py +1 -1
- edc_data_manager/rule/rule_runner.py +6 -6
- edc_device/navbars.py +1 -1
- edc_export/navbars.py +2 -2
- edc_glucose/model_mixin_factories/fasting_model_mixin_factory.py +1 -1
- edc_identifier/identifier.py +6 -9
- edc_lab_dashboard/dashboard_urls.py +7 -5
- edc_lab_dashboard/middleware.py +10 -17
- edc_lab_dashboard/navbars.py +9 -9
- edc_lab_dashboard/templates/edc_lab_dashboard/listboard/tags/status_column.html +7 -0
- edc_lab_dashboard/urls.py +2 -5
- edc_lab_dashboard/view_mixins/form_action_view_mixin.py +1 -2
- edc_lab_dashboard/views/action_views/action_view.py +6 -6
- edc_lab_dashboard/views/action_views/aliquot_view.py +1 -1
- edc_lab_dashboard/views/action_views/manage_box_item_view.py +2 -3
- edc_lab_dashboard/views/action_views/manage_manifest_view.py +1 -1
- edc_lab_dashboard/views/action_views/manifest_view.py +2 -2
- edc_lab_dashboard/views/action_views/pack_view.py +2 -2
- edc_lab_dashboard/views/action_views/process_view.py +1 -1
- edc_lab_dashboard/views/action_views/receive_view.py +1 -1
- edc_lab_dashboard/views/action_views/requisition_view.py +1 -1
- edc_lab_dashboard/views/action_views/verify_box_item_view.py +1 -1
- edc_lab_dashboard/views/listboard_views/manage_box_listboard_view.py +4 -5
- edc_lab_dashboard/views/listboard_views/manifest_listboard_view.py +5 -6
- edc_lab_dashboard/views/listboard_views/process_listboard_view.py +4 -5
- edc_lab_dashboard/views/listboard_views/receive_listboard_view.py +5 -6
- edc_lab_dashboard/views/listboard_views/verify_box_listboard_view.py +5 -6
- edc_label/navbars.py +1 -1
- edc_list_data/admin.py +3 -3
- edc_list_data/load_model_data.py +1 -1
- edc_list_data/management/commands/load_list_data.py +2 -2
- edc_list_data/site_list_data.py +4 -4
- edc_listboard/middleware.py +9 -8
- edc_listboard/templates/edc_listboard/listboard.html +1 -1
- edc_listboard/view_mixins/listboard_filter_view_mixin.py +1 -1
- edc_listboard/view_mixins/search_form_view_mixin.py +1 -1
- edc_listboard/views/listboard_view.py +16 -25
- edc_listboard/views/screen/screening_listboard_view.py +2 -2
- edc_listboard/views/subject/subject_listboard_view.py +2 -2
- edc_locator/forms/subject_locator_form_validator.py +2 -2
- edc_ltfu/action_items.py +1 -2
- edc_ltfu/forms/ltfu_form_validator_mixin.py +3 -3
- edc_ltfu/modeladmin_mixin.py +1 -1
- edc_ltfu/modelform_mixins.py +2 -2
- edc_metadata/admin/modeladmin_mixins.py +11 -9
- edc_metadata/management/commands/update_metadata.py +1 -1
- edc_metadata/management/commands/update_metadata_schedule_names.py +7 -7
- edc_metadata/management/commands/validate_entry_status.py +1 -1
- edc_metadata/management/commands/validate_rule_groups.py +1 -1
- edc_metadata/metadata/metadata_getter.py +3 -5
- edc_metadata/metadata_handler.py +5 -5
- edc_metadata/metadata_mixins/source_model_metadata_mixin.py +1 -1
- edc_metadata/metadata_refresher.py +1 -1
- edc_metadata/metadata_rules/crf/crf_rule.py +1 -1
- edc_metadata/metadata_rules/logic.py +3 -3
- edc_metadata/metadata_rules/persistant_singleton_mixin.py +2 -4
- edc_metadata/metadata_rules/requisition/requisition_rule_group.py +1 -1
- edc_metadata/metadata_rules/rule.py +4 -3
- edc_metadata/metadata_rules/rule_group.py +2 -2
- edc_metadata/metadata_rules/rule_group_meta_options.py +2 -2
- edc_metadata/metadata_rules/rule_group_metaclass.py +21 -22
- edc_metadata/metadata_rules/site.py +1 -1
- edc_metadata/metadata_updater.py +4 -3
- edc_metadata/model_mixins/creates/creates_metadata_model_mixin.py +3 -5
- edc_metadata/model_mixins/updates/updates_metadata_model_mixin.py +1 -1
- edc_metadata/next_form_getter.py +15 -19
- edc_metadata/offline_models.py +1 -1
- edc_metadata/requisition/requisition_metadata_handler.py +5 -5
- edc_metadata/update_metadata_on_schedule_change.py +2 -4
- edc_metadata/utils.py +1 -1
- edc_model/models/signals.py +7 -2
- edc_model_admin/mixins/model_admin_redirect_on_delete_mixin.py +4 -3
- edc_navbar/apps.py +0 -2
- edc_navbar/navbar.py +1 -1
- edc_navbar/navbar_item.py +29 -16
- edc_navbar/navbars.py +6 -19
- edc_navbar/site_navbars.py +6 -7
- edc_navbar/system_checks.py +3 -10
- edc_navbar/utils.py +14 -0
- edc_navbar/view_mixin.py +6 -9
- edc_pharmacy/navbars.py +1 -1
- edc_pharmacy/views/confirm_stock_from_queryset_view.py +3 -3
- edc_protocol/middleware.py +9 -13
- edc_protocol/navbars.py +1 -1
- edc_refusal/forms.py +1 -3
- edc_reportable/utils/convert_units.py +1 -1
- edc_review_dashboard/middleware.py +6 -3
- edc_review_dashboard/navbars.py +1 -2
- edc_review_dashboard/urls.py +3 -2
- edc_review_dashboard/views/subject_review_listboard_view.py +4 -2
- edc_subject_dashboard/dashboard_templates.py +1 -3
- edc_subject_dashboard/dashboard_urls.py +8 -0
- edc_subject_dashboard/middleware.py +10 -7
- edc_subject_dashboard/templates/edc_subject_dashboard/buttons/refresh_appointments_button.html +1 -1
- edc_subject_dashboard/templates/edc_subject_dashboard/dashboard.html +1 -1
- edc_subject_dashboard/templatetags/edc_subject_dashboard_extras.py +3 -1
- edc_subject_dashboard/urls.py +13 -4
- edc_subject_dashboard/views/base_requisition_view.py +2 -1
- edc_subject_dashboard/views/subject_dashboard_view.py +1 -2
- edc_timepoint/__init__.py +0 -2
- edc_timepoint/model_mixins.py +1 -2
- edc_timepoint/utils.py +1 -1
- edc_timepoint/visit_timepoint_lookup.py +6 -0
- edc_visit_schedule/admin/subject_schedule_history_admin.py +1 -2
- edc_visit_schedule/navbars.py +3 -4
- edc_visit_schedule/visit/visit.py +15 -0
- edc_visit_tracking/model_mixins/visit_model_mixin/visit_model_mixin.py +5 -0
- edc_visit_tracking/models/subject_visit.py +5 -0
- edc_lab_dashboard/model_wrappers/__init__.py +0 -8
- edc_lab_dashboard/model_wrappers/aliquot_model_wrapper.py +0 -31
- edc_lab_dashboard/model_wrappers/base_box_item_model_wrapper.py +0 -21
- edc_lab_dashboard/model_wrappers/box_model_wrapper.py +0 -12
- edc_lab_dashboard/model_wrappers/manage_box_item_model_wrapper.py +0 -6
- edc_lab_dashboard/model_wrappers/manifest_item_model_wrapper.py +0 -21
- edc_lab_dashboard/model_wrappers/manifest_model_wrapper.py +0 -11
- edc_lab_dashboard/model_wrappers/requisition_model_wrapper.py +0 -25
- edc_lab_dashboard/model_wrappers/result_model_wrapper.py +0 -8
- edc_lab_dashboard/model_wrappers/verify_box_model_wrapper.py +0 -10
- edc_navbar/get_default_navbar.py +0 -9
- {clinicedc-2.0.38.dist-info → clinicedc-2.0.40.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import contextlib
|
|
4
|
+
from collections.abc import Callable
|
|
4
5
|
from typing import TYPE_CHECKING
|
|
5
6
|
|
|
6
7
|
from edc_appointment.constants import MISSED_APPT
|
|
@@ -28,9 +29,9 @@ class Rule:
|
|
|
28
29
|
|
|
29
30
|
def __init__(
|
|
30
31
|
self,
|
|
31
|
-
predicate: P | PF |
|
|
32
|
-
consequence: str
|
|
33
|
-
alternative: str
|
|
32
|
+
predicate: P | PF | Callable | str,
|
|
33
|
+
consequence: str,
|
|
34
|
+
alternative: str,
|
|
34
35
|
) -> None:
|
|
35
36
|
self.predicate = predicate
|
|
36
37
|
self.consequence = consequence
|
|
@@ -11,7 +11,7 @@ class RuleGroupError(Exception):
|
|
|
11
11
|
pass
|
|
12
12
|
|
|
13
13
|
|
|
14
|
-
class TargetModelConflict(Exception):
|
|
14
|
+
class TargetModelConflict(Exception): # noqa: N818
|
|
15
15
|
pass
|
|
16
16
|
|
|
17
17
|
|
|
@@ -61,7 +61,7 @@ class RuleGroup:
|
|
|
61
61
|
)
|
|
62
62
|
|
|
63
63
|
@classmethod
|
|
64
|
-
def _lookup_model(cls, model: str
|
|
64
|
+
def _lookup_model(cls, model: str, category: str) -> Any:
|
|
65
65
|
sys.stdout.write(f" ( ) {model}\r")
|
|
66
66
|
model_cls = None
|
|
67
67
|
try:
|
|
@@ -42,13 +42,13 @@ class RuleGroupMetaOptions:
|
|
|
42
42
|
# source model
|
|
43
43
|
self.source_model = self.options.get("source_model")
|
|
44
44
|
if self.source_model:
|
|
45
|
-
if len(self.source_model.split(".")) != 2:
|
|
45
|
+
if len(self.source_model.split(".")) != 2: # noqa: PLR2004
|
|
46
46
|
self.source_model = f"{self.app_label}.{self.source_model}"
|
|
47
47
|
self.options.update(source_model=self.source_model)
|
|
48
48
|
# related visit model
|
|
49
49
|
self.related_visit_model = self.options.get("related_visit_model")
|
|
50
50
|
if self.related_visit_model:
|
|
51
|
-
if len(self.related_visit_model.split(".")) != 2:
|
|
51
|
+
if len(self.related_visit_model.split(".")) != 2: # noqa: PLR2004
|
|
52
52
|
raise RuleGroupMetaError(
|
|
53
53
|
"Invalid _meta attr. Expected _meta.related_visit_model to be in "
|
|
54
54
|
f"label_lower format. Got '{self.related_visit_model}'. See {group_name}."
|
|
@@ -51,7 +51,7 @@ class RuleGroupMetaclass(type):
|
|
|
51
51
|
return super().__new__(mcs, name, bases, attrs)
|
|
52
52
|
|
|
53
53
|
@classmethod
|
|
54
|
-
def __get_rules(mcs, name: str, attrs: dict, meta: Any) -> tuple:
|
|
54
|
+
def __get_rules(mcs, name: str, attrs: dict, meta: Any) -> tuple: # noqa: N804
|
|
55
55
|
"""Returns a list of rules after updating each rule's attrs
|
|
56
56
|
with values from Meta.
|
|
57
57
|
|
|
@@ -59,28 +59,27 @@ class RuleGroupMetaclass(type):
|
|
|
59
59
|
"""
|
|
60
60
|
rules = []
|
|
61
61
|
for key, value in attrs.items():
|
|
62
|
-
if not key.startswith("_"):
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
rules.append(rule)
|
|
62
|
+
if not key.startswith("_") and isinstance(value, Rule):
|
|
63
|
+
rule = value
|
|
64
|
+
rule.name = key
|
|
65
|
+
rule.group = name
|
|
66
|
+
if isinstance(rule.predicate, (str,)):
|
|
67
|
+
predicates = getattr(meta, "predicates", None)
|
|
68
|
+
if not predicates:
|
|
69
|
+
raise RuleGroupError(
|
|
70
|
+
"RuleGroup Meta attr `predicates` may not be `None` if a "
|
|
71
|
+
"rule.predicate in the RuleGroup is a string. "
|
|
72
|
+
f"See {attrs.get('__qualname__')}."
|
|
73
|
+
)
|
|
74
|
+
rule.predicate = getattr(meta.predicates, rule.predicate)
|
|
75
|
+
for k, v in meta.options.items():
|
|
76
|
+
setattr(rule, k, v)
|
|
77
|
+
rule.target_models = mcs.__get_target_models(rule, meta)
|
|
78
|
+
rules.append(rule)
|
|
80
79
|
return tuple(rules)
|
|
81
80
|
|
|
82
81
|
@classmethod
|
|
83
|
-
def __get_target_models(mcs, rule: Any, meta: Any) -> Any:
|
|
82
|
+
def __get_target_models(mcs, rule: Any, meta: Any) -> Any: # noqa: N804
|
|
84
83
|
"""Returns target models as a list of label_lowers.
|
|
85
84
|
|
|
86
85
|
Target models are the models whose metadata is acted upon.
|
|
@@ -90,7 +89,7 @@ class RuleGroupMetaclass(type):
|
|
|
90
89
|
"""
|
|
91
90
|
target_models = []
|
|
92
91
|
for target_model in rule.target_models:
|
|
93
|
-
if len(target_model.split(".")) != 2:
|
|
94
|
-
target_model = f"{meta.app_label}.{target_model}"
|
|
92
|
+
if len(target_model.split(".")) != 2: # noqa: PLR2004
|
|
93
|
+
target_model = f"{meta.app_label}.{target_model}" # noqa: PLW2901
|
|
95
94
|
target_models.append(target_model)
|
|
96
95
|
return target_models
|
edc_metadata/metadata_updater.py
CHANGED
|
@@ -31,8 +31,9 @@ class MetadataUpdater(SourceModelMetadataMixin):
|
|
|
31
31
|
|
|
32
32
|
def __init__(
|
|
33
33
|
self,
|
|
34
|
-
|
|
35
|
-
|
|
34
|
+
*,
|
|
35
|
+
related_visit: RelatedVisitModel,
|
|
36
|
+
source_model: str,
|
|
36
37
|
allow_create: bool | None = None,
|
|
37
38
|
):
|
|
38
39
|
super().__init__(source_model, related_visit)
|
|
@@ -46,7 +47,7 @@ class MetadataUpdater(SourceModelMetadataMixin):
|
|
|
46
47
|
f"source_model={self.source_model})"
|
|
47
48
|
)
|
|
48
49
|
|
|
49
|
-
def get_and_update(self, entry_status: str
|
|
50
|
+
def get_and_update(self, entry_status: str) -> CrfMetadata | RequisitionMetadata:
|
|
50
51
|
metadata_obj = self.metadata_handler.metadata_obj
|
|
51
52
|
if entry_status != KEYED and self.source_model_obj_exists:
|
|
52
53
|
entry_status = KEYED
|
|
@@ -43,10 +43,9 @@ class CreatesMetadataModelMixin(RelatedVisitProtocol, models.Model):
|
|
|
43
43
|
|
|
44
44
|
Also called by post_save signal after metadata is updated.
|
|
45
45
|
"""
|
|
46
|
-
|
|
46
|
+
return self.metadata_rule_evaluator_cls(
|
|
47
47
|
related_visit=self, allow_create=allow_create
|
|
48
|
-
)
|
|
49
|
-
metadata_rule_evaluator.evaluate_rules()
|
|
48
|
+
).evaluate_rules()
|
|
50
49
|
|
|
51
50
|
@property
|
|
52
51
|
def metadata_query_options(self) -> dict[str, Any]:
|
|
@@ -54,14 +53,13 @@ class CreatesMetadataModelMixin(RelatedVisitProtocol, models.Model):
|
|
|
54
53
|
the related_visit.
|
|
55
54
|
"""
|
|
56
55
|
visit: Visit = self.visits.get(self.appointment.visit_code)
|
|
57
|
-
|
|
56
|
+
return dict(
|
|
58
57
|
visit_schedule_name=self.appointment.visit_schedule_name,
|
|
59
58
|
schedule_name=self.appointment.schedule_name,
|
|
60
59
|
visit_code=visit.code,
|
|
61
60
|
visit_code_sequence=self.appointment.visit_code_sequence,
|
|
62
61
|
timepoint=self.appointment.timepoint,
|
|
63
62
|
)
|
|
64
|
-
return options
|
|
65
63
|
|
|
66
64
|
@property
|
|
67
65
|
def crf_metadata(self):
|
|
@@ -42,7 +42,7 @@ class UpdatesMetadataModelMixin(models.Model):
|
|
|
42
42
|
metadata_updater_cls = MetadataUpdater
|
|
43
43
|
metadata_category: str = CRF
|
|
44
44
|
|
|
45
|
-
def metadata_update(self: CrfModel | RequisitionModel, entry_status: str
|
|
45
|
+
def metadata_update(self: CrfModel | RequisitionModel, entry_status: str) -> None:
|
|
46
46
|
"""Updates metatadata."""
|
|
47
47
|
self.metadata_updater.get_and_update(entry_status=entry_status)
|
|
48
48
|
|
edc_metadata/next_form_getter.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
from typing import TYPE_CHECKING
|
|
4
5
|
|
|
5
6
|
from django.apps import apps as django_apps
|
|
@@ -10,6 +11,7 @@ from .metadata import CrfMetadataGetter, RequisitionMetadataGetter
|
|
|
10
11
|
|
|
11
12
|
if TYPE_CHECKING:
|
|
12
13
|
from edc_appointment.models import Appointment
|
|
14
|
+
from edc_model.utils import CrfLikeModel
|
|
13
15
|
from edc_visit_schedule.visit import Crf, Requisition, Visit
|
|
14
16
|
|
|
15
17
|
from .metadata import MetadataGetter
|
|
@@ -22,10 +24,10 @@ class NextFormGetter:
|
|
|
22
24
|
|
|
23
25
|
def __init__(
|
|
24
26
|
self,
|
|
25
|
-
model_obj=None,
|
|
26
|
-
appointment: Appointment = None,
|
|
27
|
-
model: str = None,
|
|
28
|
-
panel_name: str = None,
|
|
27
|
+
model_obj: CrfLikeModel | None = None,
|
|
28
|
+
appointment: Appointment | None = None,
|
|
29
|
+
model: str | None = None,
|
|
30
|
+
panel_name: str | None = None,
|
|
29
31
|
):
|
|
30
32
|
self._getter = None
|
|
31
33
|
self._next_metadata_obj = None
|
|
@@ -58,7 +60,7 @@ class NextFormGetter:
|
|
|
58
60
|
"""
|
|
59
61
|
if not self._model_obj:
|
|
60
62
|
model_cls = django_apps.get_model(self.model)
|
|
61
|
-
|
|
63
|
+
with contextlib.suppress(ObjectDoesNotExist):
|
|
62
64
|
self._model_obj = model_cls.objects.get(
|
|
63
65
|
**{
|
|
64
66
|
f"{model_cls.related_visit_model_attr()}__appointment": (
|
|
@@ -66,8 +68,6 @@ class NextFormGetter:
|
|
|
66
68
|
)
|
|
67
69
|
}
|
|
68
70
|
)
|
|
69
|
-
except ObjectDoesNotExist:
|
|
70
|
-
pass
|
|
71
71
|
return self._model_obj
|
|
72
72
|
|
|
73
73
|
@property
|
|
@@ -94,23 +94,19 @@ class NextFormGetter:
|
|
|
94
94
|
|
|
95
95
|
@property
|
|
96
96
|
def next_panel(self) -> str | None:
|
|
97
|
-
if not self._next_panel:
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
self._next_panel = self.next_metadata_obj.panel_name
|
|
101
|
-
except AttributeError:
|
|
102
|
-
pass
|
|
97
|
+
if not self._next_panel and self.next_metadata_obj:
|
|
98
|
+
with contextlib.suppress(AttributeError):
|
|
99
|
+
self._next_panel = self.next_metadata_obj.panel_name
|
|
103
100
|
return self._next_panel
|
|
104
101
|
|
|
105
102
|
@property
|
|
106
103
|
def panel_name(self) -> str | None:
|
|
107
104
|
"""Returns a panel_name or None."""
|
|
108
|
-
if not self._panel_name:
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
self._panel_name = None
|
|
105
|
+
if not self._panel_name and self.model_obj:
|
|
106
|
+
try:
|
|
107
|
+
self._panel_name = self.model_obj.panel.name
|
|
108
|
+
except AttributeError:
|
|
109
|
+
self._panel_name = None
|
|
114
110
|
return self._panel_name
|
|
115
111
|
|
|
116
112
|
@property
|
edc_metadata/offline_models.py
CHANGED
|
@@ -8,6 +8,6 @@ offline_models = []
|
|
|
8
8
|
app_config = django_apps.get_app_config("edc_metadata")
|
|
9
9
|
for model in app_config.get_models():
|
|
10
10
|
if not issubclass(model, ListModelMixin):
|
|
11
|
-
offline_models.append(model._meta.label_lower)
|
|
11
|
+
offline_models.append(model._meta.label_lower) # noqa: PERF401
|
|
12
12
|
|
|
13
13
|
site_offline_models.register(offline_models, OfflineModel)
|
|
@@ -21,24 +21,24 @@ class RequisitionMetadataHandler(MetadataHandler):
|
|
|
21
21
|
super().__init__(**kwargs)
|
|
22
22
|
self.panel = panel
|
|
23
23
|
|
|
24
|
-
def _create(self
|
|
24
|
+
def _create(self) -> RequisitionMetadata:
|
|
25
25
|
"""Returns a created RequisitionMetadata model instance for this
|
|
26
26
|
requisition.
|
|
27
27
|
"""
|
|
28
28
|
metadata_obj = None
|
|
29
29
|
try:
|
|
30
|
-
requisition_object =
|
|
30
|
+
requisition_object = next(
|
|
31
31
|
requisition
|
|
32
32
|
for requisition in self.creator.related_visit.visit.all_requisitions
|
|
33
33
|
if requisition.panel.name == self.panel.name
|
|
34
|
-
|
|
35
|
-
except
|
|
34
|
+
)
|
|
35
|
+
except StopIteration as e:
|
|
36
36
|
if self.related_visit.reason != MISSED_VISIT:
|
|
37
37
|
raise MetadataHandlerError(
|
|
38
38
|
"Panel not found. Not in visit.all_requisitions. "
|
|
39
39
|
f"Panel `{self.panel}` at `{self.creator.related_visit.visit}`. "
|
|
40
40
|
f"Got {e}. Check your visit schedule."
|
|
41
|
-
)
|
|
41
|
+
) from e
|
|
42
42
|
else:
|
|
43
43
|
metadata_obj = self.creator.create_requisition(requisition_object)
|
|
44
44
|
return metadata_obj
|
|
@@ -33,13 +33,11 @@ class UpdateMetadataOnScheduleChange:
|
|
|
33
33
|
f"value for field '{self.fieldname}'.\n"
|
|
34
34
|
f"Old value='{self.old_value}', New value='{self.new_value}'.\n"
|
|
35
35
|
)
|
|
36
|
-
for
|
|
36
|
+
for model_cls in self.models.values():
|
|
37
37
|
count = model_cls.objects.filter(**{self.fieldname: self.old_value}).count()
|
|
38
38
|
sys.stdout.write(f"{model_cls._meta.label_lower}. {count} records found.\n")
|
|
39
39
|
sys.stdout.write(
|
|
40
|
-
style.ERROR(
|
|
41
|
-
"No records have been updated. \n" "Set --dry-run=False to update.\n"
|
|
42
|
-
)
|
|
40
|
+
style.ERROR("No records have been updated. \nSet --dry-run=False to update.\n")
|
|
43
41
|
)
|
|
44
42
|
else:
|
|
45
43
|
sys.stdout.write(style.SUCCESS("Updating... \n"))
|
edc_metadata/utils.py
CHANGED
edc_model/models/signals.py
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
|
|
1
3
|
from django import dispatch
|
|
4
|
+
from django.core.exceptions import ObjectDoesNotExist
|
|
2
5
|
from django.db.models import expressions
|
|
3
6
|
from simple_history import signals
|
|
4
7
|
|
|
@@ -17,12 +20,14 @@ def remove_f_expressions(sender, instance, history_instance, **kwargs) -> None:
|
|
|
17
20
|
"""
|
|
18
21
|
f_expression_fields = []
|
|
19
22
|
for field in history_instance._meta.fields:
|
|
20
|
-
|
|
23
|
+
with contextlib.suppress(ObjectDoesNotExist):
|
|
24
|
+
field_value = getattr(history_instance, field.name)
|
|
21
25
|
if isinstance(field_value, expressions.BaseExpression):
|
|
22
26
|
f_expression_fields.append(field.name)
|
|
23
27
|
|
|
24
28
|
if f_expression_fields:
|
|
25
29
|
instance.refresh_from_db()
|
|
26
30
|
for field_name in f_expression_fields:
|
|
27
|
-
|
|
31
|
+
with contextlib.suppress(ObjectDoesNotExist):
|
|
32
|
+
field_value = getattr(instance, field_name)
|
|
28
33
|
setattr(history_instance, field_name, field_value)
|
|
@@ -6,7 +6,6 @@ from django.contrib import messages
|
|
|
6
6
|
from django.http.response import HttpResponseRedirect
|
|
7
7
|
from django.urls import reverse
|
|
8
8
|
from django.utils.encoding import force_str
|
|
9
|
-
|
|
10
9
|
from edc_dashboard.url_names import InvalidDashboardUrlName, url_names
|
|
11
10
|
|
|
12
11
|
|
|
@@ -75,8 +74,10 @@ class ModelAdminRedirectOnDeleteMixin:
|
|
|
75
74
|
"""Overridden to redirect to `post_url_on_delete`, if not None."""
|
|
76
75
|
if self.post_url_on_delete:
|
|
77
76
|
opts = self.model._meta
|
|
78
|
-
msg = (
|
|
79
|
-
|
|
77
|
+
msg = (
|
|
78
|
+
f'The {force_str(opts.verbose_name)} "{force_str(obj_display)}" '
|
|
79
|
+
"was deleted successfully."
|
|
80
|
+
)
|
|
80
81
|
messages.add_message(request, messages.SUCCESS, msg)
|
|
81
82
|
return HttpResponseRedirect(self.post_url_on_delete)
|
|
82
83
|
return super().response_delete(request, obj_display, obj_id)
|
edc_navbar/apps.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
from django.apps import AppConfig as DjangoAppConfig
|
|
2
|
-
from django.conf import settings
|
|
3
2
|
from django.core.management.color import color_style
|
|
4
3
|
|
|
5
4
|
style = color_style()
|
|
@@ -9,4 +8,3 @@ class AppConfig(DjangoAppConfig):
|
|
|
9
8
|
name = "edc_navbar"
|
|
10
9
|
verbose_name = "Edc Navbar"
|
|
11
10
|
register_default_navbar = True
|
|
12
|
-
default_navbar_name = getattr(settings, "DEFAULT_NAVBAR_NAME", "default")
|
edc_navbar/navbar.py
CHANGED
edc_navbar/navbar_item.py
CHANGED
|
@@ -6,7 +6,6 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
from django.core.management.color import color_style
|
|
7
7
|
from django.urls import NoReverseMatch
|
|
8
8
|
from django.urls.base import reverse
|
|
9
|
-
|
|
10
9
|
from edc_dashboard.url_names import InvalidDashboardUrlName, url_names
|
|
11
10
|
|
|
12
11
|
if TYPE_CHECKING:
|
|
@@ -23,10 +22,13 @@ class NavbarItem:
|
|
|
23
22
|
title: str = field(default=None)
|
|
24
23
|
label: str | None = field(default=None)
|
|
25
24
|
codename: str = field(default=None)
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
url_names_key: str | None = field(
|
|
26
|
+
default=None
|
|
27
|
+
) # must be valid key in url_names dictionary
|
|
28
|
+
url_with_namespace: str | None = field(default=None)
|
|
29
|
+
url_without_namespace: str | None = field(default=None)
|
|
28
30
|
fa_icon: str | None = field(default=None)
|
|
29
|
-
disabled: str = field(default="disabled")
|
|
31
|
+
disabled: str | None = field(default="disabled")
|
|
30
32
|
|
|
31
33
|
active: bool = field(default=None)
|
|
32
34
|
|
|
@@ -37,24 +39,35 @@ class NavbarItem:
|
|
|
37
39
|
|
|
38
40
|
def __post_init__(self):
|
|
39
41
|
self.title = self.title or self.label or self.name.title() # the anchor title
|
|
42
|
+
if self.url_with_namespace and ":" not in self.url_with_namespace:
|
|
43
|
+
raise InvalidDashboardUrlName(
|
|
44
|
+
f"Invalid url_with_namespace. Got {self.url_with_namespace}"
|
|
45
|
+
)
|
|
40
46
|
|
|
41
47
|
def get_url(self, raise_exception: bool | None = None) -> str | None:
|
|
48
|
+
url = (
|
|
49
|
+
self.url_without_namespace
|
|
50
|
+
or self.url_with_namespace
|
|
51
|
+
or url_names.get(self.url_names_key)
|
|
52
|
+
)
|
|
42
53
|
try:
|
|
43
|
-
url = reverse(
|
|
44
|
-
except NoReverseMatch:
|
|
45
|
-
url = None
|
|
54
|
+
url = reverse(url)
|
|
55
|
+
except NoReverseMatch as e:
|
|
46
56
|
if raise_exception:
|
|
47
|
-
|
|
57
|
+
errmsg = (
|
|
58
|
+
f"Reverse for Navbar url not found. Tried {url}. "
|
|
59
|
+
f"See NavbarItem(name={self.name}"
|
|
60
|
+
)
|
|
61
|
+
if self.url_without_namespace == url:
|
|
62
|
+
errmsg = f"{errmsg}, url_without_namespace={self.url_without_namespace})."
|
|
63
|
+
elif self.url_with_namespace == url:
|
|
64
|
+
errmsg = f"{errmsg}, url_with_namespace={self.url_with_namespace})."
|
|
65
|
+
elif url_names.get(self.url_names_key) == url:
|
|
66
|
+
errmsg = f"{errmsg}, url_names_key={self.url_names_key})."
|
|
67
|
+
raise NoReverseMatch(errmsg) from e
|
|
68
|
+
url = None
|
|
48
69
|
return url
|
|
49
70
|
|
|
50
|
-
@property
|
|
51
|
-
def real_url_name(self) -> str:
|
|
52
|
-
try:
|
|
53
|
-
url_name = url_names.get(self.url_name)
|
|
54
|
-
except InvalidDashboardUrlName:
|
|
55
|
-
url_name = self.url_name.split(":")[1] if self.no_url_namespace else self.url_name
|
|
56
|
-
return url_name
|
|
57
|
-
|
|
58
71
|
def set_disabled(self, user: User | None = None):
|
|
59
72
|
if user and user.has_perm(self.codename):
|
|
60
73
|
self.disabled = ""
|
edc_navbar/navbars.py
CHANGED
|
@@ -1,20 +1,17 @@
|
|
|
1
|
-
from django.apps import apps as django_apps
|
|
2
|
-
|
|
3
1
|
from .navbar import Navbar
|
|
4
2
|
from .navbar_item import NavbarItem
|
|
5
3
|
from .site_navbars import site_navbars
|
|
4
|
+
from .utils import get_default_navbar_name, get_register_default_navbar
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if app_config.register_default_navbar:
|
|
10
|
-
default_navbar = Navbar(name=app_config.default_navbar_name)
|
|
6
|
+
if get_register_default_navbar():
|
|
7
|
+
default_navbar = Navbar(name=get_default_navbar_name())
|
|
11
8
|
|
|
12
9
|
default_navbar.register(
|
|
13
10
|
NavbarItem(
|
|
14
11
|
name="home",
|
|
15
12
|
title="Home",
|
|
16
13
|
fa_icon="fa-home",
|
|
17
|
-
|
|
14
|
+
url_without_namespace="home_url",
|
|
18
15
|
codename="edc_navbar.nav_home",
|
|
19
16
|
)
|
|
20
17
|
)
|
|
@@ -25,19 +22,9 @@ if app_config.register_default_navbar:
|
|
|
25
22
|
title="Administration",
|
|
26
23
|
fa_icon="fa-cog",
|
|
27
24
|
codename="edc_navbar.nav_administration",
|
|
28
|
-
|
|
25
|
+
# url_names_key="administration",
|
|
26
|
+
url_without_namespace="administration_url",
|
|
29
27
|
)
|
|
30
28
|
)
|
|
31
29
|
|
|
32
|
-
# remove to use a form. See edc_navbar.html
|
|
33
|
-
# default_navbar.register(
|
|
34
|
-
# NavbarItem(
|
|
35
|
-
# name="logout",
|
|
36
|
-
# title="Logout",
|
|
37
|
-
# fa_icon="fa-sign-out-alt",
|
|
38
|
-
# url_name="edc_auth_admin:logout",
|
|
39
|
-
# codename="edc_navbar.nav_logout",
|
|
40
|
-
# )
|
|
41
|
-
# )
|
|
42
|
-
|
|
43
30
|
site_navbars.register(default_navbar)
|
edc_navbar/site_navbars.py
CHANGED
|
@@ -44,16 +44,16 @@ class NavbarCollection:
|
|
|
44
44
|
navbar_name=name,
|
|
45
45
|
)
|
|
46
46
|
|
|
47
|
-
def get_navbar(self, name: str
|
|
47
|
+
def get_navbar(self, name: str, selected_item: str | None = None) -> Navbar:
|
|
48
48
|
"""Returns a selected navbar in the collection."""
|
|
49
49
|
# does the navbar exist?
|
|
50
50
|
try:
|
|
51
51
|
navbar: Navbar = self.registry[name]
|
|
52
|
-
except KeyError:
|
|
52
|
+
except KeyError as e:
|
|
53
53
|
raise NavbarError(
|
|
54
54
|
f"Navbar '{name}' does not exist. Expected one of "
|
|
55
55
|
f"{list(self.registry.keys())}. See {self!r}."
|
|
56
|
-
)
|
|
56
|
+
) from e
|
|
57
57
|
else:
|
|
58
58
|
# does the navbar have items?
|
|
59
59
|
if not navbar.navbar_items:
|
|
@@ -66,10 +66,9 @@ class NavbarCollection:
|
|
|
66
66
|
navbar.set_active(selected_item)
|
|
67
67
|
return navbar
|
|
68
68
|
|
|
69
|
-
def show_user_permissions(self, username: str
|
|
69
|
+
def show_user_permissions(self, username: str, navbar_name: str):
|
|
70
70
|
user = django_apps.get_model("auth.user").objects.get(username=username)
|
|
71
|
-
|
|
72
|
-
return navbar.show_user_permissions(user=user)
|
|
71
|
+
return self.registry.get(navbar_name).show_user_permissions(user=user)
|
|
73
72
|
|
|
74
73
|
def show_user_codenames(self, username=None, navbar_name=None):
|
|
75
74
|
user_permissions = self.show_user_permissions(username, navbar_name)
|
|
@@ -101,7 +100,7 @@ class NavbarCollection:
|
|
|
101
100
|
except ImportError as e:
|
|
102
101
|
site_navbars.registry = before_import_registry
|
|
103
102
|
if module_has_submodule(mod, module_name):
|
|
104
|
-
raise NavbarError(e)
|
|
103
|
+
raise NavbarError(e) from e
|
|
105
104
|
except ImportError:
|
|
106
105
|
pass
|
|
107
106
|
|
edc_navbar/system_checks.py
CHANGED
|
@@ -8,7 +8,7 @@ from edc_navbar import site_navbars
|
|
|
8
8
|
def edc_navbar_checks(app_configs, **kwargs) -> list[CheckMessage]:
|
|
9
9
|
errors = []
|
|
10
10
|
|
|
11
|
-
for
|
|
11
|
+
for navbar in site_navbars.registry.values():
|
|
12
12
|
for navbar_item in navbar.navbar_items:
|
|
13
13
|
try:
|
|
14
14
|
app_label, codename = navbar_item.codename.split(".")
|
|
@@ -30,13 +30,6 @@ def edc_navbar_checks(app_configs, **kwargs) -> list[CheckMessage]:
|
|
|
30
30
|
errors.append(Error(msg, id="edc_navbar.E002"))
|
|
31
31
|
try:
|
|
32
32
|
navbar_item.get_url(raise_exception=True)
|
|
33
|
-
except NoReverseMatch:
|
|
34
|
-
errors.append(
|
|
35
|
-
Error(
|
|
36
|
-
f"NoReverseMatch for url. Got '{navbar_item.real_url_name}'. "
|
|
37
|
-
f"See {navbar_item.name}.",
|
|
38
|
-
id="edc_navbar.E003",
|
|
39
|
-
)
|
|
40
|
-
)
|
|
41
|
-
|
|
33
|
+
except NoReverseMatch as e:
|
|
34
|
+
errors.append(Error(str(e), id="edc_navbar.E003"))
|
|
42
35
|
return errors
|
edc_navbar/utils.py
CHANGED
|
@@ -7,3 +7,17 @@ def get_autodiscover():
|
|
|
7
7
|
|
|
8
8
|
def get_verify_on_load():
|
|
9
9
|
return getattr(settings, "EDC_NAVBAR_VERIFY_ON_LOAD", "")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_register_default_navbar():
|
|
13
|
+
return getattr(settings, "EDC_NAVBAR_REGISTER_DEFAULT_NAVBAR", True)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def get_default_navbar_name():
|
|
17
|
+
"""Returns the default navbar name.
|
|
18
|
+
|
|
19
|
+
For example: inte_dashboard for project INTE.
|
|
20
|
+
"""
|
|
21
|
+
return getattr(
|
|
22
|
+
settings, "EDC_NAVBAR_DEFAULT_NAVBAR_NAME", f"{settings.APP_NAME}_dashboard".lower()
|
|
23
|
+
)
|