From 5b504c98d97eb9ff4371030a2014aee2dfa795d5 Mon Sep 17 00:00:00 2001 From: Mad Price Ball Date: Tue, 26 May 2020 15:13:20 -0700 Subject: [PATCH] WIP adding database storage for symptom report setup Moving hardcoded stuff to explicit models to enable later moving to editable symptom lists. This is work in progress: migrations haven't been created and the code is entirely untested. --- reports/models.py | 157 +++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 154 insertions(+), 3 deletions(-) diff --git a/reports/models.py b/reports/models.py index 1292c34..5b85dda 100644 --- a/reports/models.py +++ b/reports/models.py @@ -5,7 +5,9 @@ import secrets from django.core.exceptions import ValidationError +from django.core.validators import validate_comma_separated_integer_list from django.db import models +from django.db.models import F from django.forms.widgets import CheckboxSelectMultiple from django.utils.timezone import now @@ -100,7 +102,7 @@ def valid_member(self): class Symptom(models.Model): label = models.CharField(max_length=20, choices=SYMPTOM_CHOICES, unique=True) - available = models.BooleanField(default=False) + verbose = models.CharField(max_length=40, blank=True) def __str__(self): return self.label @@ -108,6 +110,124 @@ def __str__(self): def __unicode__(self): return self.label + def save(self, *args, **kwargs): + if not self.verbose: + self.verbose = self.label + super().save(*args, **kwargs) + + +class ReportDisplayMixin(object): + """ + Structure report symptoms and categories for display purposes. + """ + + @staticmethod + def order_by_symptoms(symptomitem_queryset): + return ( + symptomitem_queryset.annotate(verbose=F("symptom__verbose")) + .extra(select={"ci_verbose": "lower(verbose)"}) + .order_by("ci_verbose") + ) + + @staticmethod + def sort_category_items(categoryitem_queryset, category_ordering): + ordering = [int(i) for i in category_ordering.split(",") if i] + categories = OrderedDict( + [(c.id, c) for c in categoryitem_queryset.order_by("name")] + ) + sorted_categories = [] + for item in ordering: + try: + sorted_categories.append(categories.pop(item)) + except KeyError: + continue + for key in categories.keys(): + sorted_categories.append(categories[key]) + return sorted_categories + + def display_format(self): + formatted = [] + symptom_items = self.get_symptom_items() + sorted_categories = self.sort_category_items( + categoryitem_queryset=self.get_category_items(), + category_ordering=self.category_ordering, + ) + + for category in sorted_categories: + formatted.append( + { + "category": category, + "symptoms": self.order_by_symptoms( + symptom_items.filter(category=category) + ), + } + ) + + formatted.append( + { + "category": None, + "symptoms": self.order_by_symptoms(symptom_items.filter(category=None)), + } + ) + + return formatted + + +class ReportSetup(ReportDisplayMixin, models.Model): + """ + The template set of symptoms and categories used for symptom reporting. + + These are arranged as: + 1. a set of categories (ReportSetupCategoryItems) + 2. a set of symptoms (ReportSetupSymptomItems) + + Symptoms may be re-used in other ReportSetups (Symptom objects). + + Categories are specific to the report setup (there's no generic "Category" + object.) Categories exist for display purposes: they can be ordered, and + symptoms within a category are typically ordered alphabetically according + to their verbose label, e.g. as done in display_format(). + + A category (ReportSetupCategoryItem) can have symptoms (ReportSetupSymptomItems) + corresponding to it. Or it could have none (an "empty" category). + + A symptom in the setup (ReportSetupSymptomItem) may have a category, or may be + unassigned (e.g. displayed later as "Uncategorized symptoms"). + """ + + title = models.CharField(max_length=30) + category_ordering = models.TextField( + validators=[validate_comma_separated_integer_list], blank=True + ) + + def __str__(self): + return "{} ({})".format(self.title, self.id) + + def get_category_items(self): + return self.reportsetupcategoryitem_set + + def get_symptom_items(self): + return self.reportsetupsymptomitem_set + + +class ReportSetupCategoryItem(models.Model): + report_setup = models.ForeignKey(ReportSetup, on_delete=models.CASCADE) + name = models.CharField(max_length=20) + + def __str__(self): + return "{} ({})".format(self.name, self.id) + + +class ReportSetupSymptomItem(models.Model): + report_setup = models.ForeignKey(ReportSetup, on_delete=models.CASCADE) + category = models.ForeignKey( + ReportSetupCategoryItem, on_delete=models.SET_NULL, null=True + ) + symptom = models.ForeignKey(Symptom, on_delete=models.PROTECT) + + def __str__(self): + return "{} ({})".format(self.symptom.label, self.id) + """ # TODO: Implement reporting of diagnostic testing. @@ -125,9 +245,12 @@ def __unicode__(self): """ -class SymptomReport(models.Model): +class SymptomReport(ReportDisplayMixin, models.Model): member = models.ForeignKey(OpenHumansMember, on_delete=models.CASCADE) created = models.DateTimeField(auto_now_add=True) + category_ordering = models.TextField( + validators=[validate_comma_separated_integer_list], blank=True + ) fever_guess = models.CharField( max_length=20, choices=FEVER_CHOICES, null=True, blank=True ) @@ -160,6 +283,12 @@ def get_symptom_values(self): for s in self.symptomreportsymptomitem_set.all() } + def get_category_items(self): + return self.symptomreportcategoryitem_set + + def get_symptom_items(self): + return self.symptomreportsymptomitem_set + @property def severity(self): """Rough attempt to assess "severity" for a report, for display purposes""" @@ -195,10 +324,32 @@ def as_json(self): return json.dumps(data) +class SymptomReportCategoryItem(models.Model): + """ + The ReportSetupCategoryItem information at the time of reporting. + """ + + name = models.CharField(max_length=20) + report = models.ForeignKey(SymptomReport, on_delete=models.CASCADE) + + def __str__(self): + return "{} ({})".format(self.name, self.id) + + class SymptomReportSymptomItem(models.Model): - symptom = models.ForeignKey(Symptom, on_delete=models.CASCADE) + """ + A specific Symptom recorded in a given report. + """ + + symptom = models.ForeignKey(Symptom, on_delete=models.PROTECT) report = models.ForeignKey(SymptomReport, on_delete=models.CASCADE) intensity = models.IntegerField(choices=SYMPTOM_INTENSITY_CHOICES, default=0) + category = models.ForeignKey( + SymptomReportCategoryItem, on_delete=models.SET_NULL, null=True + ) + + def __str__(self): + return "{} ({})".format(self.symptom.label, self.id) class SymptomReportPhysiology(models.Model):