meta-edc 0.3.37__py3-none-any.whl → 0.3.39__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.
Files changed (87) hide show
  1. meta_analytics/__init__.py +0 -2
  2. meta_analytics/dataframes/__init__.py +1 -0
  3. meta_analytics/dataframes/get_last_imp_visits_df.py +101 -0
  4. meta_auth/auth_objects.py +1 -0
  5. meta_consent/consents.py +6 -0
  6. meta_consent/locale/lg/LC_MESSAGES/django.po +69 -0
  7. meta_consent/locale/sw/LC_MESSAGES/django.po +12 -12
  8. meta_dashboard/locale/lg/LC_MESSAGES/django.po +30 -0
  9. meta_dashboard/locale/sw/LC_MESSAGES/django.po +11 -2
  10. meta_dashboard/navbars.py +2 -0
  11. meta_dashboard/tests/urls.py +0 -1
  12. meta_dashboard/view_utils/subject_screening_button.py +2 -2
  13. meta_edc/__init__.py +3 -10
  14. meta_edc/{celery/debug.py → celery.py} +2 -2
  15. meta_edc/navbars.py +2 -0
  16. meta_edc/settings/debug.py +4 -0
  17. meta_edc/settings/defaults.py +29 -16
  18. meta_edc/templates/meta_edc/bootstrap3/home.html +1 -1
  19. meta_edc/urls.py +1 -0
  20. {meta_edc-0.3.37.dist-info → meta_edc-0.3.39.dist-info}/METADATA +6 -6
  21. {meta_edc-0.3.37.dist-info → meta_edc-0.3.39.dist-info}/RECORD +72 -61
  22. {meta_edc-0.3.37.dist-info → meta_edc-0.3.39.dist-info}/WHEEL +1 -1
  23. meta_pharmacy/admin/__init__.py +0 -1
  24. meta_pharmacy/admin/actions.py +38 -0
  25. meta_pharmacy/admin_site.py +5 -1
  26. meta_pharmacy/apps.py +2 -0
  27. meta_pharmacy/forms/__init__.py +2 -0
  28. meta_pharmacy/forms/rx_form.py +16 -0
  29. meta_pharmacy/{forms.py → forms/substitutions_form.py} +1 -14
  30. meta_pharmacy/label_configs.py +30 -0
  31. meta_pharmacy/labels/__init__.py +3 -1
  32. meta_pharmacy/labels/draw_label_for_subject_with_barcode.py +58 -0
  33. meta_pharmacy/labels/draw_label_for_subject_with_code128.py +14 -0
  34. meta_pharmacy/labels/draw_label_with_test_data.py +26 -0
  35. meta_pharmacy/labels/label_data.py +14 -0
  36. meta_pharmacy/labels/print_sheets.py +22 -11
  37. meta_pharmacy/list_data.py +8 -0
  38. meta_pharmacy/management/commands/__init__.py +0 -0
  39. meta_pharmacy/management/commands/update_initial_pharmacy_data.py +10 -0
  40. meta_pharmacy/migrations/0002_initial.py +7 -5
  41. meta_pharmacy/migrations/0008_remove_lotnumber_medication_and_more.py +390 -0
  42. meta_pharmacy/migrations/0009_remove_historicalrx_slug.py +17 -0
  43. meta_pharmacy/models/__init__.py +1 -2
  44. meta_pharmacy/models/label_data.py +38 -0
  45. meta_pharmacy/models/{label.py → rx_label.py} +8 -19
  46. meta_pharmacy/utils/__init__.py +1 -0
  47. meta_pharmacy/utils/update_initial_pharmacy_data.py +146 -0
  48. meta_prn/admin/pregnancy_notification_admin.py +6 -2
  49. meta_reports/admin/__init__.py +1 -0
  50. meta_reports/admin/dbviews/glucose_summary_admin.py +1 -1
  51. meta_reports/admin/endpoints_admin.py +1 -1
  52. meta_reports/admin/last_imp_refill_admin.py +181 -0
  53. meta_reports/admin/modeladmin_mixins.py +3 -3
  54. meta_reports/migrations/0052_lastimpvisit.py +57 -0
  55. meta_reports/migrations/0053_rename_lastimpvisit_lastimprefill_and_more.py +31 -0
  56. meta_reports/models/__init__.py +1 -0
  57. meta_reports/models/last_imp_refill.py +34 -0
  58. meta_subject/admin/study_medication_admin.py +10 -0
  59. meta_subject/forms/study_medication_form.py +35 -0
  60. meta_subject/locale/lg/LC_MESSAGES/django.po +470 -0
  61. meta_subject/locale/sw/LC_MESSAGES/django.po +191 -89
  62. meta_subject/metadata_rules/predicates.py +34 -7
  63. meta_subject/migrations/0214_historicalstudymedication_stock_codes_and_more.py +44 -0
  64. meta_subject/migrations/0215_alter_historicalstudymedication_stock_codes_and_more.py +46 -0
  65. meta_subject/tests/tests/test_mnsi.py +180 -113
  66. meta_visit_schedule/visit_schedules/phase_three/crfs.py +18 -6
  67. tests/test_settings.py +4 -1
  68. meta_edc/celery/__init__.py +0 -2
  69. meta_edc/celery/live.py +0 -17
  70. meta_edc/celery/uat.py +0 -17
  71. meta_pharmacy/admin/lot_number_admin.py +0 -43
  72. meta_pharmacy/labels/get_label_data.py +0 -30
  73. meta_pharmacy/models/lot_number.py +0 -25
  74. meta_reports/models/unmanaged/README +0 -14
  75. meta_reports/models/unmanaged/patient_history_missing_baseline_cd4.py +0 -26
  76. meta_reports/models/unmanaged/patient_history_missing_baseline_cd4.sql +0 -10
  77. meta_reports/models/unmanaged/unattended_three_in_row.py +0 -24
  78. meta_reports/models/unmanaged/unattended_three_in_row.sql +0 -19
  79. meta_reports/models/unmanaged/unattended_three_in_row2.py +0 -24
  80. meta_reports/models/unmanaged/unattended_three_in_row2.sql +0 -39
  81. meta_reports/models/unmanaged/unattended_two_in_row.py +0 -22
  82. meta_reports/models/unmanaged/unattended_two_in_row.sql +0 -19
  83. {meta_reports/models/unmanaged → meta_edc/migrations}/__init__.py +0 -0
  84. {meta_edc-0.3.37.dist-info → meta_edc-0.3.39.dist-info}/AUTHORS +0 -0
  85. {meta_edc-0.3.37.dist-info → meta_edc-0.3.39.dist-info}/LICENSE +0 -0
  86. {meta_edc-0.3.37.dist-info → meta_edc-0.3.39.dist-info}/top_level.txt +0 -0
  87. /meta_pharmacy/{admin/label_admin.py → management/__init__.py} +0 -0
@@ -0,0 +1,146 @@
1
+ from django.contrib.sites.models import Site
2
+ from django.core.exceptions import ObjectDoesNotExist
3
+ from django_pylabels.models import LabelSpecification
4
+ from edc_pharmacy.models import (
5
+ Assignment,
6
+ Container,
7
+ ContainerType,
8
+ ContainerUnits,
9
+ Formulation,
10
+ Location,
11
+ Product,
12
+ Supplier,
13
+ )
14
+ from edc_pylabels.models import LabelConfiguration
15
+ from edc_pylabels.site_label_configs import site_label_configs
16
+
17
+
18
+ def update_initial_pharmacy_data():
19
+ update_assignment()
20
+ update_container()
21
+ update_location()
22
+ update_product()
23
+ update_supplier()
24
+ update_labels()
25
+
26
+
27
+ def update_assignment():
28
+ """For a trial with just active and placebo.
29
+
30
+ Better to get these labels from edc_randomizer.
31
+ """
32
+ for assignment in ["placebo", "active"]:
33
+ try:
34
+ Assignment.objects.get(name=assignment)
35
+ except ObjectDoesNotExist:
36
+ Assignment.objects.create(name=assignment, display_name=assignment.title())
37
+
38
+
39
+ def update_container():
40
+ """Here we order a number of tablets. The manufacturer sends
41
+ the order in large containers, barrels of about 30K tablets
42
+ per barrel. We repack/decant into bottles of 128 tablets
43
+ """
44
+ tablet_type = ContainerType.objects.get(name="tablet")
45
+ bottle_type = ContainerType.objects.get(name="bottle")
46
+ units = ContainerUnits.objects.get(name="tablet")
47
+ opts = {
48
+ "tablet": dict(
49
+ name="tablet",
50
+ display_name="Tablet",
51
+ container_type=tablet_type,
52
+ units=units,
53
+ qty=1,
54
+ may_order_as=True,
55
+ max_per_subject=0,
56
+ ),
57
+ "bottle30k": dict(
58
+ name="bottle30k",
59
+ display_name="Barrel 30K",
60
+ container_type=bottle_type,
61
+ units=units,
62
+ qty=30000,
63
+ may_receive_as=True,
64
+ max_per_subject=0,
65
+ ),
66
+ "bottle128": dict(
67
+ name="bottle128",
68
+ display_name="Bottle 128",
69
+ container_type=bottle_type,
70
+ units=units,
71
+ qty=128,
72
+ max_per_subject=3,
73
+ may_repack_as=True,
74
+ may_request_as=True,
75
+ may_dispense_as=True,
76
+ ),
77
+ }
78
+ for name, data in opts.items():
79
+ try:
80
+ Container.objects.get(name=name)
81
+ except ObjectDoesNotExist:
82
+ Container.objects.create(**data)
83
+
84
+
85
+ def update_location():
86
+ """Base the locations on the sites in the trial plus
87
+ a "central" pharmacy"""
88
+ for obj in Location.objects.exclude(name="central"):
89
+ obj.site_id = Site.objects.get(name=obj.name).id
90
+ obj.save(update_fields=["site_id"])
91
+
92
+
93
+ def update_product():
94
+ """Define the product, in this case just two; active and placebo.
95
+
96
+ Formulation is defined before running this script.
97
+
98
+ In this case the formulation is just the study drug/IMP.
99
+ """
100
+ formulation = Formulation.objects.get()
101
+ active = Assignment.objects.get(name="active")
102
+ placebo = Assignment.objects.get(name="placebo")
103
+ try:
104
+ Product.objects.get(formulation=formulation, assignment=active)
105
+ except ObjectDoesNotExist:
106
+ Product(assignment=active, formulation=formulation).save()
107
+ try:
108
+ Product.objects.get(formulation=formulation, assignment=placebo)
109
+ except ObjectDoesNotExist:
110
+ Product(assignment=placebo, formulation=formulation).save()
111
+
112
+
113
+ def update_supplier():
114
+ """In this case MERCK"""
115
+ try:
116
+ Supplier.objects.get(name="merck")
117
+ except ObjectDoesNotExist:
118
+ Supplier(name="merck").save()
119
+
120
+
121
+ def update_labels():
122
+ """The default label spec is a 2 x 6 label sheet
123
+
124
+ Add "label congigs" as registered in the "site_label_configs"
125
+ global. In this case there are three labels:
126
+ * a bulk label for the barrels
127
+ * a generic vertical label for decanted stock (bottles of 128)
128
+ * a patient label for allocated stock (bottles of 128)
129
+
130
+ The patient label will be placed over the generic vertical label
131
+ once the stock item is allocated to a subject.
132
+ """
133
+ try:
134
+ default = LabelSpecification.objects.get(name="default")
135
+ except ObjectDoesNotExist:
136
+ LabelSpecification().save()
137
+ default = LabelSpecification.objects.get(name="default")
138
+
139
+ for name, label_config in site_label_configs.registry.items():
140
+ LabelConfiguration.objects.create(name=name, label_specification=default)
141
+ for label_configuration in LabelConfiguration.objects.filter(name__contains="patient"):
142
+ label_configuration.requires_allocation = True
143
+ label_configuration.save()
144
+
145
+
146
+ __all__ = ["update_initial_pharmacy_data"]
@@ -57,11 +57,15 @@ class PregnancyNotificationAdmin(
57
57
  "subject_identifier",
58
58
  "dashboard",
59
59
  "edd",
60
- "may_contact",
60
+ "contact_agreed",
61
61
  )
62
62
  return custom_fields + tuple(f for f in list_display if f not in custom_fields)
63
63
 
64
64
  def get_list_filter(self, request) -> Tuple[str, ...]:
65
- list_filter = super().get_list_display(request)
65
+ list_filter = super().get_list_filter(request)
66
66
  custom_fields = ("edd", "may_contact")
67
67
  return custom_fields + tuple(f for f in list_filter if f not in custom_fields)
68
+
69
+ @admin.display(description="May contact?", ordering="may_contact")
70
+ def contact_agreed(self, obj):
71
+ return obj.may_contact
@@ -12,3 +12,4 @@ from .dbviews import (
12
12
  )
13
13
  from .endpoints_admin import EndpointsAdmin
14
14
  from .endpoints_all_admin import EndpointsAllAdmin
15
+ from .last_imp_refill_admin import LastImpRefillAdmin
@@ -62,7 +62,7 @@ class GlucoseSummaryAdmin(
62
62
  except ObjectDoesNotExist:
63
63
  value = None
64
64
  else:
65
- if endpoint_obj.offstudy_datetime:
65
+ if endpoint_obj.offstudy_date:
66
66
  url = reverse("meta_reports_admin:meta_reports_endpointsproxy_changelist")
67
67
  title = f"Go to {EndpointsProxy._meta.verbose_name}"
68
68
  else:
@@ -8,7 +8,7 @@ from .modeladmin_mixins import EndpointsModelAdminMixin
8
8
 
9
9
  @admin.register(Endpoints, site=meta_reports_admin)
10
10
  class EndpointsAdmin(EndpointsModelAdminMixin, admin.ModelAdmin):
11
- queryset_filter = dict(offstudy_datetime__isnull=True)
11
+ queryset_filter = dict(offstudy_date__isnull=True)
12
12
 
13
13
  def rendered_change_list_note(self):
14
14
  return render_to_string("meta_reports/endpoints_change_list_note.html")
@@ -0,0 +1,181 @@
1
+ from django.contrib import admin, messages
2
+ from django.core.exceptions import FieldDoesNotExist
3
+ from django.db import models
4
+ from django.db.models import QuerySet
5
+ from django.utils.html import format_html
6
+ from edc_model_admin.dashboard import ModelAdminDashboardMixin
7
+ from edc_model_admin.list_filters import PastDateListFilter
8
+ from edc_model_admin.mixins import TemplatesModelAdminMixin
9
+ from edc_pdutils.actions import export_to_csv
10
+ from edc_qareports.modeladmin_mixins import QaReportModelAdminMixin
11
+ from edc_sites.admin import SiteModelAdminMixin
12
+ from edc_sites.admin.list_filters import SiteListFilter
13
+ from edc_utils import get_utcnow
14
+
15
+ from meta_analytics.dataframes import get_last_imp_visits_df
16
+
17
+ from ..admin_site import meta_reports_admin
18
+ from ..models import LastImpRefill
19
+
20
+
21
+ class ImpVisitDateListFilter(PastDateListFilter):
22
+ title = "IMP visit date"
23
+
24
+ parameter_name = "imp_visit_date"
25
+ field_name = "imp_visit_date"
26
+
27
+
28
+ class NextApptDateListFilter(PastDateListFilter):
29
+ title = "Next appt date"
30
+
31
+ parameter_name = "next_appt_date"
32
+ field_name = "next_appt_date"
33
+
34
+
35
+ def update_report(modeladmin, request, queryset):
36
+ now = get_utcnow()
37
+ modeladmin.model.objects.all().delete()
38
+ df = get_last_imp_visits_df()
39
+ if not df.empty:
40
+ data = [
41
+ modeladmin.model(
42
+ subject_identifier=row["subject_identifier"],
43
+ site_id=row["site_id"],
44
+ imp_visit_date=row["imp_visit_date"],
45
+ imp_visit_code=row["imp_visit_code"],
46
+ next_appt_date=row["next_appt_datetime"],
47
+ next_visit_code=row["next_visit_code"],
48
+ days_since=row["days_since"].days,
49
+ days_until=row["days_until"].days,
50
+ visit_code=str(int(row["imp_visit_code"])),
51
+ visit_code_sequence=row["imp_visit_code"] % 1,
52
+ report_model=modeladmin.model._meta.label_lower,
53
+ created=now,
54
+ )
55
+ for _, row in df.iterrows()
56
+ ]
57
+ created = len(modeladmin.model.objects.bulk_create(data))
58
+ messages.success(request, "{} records were successfully created.".format(created))
59
+
60
+
61
+ @admin.register(LastImpRefill, site=meta_reports_admin)
62
+ class LastImpRefillAdmin(
63
+ QaReportModelAdminMixin,
64
+ SiteModelAdminMixin,
65
+ ModelAdminDashboardMixin,
66
+ TemplatesModelAdminMixin,
67
+ admin.ModelAdmin,
68
+ ):
69
+ include_note_column = False
70
+
71
+ change_list_title = "List of most recent IMP refills per subject"
72
+
73
+ change_list_note = format_html(
74
+ """
75
+ This report fetches the most recent Study Medication report where IMP was
76
+ refilled and adds the subject's next visit. Subjects taken "Off Schedule"
77
+ are not included in this report. To update ALL rows in this report, tick
78
+ at least one row and select 'Update report' action below.
79
+ <BR><BR>
80
+ This report has additional search features for numeric columns:
81
+ <code>days_since</code>, <code>days_until</code>, <code>imp_visit_code</code>
82
+ and <code>next_visit_code</code>.
83
+ <BR><BR>For example, type <code>days_until>=25</code> in the search below
84
+ to show rows for subjects who have an appointment 25 or more days from the
85
+ date this report was created. You might also try typing
86
+ <code>days_since>365</code> or <code>days_until<0</code>.
87
+ <BR><BR>
88
+ This also works: <code>next_visit_code>=1060</code>.
89
+ """
90
+ )
91
+
92
+ actions = [update_report, export_to_csv]
93
+
94
+ list_display = [
95
+ "dashboard",
96
+ "subject_identifier",
97
+ "imp_visit_code",
98
+ "imp_visit_date",
99
+ "days_since",
100
+ "next_visit_code",
101
+ "next_appt_date",
102
+ "days_until",
103
+ "created",
104
+ ]
105
+
106
+ list_filter = [
107
+ ImpVisitDateListFilter,
108
+ NextApptDateListFilter,
109
+ "imp_visit_code",
110
+ "next_visit_code",
111
+ SiteListFilter,
112
+ ]
113
+
114
+ search_fields = ["subject_identifier"]
115
+
116
+ def get_queryset(self, request) -> QuerySet:
117
+ qs = super().get_queryset(request)
118
+ if qs.count() == 0:
119
+ update_report(self, request, None)
120
+ qs = super().get_queryset(request)
121
+ return qs
122
+
123
+ def dataframe_to_model(self):
124
+ now = get_utcnow()
125
+ self.model.objects.all().delete()
126
+ created = 0
127
+ df = get_last_imp_visits_df()
128
+ if not df.empty:
129
+ data = [
130
+ self.model(
131
+ subject_identifier=row["subject_identifier"],
132
+ site_id=row["site_id"],
133
+ imp_visit_date=row["imp_visit_date"],
134
+ imp_visit_code=row["imp_visit_code"],
135
+ next_appt_date=row["next_appt_datetime"],
136
+ next_visit_code=row["next_visit_code"],
137
+ days_since=row["days_since"],
138
+ days_until=row["days_until"],
139
+ visit_code=str(int(row["imp_visit_code"])),
140
+ visit_code_sequence=row["imp_visit_code"] % 1,
141
+ report_model=self.model._meta.label_lower,
142
+ created=now,
143
+ )
144
+ for _, row in df.iterrows()
145
+ ]
146
+ created = len(self.model.objects.bulk_create(data))
147
+ return created
148
+
149
+ def get_search_results(self, request, queryset, search_term):
150
+ value = None
151
+ fldname = None
152
+ search_term = search_term.replace(" ", "")
153
+ operators = {">=": "gte", "<=": "lte", "<": "lt", ">": "gt"}
154
+ if op := [op for op in operators if op in search_term]:
155
+ op = op[0]
156
+ fldname, value = search_term.split(op)
157
+ try:
158
+ fldcls = self.model._meta.get_field(fldname)
159
+ except FieldDoesNotExist:
160
+ fldname = None
161
+ else:
162
+ if isinstance(fldcls, (models.IntegerField,)):
163
+ try:
164
+ value = int(value)
165
+ except ValueError:
166
+ value = None
167
+ elif isinstance(fldcls, (models.FloatField,)):
168
+ try:
169
+ value = float(value)
170
+ except ValueError:
171
+ value = None
172
+ if (
173
+ value is not None
174
+ and fldname
175
+ and fldname in ["days_until", "days_since", "imp_visit_code", "next_visit_code"]
176
+ ):
177
+ queryset, use_distinct = super().get_search_results(request, queryset, None)
178
+ queryset = queryset.filter(**{f"{fldname}__{operators[op]}": value})
179
+ else:
180
+ queryset, use_distinct = super().get_search_results(request, queryset, search_term)
181
+ return queryset, use_distinct
@@ -79,9 +79,9 @@ class EndpointsModelAdminMixin(
79
79
  def visit(self, obj=None):
80
80
  return obj.visit_code
81
81
 
82
- @admin.display(description="FBG DATE", ordering="fbg_date")
83
- def fbg_date(self, obj=None):
84
- return obj.fbg_datetime.date() if obj.fbg_datetime else None
82
+ # @admin.display(description="FBG DATE", ordering="fbg_date")
83
+ # def fbg_date(self, obj=None):
84
+ # return obj.fbg_datetime.date() if obj.fbg_datetime else None
85
85
 
86
86
  @admin.display(description="FAST", ordering="fasting")
87
87
  def fast(self, obj=None):
@@ -0,0 +1,57 @@
1
+ # Generated by Django 5.1.2 on 2024-10-16 00:53
2
+
3
+ import django.db.models.deletion
4
+ import edc_utils.date
5
+ from django.db import migrations, models
6
+
7
+
8
+ class Migration(migrations.Migration):
9
+
10
+ dependencies = [
11
+ ("meta_reports", "0051_remove_endpoints_baseline_datetime_and_more"),
12
+ ("sites", "0002_alter_domain_unique"),
13
+ ]
14
+
15
+ operations = [
16
+ migrations.CreateModel(
17
+ name="LastImpVisit",
18
+ fields=[
19
+ (
20
+ "id",
21
+ models.BigAutoField(
22
+ auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
23
+ ),
24
+ ),
25
+ ("report_model", models.CharField(max_length=50)),
26
+ ("subject_identifier", models.CharField(max_length=25)),
27
+ ("created", models.DateTimeField(default=edc_utils.date.get_utcnow)),
28
+ ("reference_date", models.DateField(null=True)),
29
+ ("imp_visit_code", models.FloatField(null=True)),
30
+ ("imp_visit_date", models.DateField(null=True)),
31
+ ("next_visit_code", models.FloatField(null=True)),
32
+ ("next_appt_date", models.DateField(null=True)),
33
+ ("visit_code", models.CharField(max_length=15, null=True)),
34
+ ("visit_code_sequence", models.IntegerField(null=True)),
35
+ ("days_since", models.IntegerField(null=True)),
36
+ ("days_until", models.IntegerField(null=True)),
37
+ (
38
+ "site",
39
+ models.ForeignKey(
40
+ on_delete=django.db.models.deletion.DO_NOTHING, to="sites.site"
41
+ ),
42
+ ),
43
+ ],
44
+ options={
45
+ "verbose_name": "Last IMP Visit",
46
+ "verbose_name_plural": "Last IMP Visits",
47
+ "abstract": False,
48
+ "default_permissions": ("view", "export", "viewallsites"),
49
+ "indexes": [
50
+ models.Index(
51
+ fields=["subject_identifier", "site"],
52
+ name="meta_report_subject_4a9e15_idx",
53
+ )
54
+ ],
55
+ },
56
+ ),
57
+ ]
@@ -0,0 +1,31 @@
1
+ # Generated by Django 5.1.2 on 2024-10-16 01:39
2
+
3
+ from django.db import migrations
4
+
5
+
6
+ class Migration(migrations.Migration):
7
+
8
+ dependencies = [
9
+ ("meta_reports", "0052_lastimpvisit"),
10
+ ("sites", "0002_alter_domain_unique"),
11
+ ]
12
+
13
+ operations = [
14
+ migrations.RenameModel(
15
+ old_name="LastImpVisit",
16
+ new_name="LastImpRefill",
17
+ ),
18
+ migrations.AlterModelOptions(
19
+ name="lastimprefill",
20
+ options={
21
+ "default_permissions": ("view", "export", "viewallsites"),
22
+ "verbose_name": "Last IMP Refill",
23
+ "verbose_name_plural": "Last IMP Refills",
24
+ },
25
+ ),
26
+ migrations.RenameIndex(
27
+ model_name="lastimprefill",
28
+ new_name="meta_report_subject_bc8268_idx",
29
+ old_name="meta_report_subject_4a9e15_idx",
30
+ ),
31
+ ]
@@ -13,3 +13,4 @@ from .dbviews import (
13
13
  )
14
14
  from .endpoints import Endpoints
15
15
  from .endpoints_proxy import EndpointsProxy
16
+ from .last_imp_refill import LastImpRefill
@@ -0,0 +1,34 @@
1
+ from django.db import models
2
+ from django_pandas.managers import DataFrameManager
3
+ from edc_qareports.model_mixins import QaReportModelMixin, qa_reports_permissions
4
+
5
+
6
+ class LastImpRefill(QaReportModelMixin, models.Model):
7
+
8
+ reference_date = models.DateField(null=True)
9
+
10
+ imp_visit_code = models.FloatField(null=True)
11
+
12
+ imp_visit_date = models.DateField(null=True)
13
+
14
+ next_visit_code = models.FloatField(null=True)
15
+
16
+ next_appt_date = models.DateField(null=True)
17
+
18
+ visit_code = models.CharField(max_length=15, null=True)
19
+
20
+ visit_code_sequence = models.IntegerField(null=True)
21
+
22
+ days_since = models.IntegerField(null=True)
23
+
24
+ days_until = models.IntegerField(null=True)
25
+
26
+ objects = DataFrameManager()
27
+
28
+ def recreate_db_view(self, **kwargs):
29
+ raise NotImplementedError()
30
+
31
+ class Meta(QaReportModelMixin.Meta):
32
+ verbose_name = "Last IMP Refill"
33
+ verbose_name_plural = "Last IMP Refills"
34
+ default_permissions = qa_reports_permissions
@@ -64,6 +64,16 @@ class StudyMedicationAdmin(CrfModelAdminMixin, SimpleHistoryAdmin):
64
64
  ),
65
65
  },
66
66
  ),
67
+ (
68
+ "Dispensed medication",
69
+ {
70
+ "description": (
71
+ "Type in the stock code on the medication "
72
+ "bottle or bottles. Separate by comma."
73
+ ),
74
+ "fields": ("stock_codes",),
75
+ },
76
+ ),
67
77
  crf_status_fieldset,
68
78
  audit_fieldset_tuple,
69
79
  )
@@ -1,9 +1,12 @@
1
+ import re
2
+
1
3
  from django import forms
2
4
  from edc_crf.modelform_mixins import CrfModelFormMixin
3
5
  from edc_form_validators import INVALID_ERROR
4
6
  from edc_pharmacy.form_validators import (
5
7
  StudyMedicationFormValidator as BaseStudyMedicationFormValidator,
6
8
  )
9
+ from edc_pharmacy.models import Stock
7
10
  from edc_visit_schedule.constants import DAY1
8
11
 
9
12
  from ..models import StudyMedication
@@ -13,6 +16,7 @@ class StudyMedicationFormValidator(BaseStudyMedicationFormValidator):
13
16
  def clean(self):
14
17
  super().clean()
15
18
  self.validate_half_dose_at_baseline()
19
+ self.validate_stock_codes_are_dispensed()
16
20
 
17
21
  def validate_half_dose_at_baseline(self):
18
22
  """Require 1000mg dose at baseline"""
@@ -32,6 +36,37 @@ class StudyMedicationFormValidator(BaseStudyMedicationFormValidator):
32
36
  {"dosage_guideline": "Invalid. Expected 1000mg/day at baseline"}
33
37
  )
34
38
 
39
+ def validate_stock_codes_are_dispensed(self):
40
+ if self.cleaned_data.get("stock_codes"):
41
+ pattern = re.compile("^([A-Z0-9]{6})(,[A-Z0-9]{6})*$")
42
+ if not pattern.match(self.cleaned_data.get("stock_codes")):
43
+ raise forms.ValidationError(
44
+ {
45
+ "stock_codes": (
46
+ "Invalid format. Enter one or more valid codes separated by comma"
47
+ )
48
+ }
49
+ )
50
+ for stock_code in self.cleaned_data.get("stock_codes").split(","):
51
+ try:
52
+ Stock.objects.get(
53
+ code=stock_code,
54
+ dispensed=True,
55
+ allocation__registered_subject__subject_identifier=(
56
+ self.subject_identifier
57
+ ),
58
+ )
59
+ except Stock.DoesNotExist:
60
+ raise forms.ValidationError(
61
+ {
62
+ "stock_codes": (
63
+ f"Invalid. Got {stock_code}. "
64
+ "Either not allocated to this subject or not dispensed. "
65
+ "Please check the bottle or check with your pharmacist."
66
+ )
67
+ }
68
+ )
69
+
35
70
 
36
71
  class StudyMedicationForm(CrfModelFormMixin, forms.ModelForm):
37
72
  form_validator_cls = StudyMedicationFormValidator