clinicedc 2.0.15__py3-none-any.whl → 2.0.17__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.15.dist-info → clinicedc-2.0.17.dist-info}/METADATA +1 -1
- {clinicedc-2.0.15.dist-info → clinicedc-2.0.17.dist-info}/RECORD +12 -12
- edc_form_describer/form_describer.py +13 -8
- edc_form_describer/forms_reference.py +24 -25
- edc_form_describer/make_forms_reference.py +10 -12
- edc_form_describer/management/commands/make_forms_reference.py +8 -8
- edc_form_describer/markdown_writer.py +11 -10
- edc_model_to_dataframe/model_to_dataframe.py +46 -55
- edc_model_to_dataframe/read_frame_edc.py +1 -1
- edc_visit_schedule/schedule/schedule.py +13 -13
- {clinicedc-2.0.15.dist-info → clinicedc-2.0.17.dist-info}/WHEEL +0 -0
- {clinicedc-2.0.15.dist-info → clinicedc-2.0.17.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: clinicedc
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.17
|
|
4
4
|
Summary: A clinical trials data management framework built on Django
|
|
5
5
|
Keywords: django,clinicedc,edc,clinical trials,research,data management,esource
|
|
6
6
|
Author: Erik van Widenfelt, Jonathan Willitts
|
|
@@ -1087,13 +1087,13 @@ edc_fieldsets/models.py,sha256=sAQOBRo_PM7Uu3Hvxx2j5CLDDpuei8-vAYdkOUSuYkg,32
|
|
|
1087
1087
|
edc_fieldsets/urls.py,sha256=_SQMjzRQvSHqalE_zviNqzpbcwJgLdjAgOh3-Eprb58,749
|
|
1088
1088
|
edc_form_describer/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1089
1089
|
edc_form_describer/apps.py,sha256=unr6UihcCOaoVtj56Q6DRQq9WDjivoE1zZjyy2HTBqU,121
|
|
1090
|
-
edc_form_describer/form_describer.py,sha256=
|
|
1091
|
-
edc_form_describer/forms_reference.py,sha256=
|
|
1092
|
-
edc_form_describer/make_forms_reference.py,sha256=
|
|
1090
|
+
edc_form_describer/form_describer.py,sha256=0F7eu2rLpThwv9lU7taSZFr6mJed1Gpv6hUeMYEWlGA,8435
|
|
1091
|
+
edc_form_describer/forms_reference.py,sha256=TYofCReEipIceNXSddL29jcApYUrgU8LNq5UElJtSz0,4709
|
|
1092
|
+
edc_form_describer/make_forms_reference.py,sha256=wQY39EFscPWTszj8gzpLhf0WKktL91uIVBdx1cMh_gU,1327
|
|
1093
1093
|
edc_form_describer/management/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1094
1094
|
edc_form_describer/management/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1095
|
-
edc_form_describer/management/commands/make_forms_reference.py,sha256=
|
|
1096
|
-
edc_form_describer/markdown_writer.py,sha256=
|
|
1095
|
+
edc_form_describer/management/commands/make_forms_reference.py,sha256=Pni3ZPpGM5Ds7-GYXPvl_m2zxHOE2_rFvmdlXre0FnM,3168
|
|
1096
|
+
edc_form_describer/markdown_writer.py,sha256=UFjVvz5cUHyJnlUDNm6RIPBGc675HL-IsoUS6TDrKJg,1809
|
|
1097
1097
|
edc_form_describer/migrations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1098
1098
|
edc_form_describer/models.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
1099
1099
|
edc_form_describer/urls.py,sha256=_FX1x4JJH8wKclMUbJI847xPbjxqHJazbRZYj30wW7g,113
|
|
@@ -1966,8 +1966,8 @@ edc_model_form/utils.py,sha256=gGe3etrp5YF4AKMGFIR3I9V1CfX4c5Wm6fK-8JkUeT4,538
|
|
|
1966
1966
|
edc_model_to_dataframe/__init__.py,sha256=iDR_F5oKkKDL9FfBXl0WKpwc2iOWbOlY-8wpCipBNnQ,115
|
|
1967
1967
|
edc_model_to_dataframe/apps.py,sha256=US1ehhJPgkvmIDZXiUPMoSjHTWIztUbYGtx_azTXPIQ,290
|
|
1968
1968
|
edc_model_to_dataframe/constants.py,sha256=xgCr2ZXUsjZqf4wfpY0JBhQoARzQEVS8tnkfBWwOMVQ,702
|
|
1969
|
-
edc_model_to_dataframe/model_to_dataframe.py,sha256=
|
|
1970
|
-
edc_model_to_dataframe/read_frame_edc.py,sha256=
|
|
1969
|
+
edc_model_to_dataframe/model_to_dataframe.py,sha256=Ut-Ixi7eoppBlnlgq4gBnhBh3ts13n1UjLMGQcKY7Bs,18684
|
|
1970
|
+
edc_model_to_dataframe/read_frame_edc.py,sha256=aOj3FUUvTCr-V-RmkBQtSGpg46zpWfX40HfOT3eRS4U,800
|
|
1971
1971
|
edc_model_to_dataframe/urls.py,sha256=KChLHeE6yfONC6IdMm3Q4U3u7tmy_Mb1w2lrbpFz96g,148
|
|
1972
1972
|
edc_navbar/__init__.py,sha256=Y95juevvOawGZmOV3Ofef7oGwYSWWttAYT0v0mehkJc,195
|
|
1973
1973
|
edc_navbar/apps.py,sha256=fiWHsCYaDnHXNfYUu6DuvlAcO2k45-zkdbuXwXI3ksc,367
|
|
@@ -3287,7 +3287,7 @@ edc_visit_schedule/navbars.py,sha256=RWx57OD-KxpPh0ihRyangGGiOOR7vfxRFjPCj8C5uF4
|
|
|
3287
3287
|
edc_visit_schedule/ordered_collection.py,sha256=7A5YqG0qGx6jAE0z6bd3BijlW9oYxiQrF-5fzB3hvok,1699
|
|
3288
3288
|
edc_visit_schedule/post_migrate_signals.py,sha256=7vFfx7ifMwmvrf7MCzpk9q4g1pbM4n44uM4TrH1hPGQ,748
|
|
3289
3289
|
edc_visit_schedule/schedule/__init__.py,sha256=bzyYW9y3pPCFTjwjvkUVgkW3d1g1D2SCNfgE9bnLjII,101
|
|
3290
|
-
edc_visit_schedule/schedule/schedule.py,sha256=
|
|
3290
|
+
edc_visit_schedule/schedule/schedule.py,sha256=RhUc5UPKZacqYmdxI8-tkEkERZFfq0TAN-wDF8dBMEU,14881
|
|
3291
3291
|
edc_visit_schedule/schedule/visit_collection.py,sha256=9OJKrLtx-H0eL9oo0dsRqYGPICiAMqEF4xIhhlinx0A,2033
|
|
3292
3292
|
edc_visit_schedule/schedule/window.py,sha256=P_hNhAXBmRxED_0UrlpQoYHF5r_nTEpTK7U2JSAnpCs,5222
|
|
3293
3293
|
edc_visit_schedule/simple_model_validator.py,sha256=pE2QCSI1VhSr9Dd-NGd-72u6e479_vzbS8iL17eg4Yc,771
|
|
@@ -3406,7 +3406,7 @@ edc_vitals/models/fields/waist_circumference.py,sha256=fZcHFDdEwWLjIVLktKrFCD9UU
|
|
|
3406
3406
|
edc_vitals/models/fields/weight.py,sha256=zo9_9e3Cpu0UqoRbWS-iDkcDo6fK80b1dDQy4x4MyxE,921
|
|
3407
3407
|
edc_vitals/utils.py,sha256=vXid44KUXxeaSyund_y5MNXc5DFJs052_PwUAjszE2k,1384
|
|
3408
3408
|
edc_vitals/validators.py,sha256=vNiElWMs0rRnHRNuVoPLRf0rW_C_0xcfUyep1Y_Si5s,156
|
|
3409
|
-
clinicedc-2.0.
|
|
3410
|
-
clinicedc-2.0.
|
|
3411
|
-
clinicedc-2.0.
|
|
3412
|
-
clinicedc-2.0.
|
|
3409
|
+
clinicedc-2.0.17.dist-info/licenses/LICENSE,sha256=jOtLnuWt7d5Hsx6XXB2QxzrSe2sWWh3NgMfFRetluQM,35147
|
|
3410
|
+
clinicedc-2.0.17.dist-info/WHEEL,sha256=-neZj6nU9KAMg2CnCY6T3w8J53nx1kFGw_9HfoSzM60,79
|
|
3411
|
+
clinicedc-2.0.17.dist-info/METADATA,sha256=Vh9mgF060UWvEzXQ9O7I8XTXxKqDH9REwVJLEdtoDrs,15899
|
|
3412
|
+
clinicedc-2.0.17.dist-info/RECORD,,
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import re
|
|
4
5
|
import string
|
|
5
6
|
import sys
|
|
6
|
-
from datetime import datetime
|
|
7
7
|
from math import floor
|
|
8
8
|
|
|
9
9
|
from django.core.management.color import color_style
|
|
10
|
+
from django.utils import timezone
|
|
10
11
|
|
|
11
12
|
from edc_fieldsets.fieldsets import Fieldsets
|
|
12
13
|
from edc_model.constants import DEFAULT_BASE_FIELDS
|
|
@@ -57,14 +58,20 @@ class FormDescriber:
|
|
|
57
58
|
self.markdown: list[str] = []
|
|
58
59
|
add_timestamp = True if add_timestamp is None else add_timestamp
|
|
59
60
|
self.anchor_prefix = anchor_prefix or self.anchor_prefix
|
|
60
|
-
timestamp =
|
|
61
|
+
timestamp = timezone.now().strftime("%Y-%m-%d %H:%M")
|
|
61
62
|
self.level = level or self.level
|
|
62
63
|
self.conditional_fieldset = None
|
|
63
64
|
self.admin_cls = admin_cls
|
|
64
65
|
try:
|
|
65
66
|
self.model_cls = admin_cls.model
|
|
66
67
|
except AttributeError:
|
|
67
|
-
|
|
68
|
+
try:
|
|
69
|
+
self.model_cls = admin_cls.form._meta.model
|
|
70
|
+
except AttributeError as e:
|
|
71
|
+
raise FormDescriberError(
|
|
72
|
+
f"Unable to determine admin class model. Got {admin_cls}"
|
|
73
|
+
) from e
|
|
74
|
+
|
|
68
75
|
self.visit_code = visit_code
|
|
69
76
|
self.models_fields = {fld.name: fld for fld in self.model_cls._meta.get_fields()}
|
|
70
77
|
|
|
@@ -115,7 +122,7 @@ class FormDescriber:
|
|
|
115
122
|
def anchor(self):
|
|
116
123
|
allow = string.ascii_letters + string.digits + "-"
|
|
117
124
|
slug = self.verbose_name.lower().replace(" ", "-")
|
|
118
|
-
slug = re.sub("[
|
|
125
|
+
slug = re.sub(f"[^{allow}]", "", slug)
|
|
119
126
|
return f"{self.anchor_prefix}-{slug}"
|
|
120
127
|
|
|
121
128
|
def describe(self):
|
|
@@ -135,7 +142,7 @@ class FormDescriber:
|
|
|
135
142
|
|
|
136
143
|
for fieldset_name, fieldset in self.fieldsets:
|
|
137
144
|
if fieldset_name not in ["Audit"]:
|
|
138
|
-
fieldset_name = fieldset_name or "Main"
|
|
145
|
+
fieldset_name = fieldset_name or "Main" # noqa: PLW2901
|
|
139
146
|
self.markdown.append(f"\n**Section: {fieldset_name}**")
|
|
140
147
|
if fieldset.get("classes") != "collapse":
|
|
141
148
|
for fname in fieldset.get("fields"):
|
|
@@ -172,10 +179,8 @@ class FormDescriber:
|
|
|
172
179
|
self.markdown.append(f"* custom_prompt: *{self.custom_form_labels.get(fname)}*")
|
|
173
180
|
self.markdown.append(f"- db_table: {self.model_cls._meta.db_table}")
|
|
174
181
|
self.markdown.append(f"- column: {field_cls.name}")
|
|
175
|
-
|
|
182
|
+
with contextlib.suppress(AttributeError):
|
|
176
183
|
self.markdown.append(f"- metadata: {field_cls.metadata}")
|
|
177
|
-
except AttributeError:
|
|
178
|
-
pass
|
|
179
184
|
self.markdown.append(f"- type: {field_cls.get_internal_type()}")
|
|
180
185
|
if field_cls.max_length:
|
|
181
186
|
self.markdown.append(f"- length: {field_cls.max_length}")
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
from datetime import datetime
|
|
2
1
|
from importlib.metadata import version
|
|
2
|
+
from pathlib import Path
|
|
3
3
|
|
|
4
4
|
from django.apps import apps as django_apps
|
|
5
5
|
from django.conf import settings
|
|
6
|
+
from django.utils import timezone
|
|
6
7
|
|
|
7
8
|
from .form_describer import FormDescriber
|
|
8
9
|
from .markdown_writer import MarkdownWriter
|
|
@@ -36,24 +37,20 @@ class FormsReference:
|
|
|
36
37
|
self.add_per_form_timestamp = (
|
|
37
38
|
True if add_per_form_timestamp is None else add_per_form_timestamp
|
|
38
39
|
)
|
|
39
|
-
self.timestamp =
|
|
40
|
+
self.timestamp = timezone.now().strftime("%Y-%m-%d %H:%M")
|
|
40
41
|
for visit_schedule in self.visit_schedules:
|
|
41
42
|
self.plans.update({visit_schedule.name: {}})
|
|
42
43
|
for schedule in visit_schedule.schedules.values():
|
|
43
44
|
for visit_code, visit in schedule.visits.items():
|
|
44
|
-
crfs = []
|
|
45
|
-
requisitions = []
|
|
46
|
-
for c in visit.crfs:
|
|
47
|
-
crfs.append(c.model)
|
|
48
|
-
for r in visit.requisitions:
|
|
49
|
-
requisitions.append(r.panel.name)
|
|
45
|
+
crfs = [c.model for c in visit.crfs]
|
|
46
|
+
requisitions = [r.panel.name for r in visit.requisitions]
|
|
50
47
|
self.plans[visit_schedule.name].update(
|
|
51
48
|
{visit_code: {"crfs": crfs, "requisitions": requisitions}}
|
|
52
49
|
)
|
|
53
50
|
|
|
54
51
|
def to_file(
|
|
55
52
|
self,
|
|
56
|
-
path: str | None = None,
|
|
53
|
+
path: Path | str | None = None,
|
|
57
54
|
overwrite: bool | None = None,
|
|
58
55
|
pad: int | None = None,
|
|
59
56
|
):
|
|
@@ -95,23 +92,25 @@ class FormsReference:
|
|
|
95
92
|
for index, model in enumerate(documents.get("crfs")):
|
|
96
93
|
model_cls = django_apps.get_model(model)
|
|
97
94
|
admin_cls = self.admin_site._registry.get(model_cls)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
95
|
+
if admin_cls:
|
|
96
|
+
describer = self.describer_cls(
|
|
97
|
+
admin_cls=admin_cls,
|
|
98
|
+
include_hidden_fields=self.include_hidden_fields,
|
|
99
|
+
visit_code=visit_code,
|
|
100
|
+
level=self.h4,
|
|
101
|
+
anchor_prefix=self.anchor_prefix,
|
|
102
|
+
add_timestamp=self.add_per_form_timestamp,
|
|
103
|
+
)
|
|
104
|
+
describer.markdown.append("\n")
|
|
105
|
+
anchor = f"{self.get_anchor(describer.anchor)}"
|
|
106
|
+
toc.append(
|
|
107
|
+
f'{index + 1}. <a href="#{anchor}">{describer.verbose_name}</a>'
|
|
108
|
+
)
|
|
109
|
+
markdown.extend(describer.markdown)
|
|
112
110
|
markdown.append(f"{self.h4} Requisitions\n")
|
|
113
|
-
|
|
114
|
-
|
|
111
|
+
markdown.extend(
|
|
112
|
+
[f"* {panel_name}\n" for panel_name in documents.get("requisitions")]
|
|
113
|
+
)
|
|
115
114
|
markdown = self.insert_toc(toc, markdown)
|
|
116
115
|
markdown.insert(0, f"{self.h1} {self.title}")
|
|
117
116
|
markdown.append(
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import os
|
|
4
3
|
import sys
|
|
5
4
|
from importlib import import_module
|
|
6
5
|
|
|
@@ -16,11 +15,10 @@ style = color_style()
|
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
def make_forms_reference(
|
|
19
|
-
app_label: str
|
|
20
|
-
admin_site_name: str
|
|
21
|
-
visit_schedule_name: str
|
|
22
|
-
title: str = None,
|
|
23
|
-
path: str | None = None,
|
|
18
|
+
app_label: str,
|
|
19
|
+
admin_site_name: str,
|
|
20
|
+
visit_schedule_name: str,
|
|
21
|
+
title: str | None = None,
|
|
24
22
|
):
|
|
25
23
|
module = import_module(app_label)
|
|
26
24
|
admin_site = getattr(module.admin_site, admin_site_name)
|
|
@@ -29,9 +27,9 @@ def make_forms_reference(
|
|
|
29
27
|
sys.stdout.write(
|
|
30
28
|
style.MIGRATE_HEADING(f"Refreshing CRF reference document for {app_label}\n")
|
|
31
29
|
)
|
|
32
|
-
doc_folder =
|
|
33
|
-
if not
|
|
34
|
-
|
|
30
|
+
doc_folder = settings.BASE_DIR / "docs"
|
|
31
|
+
if not doc_folder.exists():
|
|
32
|
+
doc_folder.mkdir()
|
|
35
33
|
|
|
36
34
|
forms = FormsReference(
|
|
37
35
|
visit_schedules=[visit_schedule],
|
|
@@ -40,8 +38,8 @@ def make_forms_reference(
|
|
|
40
38
|
add_per_form_timestamp=False,
|
|
41
39
|
)
|
|
42
40
|
|
|
43
|
-
path =
|
|
41
|
+
path = doc_folder / f"forms_reference_{app_label}.md"
|
|
44
42
|
forms.to_file(path=path, overwrite=True)
|
|
45
43
|
|
|
46
|
-
|
|
47
|
-
|
|
44
|
+
sys.stdout.write(f"{path}\n")
|
|
45
|
+
sys.stdout.write("Done.\n")
|
|
@@ -17,16 +17,16 @@ style = color_style()
|
|
|
17
17
|
|
|
18
18
|
|
|
19
19
|
def update_forms_reference(
|
|
20
|
-
app_label: str
|
|
21
|
-
admin_site_name: str
|
|
22
|
-
visit_schedule_name: str
|
|
23
|
-
title: str = None,
|
|
20
|
+
app_label: str,
|
|
21
|
+
admin_site_name: str,
|
|
22
|
+
visit_schedule_name: str,
|
|
23
|
+
title: str | None = None,
|
|
24
24
|
filename: str | None = None,
|
|
25
25
|
doc_folder: str | None = None,
|
|
26
26
|
):
|
|
27
27
|
module = import_module(app_label)
|
|
28
28
|
default_doc_folder = Path(settings.BASE_DIR / "docs")
|
|
29
|
-
filename = filename or "
|
|
29
|
+
filename = filename or f"forms_reference_{app_label}.md"
|
|
30
30
|
admin_site = getattr(module.admin_site, admin_site_name)
|
|
31
31
|
visit_schedule = site_visit_schedules.get_visit_schedule(visit_schedule_name)
|
|
32
32
|
title = title or _("%(title_app)s Forms Reference") % dict(title_app=app_label.upper())
|
|
@@ -47,8 +47,8 @@ def update_forms_reference(
|
|
|
47
47
|
path = doc_folder / filename
|
|
48
48
|
forms.to_file(path=path, overwrite=True)
|
|
49
49
|
|
|
50
|
-
|
|
51
|
-
|
|
50
|
+
sys.stdout.write(f"{path}\n")
|
|
51
|
+
sys.stdout.write("Done\n")
|
|
52
52
|
|
|
53
53
|
|
|
54
54
|
class Command(BaseCommand):
|
|
@@ -85,7 +85,7 @@ class Command(BaseCommand):
|
|
|
85
85
|
default=None,
|
|
86
86
|
)
|
|
87
87
|
|
|
88
|
-
def handle(self, *args, **options):
|
|
88
|
+
def handle(self, *args, **options): # noqa: ARG002
|
|
89
89
|
app_label = options["app_label"]
|
|
90
90
|
admin_site_name = options["admin_site_name"]
|
|
91
91
|
visit_schedule_name = options["visit_schedule_name"]
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from django.utils import timezone
|
|
5
6
|
|
|
6
7
|
|
|
7
8
|
class MarkdownWriter:
|
|
@@ -11,23 +12,23 @@ class MarkdownWriter:
|
|
|
11
12
|
@staticmethod
|
|
12
13
|
def get_path(path: str | None = None, overwrite: bool | None = None) -> str:
|
|
13
14
|
if not path:
|
|
14
|
-
timestamp =
|
|
15
|
+
timestamp = timezone.now().strftime("%Y%m%d%H%M")
|
|
15
16
|
path = f"forms_{timestamp}.md"
|
|
16
|
-
if
|
|
17
|
+
if Path(path).exists():
|
|
17
18
|
if overwrite:
|
|
18
|
-
|
|
19
|
+
Path(path).unlink()
|
|
19
20
|
else:
|
|
20
21
|
raise FileExistsError(f"File exists. Got '{path}'")
|
|
21
22
|
return path
|
|
22
23
|
|
|
23
24
|
@staticmethod
|
|
24
|
-
def to_markdown(markdown: list[str]
|
|
25
|
+
def to_markdown(markdown: list[str]) -> str:
|
|
25
26
|
"""Returns the markdown as a text string."""
|
|
26
27
|
return "\n".join(markdown)
|
|
27
28
|
|
|
28
29
|
def to_file(
|
|
29
30
|
self,
|
|
30
|
-
markdown: list[str]
|
|
31
|
+
markdown: list[str],
|
|
31
32
|
pad: int | None = None,
|
|
32
33
|
append: bool | None = None,
|
|
33
34
|
prepend: bool | None = None,
|
|
@@ -42,9 +43,9 @@ class MarkdownWriter:
|
|
|
42
43
|
else:
|
|
43
44
|
self._write(markdown)
|
|
44
45
|
|
|
45
|
-
def _write(self, markdown: str
|
|
46
|
+
def _write(self, markdown: str, mode: str | None = None) -> None:
|
|
46
47
|
mode = mode or "w"
|
|
47
|
-
with
|
|
48
|
+
with Path(self.path).open(mode) as f:
|
|
48
49
|
f.write(markdown)
|
|
49
50
|
|
|
50
51
|
def _append(self, markdown) -> None:
|
|
@@ -53,7 +54,7 @@ class MarkdownWriter:
|
|
|
53
54
|
|
|
54
55
|
def _prepend(self, markdown=None) -> None:
|
|
55
56
|
mode = "r+"
|
|
56
|
-
with
|
|
57
|
+
with Path(self.path).open(mode) as f:
|
|
57
58
|
content = f.read()
|
|
58
59
|
f.seek(0, 0)
|
|
59
60
|
f.write(markdown + "\n" + content)
|
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
from copy import copy
|
|
4
|
-
from typing import TYPE_CHECKING
|
|
5
5
|
|
|
6
6
|
import numpy as np
|
|
7
7
|
import pandas as pd
|
|
8
8
|
from django.apps import apps as django_apps
|
|
9
|
+
from django.contrib.sites.models import Site
|
|
9
10
|
from django.core.exceptions import FieldError
|
|
10
11
|
from django.db import OperationalError
|
|
12
|
+
from django.db.models import QuerySet
|
|
11
13
|
from django_crypto_fields.utils import get_encrypted_fields, has_encrypted_fields
|
|
12
14
|
from django_pandas.io import read_frame
|
|
15
|
+
from pandas import Series
|
|
13
16
|
|
|
14
|
-
from .
|
|
17
|
+
from edc_lab.models import Panel
|
|
18
|
+
from edc_list_data.model_mixins import ListModelMixin, ListUuidModelMixin
|
|
19
|
+
from edc_model.models import BaseUuidModel
|
|
20
|
+
from edc_sites.model_mixins import SiteModelMixin
|
|
15
21
|
|
|
16
|
-
|
|
17
|
-
from django.db.models import QuerySet
|
|
22
|
+
from .constants import ACTION_ITEM_COLUMNS, SYSTEM_COLUMNS
|
|
18
23
|
|
|
19
|
-
from edc_model.models import BaseUuidModel
|
|
20
|
-
from edc_sites.model_mixins import SiteModelMixin
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
class MyModel(SiteModelMixin, BaseUuidModel):
|
|
26
|
+
class Meta(BaseUuidModel.Meta):
|
|
27
|
+
pass
|
|
25
28
|
|
|
26
29
|
|
|
27
30
|
__all__ = ["ModelToDataframe", "ModelToDataframeError"]
|
|
@@ -52,16 +55,16 @@ class ModelToDataframe:
|
|
|
52
55
|
See also: get_crf()
|
|
53
56
|
"""
|
|
54
57
|
|
|
55
|
-
sys_field_names:
|
|
58
|
+
sys_field_names: tuple[str, ...] = (
|
|
56
59
|
"_state",
|
|
57
60
|
"_user_container_instance",
|
|
58
61
|
"_domain_cache",
|
|
59
62
|
"using",
|
|
60
63
|
"slug",
|
|
61
|
-
|
|
62
|
-
edc_sys_columns:
|
|
63
|
-
action_item_columns:
|
|
64
|
-
illegal_chars: dict[str] = {
|
|
64
|
+
)
|
|
65
|
+
edc_sys_columns: tuple[str, ...] = SYSTEM_COLUMNS
|
|
66
|
+
action_item_columns: tuple[str, ...] = ACTION_ITEM_COLUMNS
|
|
67
|
+
illegal_chars: dict[str, str] = { # noqa: RUF012
|
|
65
68
|
"\u2019": "'",
|
|
66
69
|
"\u2018": "'",
|
|
67
70
|
"\u201d": '"',
|
|
@@ -72,7 +75,7 @@ class ModelToDataframe:
|
|
|
72
75
|
def __init__(
|
|
73
76
|
self,
|
|
74
77
|
model: str | None = None,
|
|
75
|
-
queryset: QuerySet | None = None,
|
|
78
|
+
queryset: [QuerySet] | None = None,
|
|
76
79
|
query_filter: dict | None = None,
|
|
77
80
|
decrypt: bool | None = None,
|
|
78
81
|
drop_sys_columns: bool | None = None,
|
|
@@ -104,7 +107,7 @@ class ModelToDataframe:
|
|
|
104
107
|
try:
|
|
105
108
|
self.model_cls = django_apps.get_model(self.model)
|
|
106
109
|
except LookupError as e:
|
|
107
|
-
raise LookupError(f"Model is {self.model}. Got `{e}`")
|
|
110
|
+
raise LookupError(f"Model is {self.model}. Got `{e}`") from e
|
|
108
111
|
if self.sites:
|
|
109
112
|
try:
|
|
110
113
|
if queryset:
|
|
@@ -151,7 +154,7 @@ class ModelToDataframe:
|
|
|
151
154
|
|
|
152
155
|
dataframe = self.merge_m2ms(dataframe)
|
|
153
156
|
|
|
154
|
-
dataframe.rename(columns=self.columns
|
|
157
|
+
dataframe = dataframe.rename(columns=self.columns)
|
|
155
158
|
|
|
156
159
|
# remove timezone if asked
|
|
157
160
|
if self.remove_timezone:
|
|
@@ -174,12 +177,12 @@ class ModelToDataframe:
|
|
|
174
177
|
dataframe[column] = dataframe[column].dt.total_seconds()
|
|
175
178
|
|
|
176
179
|
# fillna
|
|
177
|
-
dataframe.fillna(value=np.nan, axis=0
|
|
180
|
+
dataframe = dataframe.fillna(value=np.nan, axis=0)
|
|
178
181
|
|
|
179
182
|
# remove illegal chars
|
|
180
183
|
for column in list(dataframe.select_dtypes(include=["object"]).columns):
|
|
181
184
|
dataframe[column] = dataframe.apply(
|
|
182
|
-
lambda x: self._clean_chars(x[
|
|
185
|
+
lambda x, col=column: self._clean_chars(x[col]), axis=1
|
|
183
186
|
)
|
|
184
187
|
self._dataframe = dataframe
|
|
185
188
|
return self._dataframe
|
|
@@ -188,7 +191,7 @@ class ModelToDataframe:
|
|
|
188
191
|
queryset = self.queryset.values_list(*self.columns).filter(**self.query_filter)
|
|
189
192
|
return pd.DataFrame(list(queryset), columns=[v for v in self.columns])
|
|
190
193
|
|
|
191
|
-
def get_dataframe_with_encrypted_fields(self, row_count: int) -> pd.DataFrame:
|
|
194
|
+
def get_dataframe_with_encrypted_fields(self, row_count: int) -> pd.DataFrame: # noqa: ARG002
|
|
192
195
|
df = read_frame(
|
|
193
196
|
self.queryset.filter(**self.query_filter), verbose=self.read_frame_verbose
|
|
194
197
|
)
|
|
@@ -233,28 +236,27 @@ class ModelToDataframe:
|
|
|
233
236
|
dataframe = dataframe.merge(df_m2m, on="id", how="left")
|
|
234
237
|
return dataframe
|
|
235
238
|
|
|
236
|
-
def _clean_chars(self, s):
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
s = s.replace(k, v)
|
|
246
|
-
except (AttributeError, TypeError):
|
|
247
|
-
break
|
|
248
|
-
return s
|
|
239
|
+
def _clean_chars(self, s: Series) -> Series:
|
|
240
|
+
if not s.empty:
|
|
241
|
+
for k, v in self.illegal_chars.items():
|
|
242
|
+
try:
|
|
243
|
+
s = s.replace(k, v)
|
|
244
|
+
except (AttributeError, TypeError):
|
|
245
|
+
break
|
|
246
|
+
return s
|
|
247
|
+
return np.nan
|
|
249
248
|
|
|
250
249
|
def move_sys_columns_to_end(self, columns: dict[str, str]) -> dict[str, str]:
|
|
251
250
|
system_columns = [
|
|
252
251
|
f.name for f in self.model_cls._meta.get_fields() if f.name in SYSTEM_COLUMNS
|
|
253
252
|
]
|
|
254
253
|
new_columns = {k: v for k, v in columns.items() if k not in system_columns}
|
|
255
|
-
if
|
|
256
|
-
|
|
257
|
-
|
|
254
|
+
if (
|
|
255
|
+
system_columns
|
|
256
|
+
and len(new_columns.keys()) != len(columns.keys())
|
|
257
|
+
and not self.drop_sys_columns
|
|
258
|
+
):
|
|
259
|
+
new_columns.update({k: k for k in system_columns})
|
|
258
260
|
return new_columns
|
|
259
261
|
|
|
260
262
|
def move_action_item_columns(self, columns: dict[str, str]) -> dict[str, str]:
|
|
@@ -262,12 +264,11 @@ class ModelToDataframe:
|
|
|
262
264
|
f.name for f in self.model_cls._meta.get_fields() if f.name in ACTION_ITEM_COLUMNS
|
|
263
265
|
]
|
|
264
266
|
new_columns = {k: v for k, v in columns.items() if k not in ACTION_ITEM_COLUMNS}
|
|
265
|
-
if action_item_columns
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
270
|
-
new_columns.update({k: k for k in ACTION_ITEM_COLUMNS})
|
|
267
|
+
if action_item_columns and (
|
|
268
|
+
len(new_columns.keys()) != len(columns.keys())
|
|
269
|
+
and not self.drop_action_item_columns
|
|
270
|
+
):
|
|
271
|
+
new_columns.update({k: k for k in ACTION_ITEM_COLUMNS})
|
|
271
272
|
return new_columns
|
|
272
273
|
|
|
273
274
|
@property
|
|
@@ -285,12 +286,10 @@ class ModelToDataframe:
|
|
|
285
286
|
columns = {col: col for col in columns_list}
|
|
286
287
|
for column_name in columns_list:
|
|
287
288
|
if column_name.endswith("_visit_id"):
|
|
288
|
-
|
|
289
|
+
with contextlib.suppress(FieldError):
|
|
289
290
|
columns = self.add_columns_for_subject_visit(
|
|
290
291
|
column_name=column_name, columns=columns
|
|
291
292
|
)
|
|
292
|
-
except FieldError:
|
|
293
|
-
pass
|
|
294
293
|
if column_name.endswith("_requisition") or column_name.endswith(
|
|
295
294
|
"requisition_id"
|
|
296
295
|
):
|
|
@@ -316,10 +315,8 @@ class ModelToDataframe:
|
|
|
316
315
|
else:
|
|
317
316
|
raise
|
|
318
317
|
for name in self.sys_field_names:
|
|
319
|
-
|
|
318
|
+
with contextlib.suppress(ValueError):
|
|
320
319
|
columns_list.remove(name)
|
|
321
|
-
except ValueError:
|
|
322
|
-
pass
|
|
323
320
|
if not self.decrypt:
|
|
324
321
|
columns_list = [col for col in columns_list if col not in self.encrypted_columns]
|
|
325
322
|
return columns_list
|
|
@@ -338,7 +335,6 @@ class ModelToDataframe:
|
|
|
338
335
|
@property
|
|
339
336
|
def list_columns(self) -> list[str]:
|
|
340
337
|
"""Return a list of column names with fk to a list model."""
|
|
341
|
-
from edc_list_data.model_mixins import ListModelMixin, ListUuidModelMixin
|
|
342
338
|
|
|
343
339
|
if not self._list_columns:
|
|
344
340
|
list_columns = []
|
|
@@ -348,14 +344,13 @@ class ModelToDataframe:
|
|
|
348
344
|
and fld_cls.related_model
|
|
349
345
|
and issubclass(fld_cls.related_model, (ListModelMixin, ListUuidModelMixin))
|
|
350
346
|
):
|
|
351
|
-
list_columns.append(fld_cls.attname)
|
|
347
|
+
list_columns.append(fld_cls.attname) # noqa: PERF401
|
|
352
348
|
self._list_columns = list(set(list_columns))
|
|
353
349
|
return self._list_columns
|
|
354
350
|
|
|
355
351
|
@property
|
|
356
352
|
def site_columns(self) -> list[str]:
|
|
357
353
|
"""Return a list of column names with fk to a site model."""
|
|
358
|
-
from django.contrib.sites.models import Site
|
|
359
354
|
|
|
360
355
|
if not self._site_columns:
|
|
361
356
|
site_columns = []
|
|
@@ -372,10 +367,6 @@ class ModelToDataframe:
|
|
|
372
367
|
@property
|
|
373
368
|
def other_columns(self) -> list[str]:
|
|
374
369
|
"""Return other column names with fk to a common models."""
|
|
375
|
-
from django.contrib.sites.models import Site
|
|
376
|
-
|
|
377
|
-
from edc_lab.models import Panel
|
|
378
|
-
|
|
379
370
|
related_model = [Site, Panel]
|
|
380
371
|
if not self._list_columns:
|
|
381
372
|
list_columns = []
|
|
@@ -7,7 +7,7 @@ __all__ = ["read_frame_edc"]
|
|
|
7
7
|
|
|
8
8
|
|
|
9
9
|
def read_frame_edc(
|
|
10
|
-
queryset: QuerySet | str = None,
|
|
10
|
+
queryset: [QuerySet] | str | None = None,
|
|
11
11
|
drop_sys_columns: bool | None = None,
|
|
12
12
|
drop_action_item_columns: bool | None = None,
|
|
13
13
|
read_frame_verbose: bool | None = None,
|
|
@@ -45,7 +45,7 @@ class ScheduleNameError(Exception):
|
|
|
45
45
|
pass
|
|
46
46
|
|
|
47
47
|
|
|
48
|
-
class AlreadyRegisteredVisit(Exception):
|
|
48
|
+
class AlreadyRegisteredVisit(Exception): # noqa: N818
|
|
49
49
|
pass
|
|
50
50
|
|
|
51
51
|
|
|
@@ -69,15 +69,15 @@ class Schedule:
|
|
|
69
69
|
|
|
70
70
|
def __init__(
|
|
71
71
|
self,
|
|
72
|
-
name
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
offschedule_model: str = None,
|
|
76
|
-
loss_to_followup_model: str = None,
|
|
77
|
-
appointment_model: str | None = None,
|
|
78
|
-
history_model: str | None = None,
|
|
72
|
+
name: str,
|
|
73
|
+
onschedule_model: str,
|
|
74
|
+
offschedule_model: str,
|
|
79
75
|
consent_definitions: list[ConsentDefinition] | ConsentDefinition = None,
|
|
76
|
+
loss_to_followup_model: str | None = None,
|
|
77
|
+
appointment_model: str | None = None,
|
|
80
78
|
offstudymedication_model: str | None = None,
|
|
79
|
+
history_model: str | None = None,
|
|
80
|
+
verbose_name: str | None = None,
|
|
81
81
|
sequence: str | None = None,
|
|
82
82
|
base_timepoint: float | Decimal | None = None,
|
|
83
83
|
):
|
|
@@ -146,10 +146,10 @@ class Schedule:
|
|
|
146
146
|
|
|
147
147
|
def visits_for_subject(
|
|
148
148
|
self,
|
|
149
|
-
subject_identifier: str
|
|
150
|
-
report_datetime: datetime
|
|
151
|
-
site_id: int = None,
|
|
152
|
-
consent_definition: ConsentDefinition = None,
|
|
149
|
+
subject_identifier: str,
|
|
150
|
+
report_datetime: datetime,
|
|
151
|
+
site_id: int | None = None,
|
|
152
|
+
consent_definition: ConsentDefinition | None = None,
|
|
153
153
|
) -> VisitCollection:
|
|
154
154
|
"""Returns a deep copy of visits collection filtered for a
|
|
155
155
|
given consented subject.
|
|
@@ -352,7 +352,7 @@ class Schedule:
|
|
|
352
352
|
|
|
353
353
|
def get_consent_definition(
|
|
354
354
|
self,
|
|
355
|
-
report_datetime: datetime
|
|
355
|
+
report_datetime: datetime,
|
|
356
356
|
site: SingleSite = None,
|
|
357
357
|
consent_definition: ConsentDefinition = None,
|
|
358
358
|
) -> ConsentDefinition:
|
|
File without changes
|
|
File without changes
|