diff --git a/labs/__init__.py b/labs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/labs/admin.py b/labs/admin.py new file mode 100644 index 0000000..07b420b --- /dev/null +++ b/labs/admin.py @@ -0,0 +1,14 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.contrib import admin +from .models import * + +# Register your models here. + +admin.site.register(ContinuousMeasurement) +admin.site.register(DiscreteMeasurement) +admin.site.register(Lab) +admin.site.register(LabType) +admin.site.register(MeasurementType) +admin.site.register(DiscreteResultType) \ No newline at end of file diff --git a/labs/apps.py b/labs/apps.py new file mode 100644 index 0000000..1ce5de5 --- /dev/null +++ b/labs/apps.py @@ -0,0 +1,8 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.apps import AppConfig + + +class LabsConfig(AppConfig): + name = 'labs' diff --git a/labs/fixtures/labs.json b/labs/fixtures/labs.json new file mode 100644 index 0000000..4746579 --- /dev/null +++ b/labs/fixtures/labs.json @@ -0,0 +1 @@ +[{"model": "labs.labtype", "pk": "A1c", "fields": {}}, {"model": "labs.labtype", "pk": "BMP", "fields": {}}, {"model": "labs.labtype", "pk": "HIV", "fields": {}}, {"model": "labs.labtype", "pk": "Urinalysis", "fields": {}}, {"model": "labs.measurementtype", "pk": "A1c", "fields": {"short_name": "A1c", "unit": "%", "lab_type": "A1c"}}, {"model": "labs.measurementtype", "pk": "HIV Test", "fields": {"short_name": "HIV", "unit": "", "lab_type": "HIV"}}, {"model": "labs.measurementtype", "pk": "Nitrite", "fields": {"short_name": "Nitrite", "unit": "", "lab_type": "Urinalysis"}}, {"model": "labs.measurementtype", "pk": "Potassium, serum", "fields": {"short_name": "K+", "unit": "mmol/L", "lab_type": "BMP"}}, {"model": "labs.measurementtype", "pk": "Sodium, serum", "fields": {"short_name": "Na+", "unit": "mmol/L", "lab_type": "BMP"}}, {"model": "labs.discreteresulttype", "pk": "Negative", "fields": {"measurement_type": ["HIV Test", "Nitrite"]}}, {"model": "labs.discreteresulttype", "pk": "Positive", "fields": {"measurement_type": ["HIV Test", "Nitrite"]}}] \ No newline at end of file diff --git a/labs/forms.py b/labs/forms.py new file mode 100644 index 0000000..0529095 --- /dev/null +++ b/labs/forms.py @@ -0,0 +1,94 @@ +from django.forms import ( + fields, ModelForm, ModelChoiceField, ModelMultipleChoiceField, DecimalField, RadioSelect,Form +) +from django.shortcuts import render, redirect, get_object_or_404 +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Submit, Layout, Div, Row, HTML, Field +from crispy_forms.bootstrap import ( + InlineCheckboxes, AppendedText, PrependedText) +from pttrack.models import Patient +from . import models +from django.db.models import DateTimeField, ForeignKey +import django.db +import decimal + + +# Create a lab object to a patient without any measurements +class LabCreationForm(ModelForm): + class Meta: + model = models.Lab + exclude = ['patient','written_datetime'] + + def __init__(self, *args, **kwargs): + patient_obj = kwargs.pop('pt') + patient_name = patient_obj.name() + super(LabCreationForm, self).__init__(*args, **kwargs) + self.helper = FormHelper() + self.helper.form_method = 'post' + + self.helper.layout = Layout( + Row(HTML('

Lab

'), + HTML('

Patient name: %s

' %patient_name), + Div('lab_type', css_class='col-sm-6')), + + Submit('choose-lab', 'Choose Lab', css_class='btn btn-success') + ) + + +# Fill in corresponding measurements in a lab object +class MeasurementsCreationForm(Form): + def __init__(self, *args, **kwargs): + self.qs_fields = kwargs.pop('qs_mt') + self.new_lab = kwargs.pop('new_lab') + + # Should check if the lab is already filled with measurments + # How to check? + + super(MeasurementsCreationForm, self).__init__(*args, **kwargs) + + pt = self.new_lab.patient + + pt_info = Row(HTML('

Lab

'), + HTML('

Patient name: %s

' %pt.name()), + HTML('

Lab type: %s

' %self.new_lab.lab_type)) + + self.fieldsss = [pt_info] + for measurement_type in self.qs_fields: + str_name = measurement_type.short_name + unit = measurement_type.unit + self.fieldsss.append(Field(str_name)) + self.fieldsss[-1] = AppendedText(str_name,unit) + + value_qs=models.DiscreteResultType.objects.filter(measurement_type=measurement_type) + if len(value_qs)==0: + self.fields[str_name] = DecimalField() + else: + self.fields[str_name]=ModelChoiceField(queryset=value_qs) + + + self.helper = FormHelper() + self.helper.form_method = 'post' + + if len(self.fieldsss)==0: + button = [] + else: + button = [Submit('save-lab', 'Save Lab', css_class='btn btn-success')] + + self.helper.layout = Layout( + *(self.fieldsss + button) + ) + + def save(self): + for field in self.qs_fields: + if type(self.cleaned_data[field.short_name])==decimal.Decimal: + models.ContinuousMeasurement.objects.create( + measurement_type = field, + lab = self.new_lab, + value = self.cleaned_data[field.short_name] + ) + else: + models.DiscreteMeasurement.objects.create( + measurement_type = field, + lab = self.new_lab, + value = self.cleaned_data[field.short_name] + ) diff --git a/labs/migrations/0001_initial.py b/labs/migrations/0001_initial.py new file mode 100644 index 0000000..07c5916 --- /dev/null +++ b/labs/migrations/0001_initial.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.28 on 2020-05-24 18:40 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('pttrack', '0010_auto_20200524_1340'), + ] + + operations = [ + migrations.CreateModel( + name='DiscreteResultType', + fields=[ + ('name', models.CharField(max_length=30, primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name='Lab', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('written_datetime', models.DateTimeField(auto_now_add=True)), + ], + ), + migrations.CreateModel( + name='LabType', + fields=[ + ('name', models.CharField(max_length=30, primary_key=True, serialize=False)), + ], + ), + migrations.CreateModel( + name='Measurement', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ], + ), + migrations.CreateModel( + name='MeasurementType', + fields=[ + ('long_name', models.CharField(max_length=30, primary_key=True, serialize=False)), + ('short_name', models.CharField(max_length=15)), + ('unit', models.CharField(blank=True, max_length=15)), + ('lab_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='labs.LabType')), + ], + ), + migrations.CreateModel( + name='ContinuousMeasurement', + fields=[ + ('measurement_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='labs.Measurement')), + ('value', models.DecimalField(blank=True, decimal_places=1, max_digits=5, null=True)), + ], + bases=('labs.measurement',), + ), + migrations.CreateModel( + name='DiscreteMeasurement', + fields=[ + ('measurement_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='labs.Measurement')), + ], + bases=('labs.measurement',), + ), + migrations.AddField( + model_name='measurement', + name='lab', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='labs.Lab'), + ), + migrations.AddField( + model_name='measurement', + name='measurement_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='labs.MeasurementType'), + ), + migrations.AddField( + model_name='lab', + name='lab_type', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='labs.LabType'), + ), + migrations.AddField( + model_name='lab', + name='patient', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='pttrack.Patient'), + ), + migrations.AddField( + model_name='discreteresulttype', + name='measurement_type', + field=models.ManyToManyField(to='labs.MeasurementType'), + ), + migrations.AddField( + model_name='discretemeasurement', + name='value', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='labs.DiscreteResultType'), + ), + ] diff --git a/labs/migrations/__init__.py b/labs/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/labs/models.py b/labs/models.py new file mode 100644 index 0000000..6457db6 --- /dev/null +++ b/labs/models.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import datetime + +from django.db import models +from django.utils import timezone +from pttrack.models import Patient + +# type of lab panels +# e.g. BMP, A1c, CBC, etc. +class LabType(models.Model): + name = models.CharField(max_length=30, primary_key=True) + + def __unicode__(self): + return self.name + + +# object of a lab panel +class Lab(models.Model): + patient = models.ForeignKey(Patient) + + written_datetime = models.DateTimeField(auto_now_add=True) + + lab_type = models.ForeignKey(LabType) + + def __unicode__(self): + to_tz = timezone.get_default_timezone() + str_time = self.written_datetime.astimezone(to_tz).strftime("%B-%d-%Y, %H:%M") + return '%s | %s | %s ' %(str(self.patient),str(self.lab_type),str_time) + + +# type of measurements in a lab panel +# e.g. Na+, K+ in BMP, A1c in A1c, WBC in CBC, etc. +class MeasurementType(models.Model): + long_name = models.CharField(max_length=30, primary_key=True) + short_name = models.CharField(max_length=15) + unit = models.CharField(max_length=15, blank=True) + + lab_type = models.ForeignKey(LabType) + + def __unicode__(self): + return self.long_name + + +# parent class of measurements +class Measurement(models.Model): + measurement_type = models.ForeignKey(MeasurementType) + lab = models.ForeignKey(Lab) + + +# object of a continuous measurement +class ContinuousMeasurement(Measurement): + value = models.DecimalField(max_digits=5, decimal_places=1,blank=True, null=True) + + def __unicode__(self): + return '%s: %2g' %(self.measurement_type, self.value) + + +# type of discrete results +# e.g. Positive, Negative, Trace, etc. +class DiscreteResultType(models.Model): + name = models.CharField(max_length=30, primary_key=True) + measurement_type = models.ManyToManyField(MeasurementType) + + def __unicode__(self): + return self.name + + +# object of a continuous measurement +class DiscreteMeasurement(Measurement): + value = models.ForeignKey(DiscreteResultType) + + def __unicode__(self): + value_name = DiscreteResultType.objects.get(pk=self.value) + return '%s: %s' %(self.measurement_type, value_name.name) diff --git a/labs/templates/labs/lab_all.html b/labs/templates/labs/lab_all.html new file mode 100644 index 0000000..f039e5a --- /dev/null +++ b/labs/templates/labs/lab_all.html @@ -0,0 +1,21 @@ +{% extends "pttrack/base.html" %} + +{% block title %} +All Labs +{% endblock %} + +{% block header%} +

All Labs

+{% endblock %} + +{% block content %} + +{% if error_message %}

{{ error_message }}

{% endif %} + +
+ {% for lab in labs %} +

{{ lab }}

+ {% endfor %} +
+ +{% endblock %} \ No newline at end of file diff --git a/labs/templates/labs/lab_create.html b/labs/templates/labs/lab_create.html new file mode 100644 index 0000000..13f68c4 --- /dev/null +++ b/labs/templates/labs/lab_create.html @@ -0,0 +1,23 @@ +{% extends "pttrack/base.html" %} + +{% block title %} +Create New Lab +{% endblock %} + +{% block header %} +

Create New Lab

+{% endblock %} + +{% block content %} + +{% if error_message %}

{{ error_message }}

{% endif %} + +
+
+ {% load crispy_forms_tags %} + {% crispy form %} +
+ +
+ +{% endblock %} \ No newline at end of file diff --git a/labs/templates/labs/lab_detail.html b/labs/templates/labs/lab_detail.html new file mode 100644 index 0000000..9772fb2 --- /dev/null +++ b/labs/templates/labs/lab_detail.html @@ -0,0 +1,28 @@ +{% extends "pttrack/base.html" %} + +{% block title %} +Lab detail +{% endblock %} + +{% block header %} +

Lab detail

+

+{% endblock %} + +{% block content %} + +{% if error_message %}

{{ error_message }}

{% endif %} + +
+ {{ lab }} + +
+ +{% endblock %} \ No newline at end of file diff --git a/labs/tests.py b/labs/tests.py new file mode 100644 index 0000000..5982e6b --- /dev/null +++ b/labs/tests.py @@ -0,0 +1,6 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.test import TestCase + +# Create your tests here. diff --git a/labs/urls.py b/labs/urls.py new file mode 100644 index 0000000..97ae38c --- /dev/null +++ b/labs/urls.py @@ -0,0 +1,24 @@ +from django.conf.urls import url +from pttrack.urls import wrap_url +from django.views.generic import DetailView + +from . import models +from . import views + +unwrapped_urlconf = [ + url(r'^all/(?P[0-9]+)/$', + views.LabListView.as_view(), + name="all-labs"), + url(r'^(?P[0-9]+)/$', + views.LabDetailView.as_view(), + name='lab-detail'), + url(r'^newm/(?P[0-9]+)/$', + views.full_lab_create, + name='new-full-lab'), #create all measurements assoc w/ the lab obj + url(r'^newl/(?P[0-9]+)/$', + views.lab_create, + name='new-lab'), #create "parent" lab object +] + +wrap_config = {} +urlpatterns = [wrap_url(url, **wrap_config) for url in unwrapped_urlconf] \ No newline at end of file diff --git a/labs/views.py b/labs/views.py new file mode 100644 index 0000000..4560522 --- /dev/null +++ b/labs/views.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from pttrack.models import Patient + +from django.shortcuts import render, redirect, get_object_or_404 +from django.core.urlresolvers import reverse +from django.views.generic import ListView, DetailView, CreateView +from .models import * +from .forms import LabCreationForm, MeasurementsCreationForm + +from crispy_forms.helper import FormHelper +from crispy_forms.layout import Submit, Layout, Div, Row, HTML, Field +from crispy_forms.bootstrap import ( + InlineCheckboxes, AppendedText, PrependedText) + + +class LabListView(ListView): + model = Lab + template_name = 'labs/lab_all.html' + context_object_name = 'labs' + ordering = ['-written_datetime'] + + def get_queryset(self): + self.pt = get_object_or_404(Patient, pk=self.kwargs['pt_id']) + return Lab.objects.filter(patient=self.kwargs['pt_id']) + + +class LabDetailView(DetailView): + model = Lab + context_object_name = 'lab' + + def get_context_data(self, **kwargs): + context = super(LabDetailView,self).get_context_data(**kwargs) + self.lab = get_object_or_404(Lab, pk=self.kwargs['pk']) + context['cont_list'] = ContinuousMeasurement.objects.filter(lab=self.lab) + context['disc_list'] = DiscreteMeasurement.objects.filter(lab=self.lab) + return context + + +def lab_create(request, pt_id): + pt = get_object_or_404(Patient, pk=pt_id) + + if request.method == 'POST': + form = LabCreationForm(request.POST,pt=pt) + if form.is_valid(): + new_lab = form.save(commit=False) + new_lab.patient = pt + new_lab.save() + return redirect(reverse("new-full-lab", args=(new_lab.pk,))) + else: + form = LabCreationForm(pt=pt) + + return render(request, 'labs/lab_create.html', {'form':form}) + + +def full_lab_create(request, lab_id): + lab = get_object_or_404(Lab, pk=lab_id) + + lab_type = lab.lab_type + qs_mt = MeasurementType.objects.filter(lab_type=lab_type) + + if request.method == 'POST': + form = MeasurementsCreationForm(request.POST,qs_mt=qs_mt, new_lab=lab) + + if form.is_valid(): + form.save() + return redirect(reverse("lab-detail", args=(lab.pk,))) + + else: + form = MeasurementsCreationForm(qs_mt=qs_mt, new_lab=lab) + + return render(request, 'labs/lab_create.html', {'form':form}) diff --git a/osler.sublime-project b/osler.sublime-project index 968046e..9c46a87 100644 --- a/osler.sublime-project +++ b/osler.sublime-project @@ -50,7 +50,8 @@ "__init__.py" ], "path": "audit" - }, { + }, + { "file_exclude_patterns": [ "__init__.py" @@ -98,6 +99,9 @@ "__init__.py" ], "path": "workup" + }, + { + "path": "labs" } ] } diff --git a/osler/base_settings.py b/osler/base_settings.py index f9815b1..2bf83fa 100644 --- a/osler/base_settings.py +++ b/osler/base_settings.py @@ -36,6 +36,7 @@ 'simple_history', 'rest_framework', 'audit', + 'labs', ) MIDDLEWARE = ( diff --git a/osler/urls.py b/osler/urls.py index f72b0a9..d68431c 100644 --- a/osler/urls.py +++ b/osler/urls.py @@ -17,6 +17,7 @@ url(r'^accounts/', include('django.contrib.auth.urls')), url(r'^api/', include('api.urls')), url(r'^referral/', include('referral.urls')), + url(r'^labs/', include('labs.urls')), url(r'^$', RedirectView.as_view(pattern_name="dashboard-dispatch", permanent=False), diff --git a/pttrack/migrations/0010_auto_20200524_1340.py b/pttrack/migrations/0010_auto_20200524_1340.py new file mode 100644 index 0000000..899d2fe --- /dev/null +++ b/pttrack/migrations/0010_auto_20200524_1340.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.28 on 2020-05-24 18:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('pttrack', '0009_set_orderings_20190902_2116'), + ] + + operations = [ + migrations.AlterField( + model_name='actionitem', + name='due_date', + field=models.DateField(help_text=b'MM/DD/YYYY'), + ), + migrations.AlterField( + model_name='historicalactionitem', + name='due_date', + field=models.DateField(help_text=b'MM/DD/YYYY'), + ), + ] diff --git a/referral/migrations/0003_auto_20200524_1340.py b/referral/migrations/0003_auto_20200524_1340.py new file mode 100644 index 0000000..17b6e9f --- /dev/null +++ b/referral/migrations/0003_auto_20200524_1340.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.11.28 on 2020-05-24 18:40 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('referral', '0002_auto_20190902_2116'), + ] + + operations = [ + migrations.AlterField( + model_name='followuprequest', + name='due_date', + field=models.DateField(help_text=b'MM/DD/YYYY'), + ), + ]