class Event(BaseModel): ident = models.CharField(max_length=30, default="", unique=True) type = models.ForeignKey(EventType, on_delete=models.PROTECT) person = models.ForeignKey(Person) date = models.DateTimeField(null=True, blank=True) samples = models.ManyToManyField(Sample, blank=True, help_text="Samples resulting from event") # fixme: remove study = models.ForeignKey(Study, help_text="Designates which study \"owns\" the samples") data = JSONField(null=True, blank=True, help_text="Fields as defined by the event type") @staticmethod def make_id(id): return "E-%06d" % id @init_record_id("ident") def save(self, *args, **kwargs): super().save(*args, **kwargs) def get_merged_schema(self): schemas = [] et = self.type while et: schemas.append(et.fields) # fixme: use a single query et = et.super_type merged = {} for schema in reversed(schemas): merged.update(schema.get("properties") or {}) return {"properties": merged}
class MigrationRun(models.Model): """ This model stores the logs from the kintrak migration. """ start_time = models.DateTimeField(auto_now_add=True) finish_time = models.DateTimeField(blank=True, null=True) report = JSONField(default={})
class Study(AbstractNameDescList): """ Study is a functional area of turtleweb. The study area corresponds to a category of ailment -- e.g. colorectal cancer, gynaecology, breast cancer. It's not a very good name, but that's what they were calling it in turtle. We should figure out a better name. Examples of studies: - CRC - Breast Cancer - WAGO (Gynae) """ slug = models.SlugField(unique=True) archived = models.DateTimeField( blank=True, null=True, help_text= "When non-None, the study is archived. Archiving hides the study away so it can be forgotten about" ) data = JSONField(null=True, blank=True, default={}) class Meta(AbstractNameDescList.Meta): verbose_name_plural = "studies" @classmethod def get_for(cls, person): qs = Study.objects.filter(members__patient=person) return qs.order_by("members__consent_request_date", "slug").first() def num_consent(self): return self.members.filter( consents__status__name__iexact=ConsentStatus.CONSENTED).count()
class UserPrefs(models.Model): """ Simple profile which frontend can store settings in. """ user = models.OneToOneField(User, related_name="prefs") prefs = JSONField(default={}) class Meta: verbose_name_plural = "user prefs"
class EventType(AbstractNameList): super_type = models.ForeignKey("self", null=True, blank=True) fields = JSONField(null=True, blank=True) studies = models.ManyToManyField(Study, related_name="event_types", blank=True, help_text="Studies which this event type applies to." + " If blank, event type applies to all studies.") class Meta(AbstractNameList.Meta): unique_together = [("super_type", "name")]
class Collaborator(BaseModel): first_name = models.CharField(max_length=200) last_name = models.CharField(max_length=200) title = models.ForeignKey("people.Title") contact = models.ForeignKey("contacts.ContactDetails") data = JSONField(null=True, blank=True) @property def full_name(self): return "%s, %s %s" % (self.last_name, self.title.name, self.first_name) def __str__(self): return self.full_name
class StudyConsent(models.Model): """ Represents patient consent for their data to be kept in the system and used by the hospital. """ study_member = models.ForeignKey(StudyMember, related_name="consents") status = models.ForeignKey(ConsentStatus) date = models.DateTimeField(null=True, blank=True, help_text="Date " + "consent was given") consented_by = models.ForeignKey(ConsentObtainedBy, related_name="consents", null=True, blank=True) data = JSONField(null=True, blank=True)
class StudyGroup(BaseModel): """ A study group is a collection of patients used for organizing research, etc. It is like a patient list, cohort, etc """ study = models.ForeignKey(Study) name = models.CharField(max_length=200) desc = models.TextField(blank=True) owner = models.ForeignKey(User, related_name="+") members = models.ManyToManyField(Person, related_name="study_groups", blank=True) collaborators = models.ManyToManyField(Collaborator, related_name="study_groups", blank=True) data = JSONField(null=True, blank=True) def __str__(self): return self.name
class Intervention(AbstractNameDescList): """ An intervention the description of a drug, drug combination, treatment, etc, that can be given to patients. The categorization a treatments is fairly broad which is good enough for research purposes, but not detailed and exact enough for e.g. clinical trials. """ super_type = models.ForeignKey("self", null=True, blank=True) group = models.CharField(max_length=250, help_text="Intervention category") abbr_name = models.CharField(max_length=100) alternative_name = models.CharField( max_length=250, help_text="Drugs/treatments can go under multiple names") comments = models.TextField(max_length=1000, blank=True) units = models.CharField( max_length=100, blank=True, help_text="Units of treatment grey/mg/mL/litres/pints/etc") route = models.CharField( max_length=250, blank=True, verbose_name="Route of administration", help_text= "How the intervention is delivered to the patient (injection/radiation beam/oral/etc)" ) fields = JSONField(null=True, blank=True) studies = models.ManyToManyField( Study, related_name="interventions", blank=True, help_text="Studies which this intervention applies to." + " If blank, intervention applies to all studies.") class Meta(AbstractNameDescList.Meta): unique_together = [("super_type", "name")]
class CustomDataSchema(models.Model): """ Allows definition of per-study JSON schema for the "data" field of various models. If study is null, then the schema applies to all studies. """ content_type = models.ForeignKey(ContentType) study = models.ForeignKey(Study, null=True, blank=True) schema = JSONField(default={}) class Meta(AbstractNameDescList.Meta): unique_together = [("content_type", "study")] ordering = ["content_type", "study"] def __str__(self): return "%s study=%s" % (self.content_type, self.study) @classmethod def get_field(cls, field_name, model_name, study_id=None): """ Finds a schema property definition for a field of a model. The field definition specific to a study has precedence over the field definition for all studies. """ def study_match(c): return c.study_id is None or c.study_id == study_id path = ["properties", field_name] cds = cls.objects.raw( """ SELECT project_customdataschema.id, study_id, schema#>%s AS field FROM project_customdataschema INNER JOIN django_content_type ON project_customdataschema.content_type_id = django_content_type.id WHERE django_content_type.model = %s ORDER BY study_id DESC; """, [path, model_name]) fields = [c.field for c in cds if c.field and study_match(c)] return fields[0] if len(fields) else None
class Treatment(AuditAndPrivacy, BaseModel): """ Treatment is a simple list of interventions which have been or are being given to the patient. """ person = models.ForeignKey(Person, related_name="treatments") intervention = models.ForeignKey(Intervention, on_delete=models.PROTECT) # Not sure what event is for event = models.ForeignKey(Event, null=True, blank=True) # diagnosis = models.ForeignKey(Diagnosis) comments = models.TextField(max_length=2000, blank=True) cycles = models.IntegerField(null=True) doctor = models.ForeignKey(Doctor, null=True, blank=True, help_text="Doctor for tx") dose = models.FloatField(null=True, help_text="Dose of treatment") start_date = models.DateField(null=True, blank=True) stop_date = models.DateField(null=True, blank=True) stop_reason = models.CharField(max_length=500) data = JSONField(null=True, blank=True)
class Person(models.Model): """ Model which represents patients, their families and unborn babies. Users, staff members, doctors, and other "people" entities also inherit from this model. """ SEX_CHOICES = (("", "Unknown"), ("M", "Male"), ("F", "Female")) first_name = models.CharField(max_length=256) second_name = models.CharField(max_length=256, blank=True) last_name = models.CharField(max_length=256) maiden_name = models.CharField(max_length=256, blank=True, help_text="Surname prior to marital name change") other_name = models.CharField(max_length=256, blank=True, help_text="Other previous surnames") title = models.ForeignKey("Title", default=default_title, on_delete=models.PROTECT) initials = models.CharField(max_length=64, blank=True, help_text="Initials of given names. If blank, value will be derived from names") sex = models.CharField(max_length=1, choices=SEX_CHOICES) born = models.BooleanField(default=True, help_text="Is this person born yet?") deceased = models.BooleanField(default=False, help_text="Has this person died?") dob = models.DateField(null=True, blank=True, verbose_name="Date of Birth", db_index=True, help_text="Date of birth, if known") dod = models.DateField(null=True, blank=True, verbose_name="Date of Death", db_index=True, help_text="Date of death, if known") dob_checked = models.BooleanField(default=True, verbose_name="DOB Checked", help_text="It is verified that the birth date is correct") dod_checked = models.BooleanField(default=True, verbose_name="DOD Checked", help_text="It is verified that the death date is correct") place_of_birth = models.CharField(max_length=256, blank=True) cause_of_death = models.CharField(max_length=256, blank=True) mother = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL, related_name="maternal_children") father = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL, related_name="paternal_children") twins = models.ManyToManyField("self", blank=True) twins_identical = models.BooleanField(default=False, help_text="Monozygotic/Dizygotic - Identical/Fraternal") comment = models.TextField(blank=True) data = JSONField(null=True, blank=True, help_text="Fields as defined by the user") patient_id = models.CharField(max_length=30, unique=True, default="") class Meta: ordering = ["id"] @staticmethod def make_id(id): return "P-%06d" % id @init_record_id("patient_id") def save(self, *args, **kwargs): super().save(*args, **kwargs) def get_short_name(self): return self.first_name def get_full_name(self): "Returns all names from first to last" return " ".join(v for v in [self.first_name, self.second_name, self.other_name, self.last_name] if v) def get_formal_name(self): "Returns title and surname" return "%s %s" % (self.title, self.last_name) def get_pid_full_name(self): return " ".join(v for v in [self.patient_id, self.get_full_name()] if v) def get_reverse_name(self): given_names = " ".join([self.first_name, self.second_name, self.other_name]) return ", ".join([self.last_name, given_names]) def __str__(self): name = self.get_full_name() return " ".join([self.patient_id] + ([name] if name else [])) def __repr__(self): return "<%s: [id=%s] %s>" % (self.__class__.__name__, self.id, self.get_full_name()) @property def paternal_siblings(self): return Person.objects.filter(father=self.father) return self.father.paternal_children @property def maternal_siblings(self): # return Person.objects.filter(mother=self.mother) return self.mother.maternal_children @property def siblings(self): return Person.objects.filter(Q(mother=self.mother) | Q(father=self.father)) def consent_status_dict(self, study_id): from ..project.models import StudyConsent consent = StudyConsent.objects.filter(study_member__study_id=study_id, study_member__patient=self) vals = consent.values("status__name", "date").first() if vals: return { "consent": vals["status__name"], "consent_date": vals["date"], } else: return None def consent_status(self, study_id): d = self.consent_status_dict(study_id) return d["consent"] if d else None def consent_date(self, study_id): d = self.consent_status_dict(study_id) return d["consent_date"] if d else None
class Container(models.Model): """ A container is something which goes inside other containers and/or can contain samples. Depending upon the dimension of its class, the product of width, height, and depth determines how many samples can be held within the container. """ cls = models.ForeignKey(ContainerClass, related_name="containers") name = models.CharField(max_length=250) order = models.PositiveIntegerField(default=0) default = models.BooleanField(default=False) container = models.ForeignKey("Container", null=True, blank=True, related_name="containers") width = models.IntegerField(default=0) height = models.IntegerField(default=0) depth = models.IntegerField(default=0) data = JSONField(null=True, blank=True) def save(self, *args, **kwargs): if self.cls: self.width = self.width if self.width else self.cls.def_width self.height = self.height if self.height else self.cls.def_height self.depth = self.depth if self.depth else self.cls.def_depth super(Container, self).save(*args, **kwargs) class Meta: ordering = ["container_id", "order"] def __str__(self): return self.name def get_subcontainer_ids(self): """ Return list of ids of all subcontainers. """ return type(self).sub_container_ids_for(self.id) @classmethod def sub_container_ids_for(cls, container_id): qs = cls.objects.raw( """ WITH RECURSIVE q AS ( SELECT p.*, 0 AS level FROM biobank_container p WHERE id = %s UNION ALL SELECT pc.*, level + 1 FROM q JOIN biobank_container pc ON (pc.container_id = q.id) ) SELECT id FROM q ORDER BY level DESC """, [container_id]) return [c.id for c in qs] @classmethod def super_container_ids_for(cls, container_id): qs = cls.objects.raw( """ WITH RECURSIVE q AS ( SELECT p.*, 0 AS level FROM biobank_container p WHERE id = %s UNION ALL SELECT pc.*, level + 1 FROM q JOIN biobank_container pc ON (pc.id = q.container_id) ) SELECT id FROM q ORDER BY level DESC """, [container_id]) return [c.id for c in qs] @classmethod def visible_container_ids(cls, container_id): """ Gets all the containers which would be visible if a container were selected in the biobank hierarchy. """ # fixme: query isn't right ... need to select "aunty and # uncle" containers as well qs = cls.objects.raw( """ (WITH RECURSIVE q AS ( SELECT p.*, 0 AS level FROM biobank_container p WHERE id = %s UNION ALL SELECT pc.*, level + 1 FROM q JOIN biobank_container pc ON (pc.id = q.container_id) ) SELECT id FROM q ORDER BY level DESC) UNION SELECT id FROM biobank_container WHERE container_id = %s UNION SELECT id FROM biobank_container WHERE container_id IS NULL """, [container_id, container_id]) return [c.id for c in qs] def num_samples(self): cs = self.get_subcontainer_ids() return Sample.objects.filter(location__container__in=cs).count() def get_path(self): ids = self.super_container_ids_for(self.id) names = dict( type(self).objects.filter(id__in=ids).values_list("id", "name")) return [names[id] for id in ids]
class Transaction(models.Model): sample = models.ForeignKey(Sample, related_name="transactions") date = models.DateTimeField(default=timezone.now, null=True) comment = models.TextField() data = JSONField(null=True, blank=True) class Meta: ordering = ["sample_id", "date", "id"] get_latest_by = "date" # Unfortunately Django requires a lot of futzing around to get # nice non-abstract model inheritance. TYPE_CHOICES = (("C", "Collection"), ("U", "Use"), ("D", "Destruction"), ("X", "Sending"), ("A", "Split"), ("S", "Subdivision"), ("P", "Processed"), ("F", "Frozen/Fixed"), ("J", "Subcultured"), ("K", "Subcultured from"), ("L", "Adjustment"), ("", "Note")) type = models.CharField(max_length=1, choices=TYPE_CHOICES, default="") SUBCLS_MAP = { "C": "SampleCollection", "U": "SampleUse", "D": "SampleDestroy", "X": "SampleSending", "M": "SampleMove", "A": "SampleSplit", "S": "SampleSubdivision", "P": "SampleProcessed", "F": "SampleFrozenFixed", "J": "SampleSubcultured", "K": "SampleSubculturedFrom", "L": "SampleAdjustment", "": "Transaction" } @property def ext(self): if self.type: return getattr(self, self.SUBCLS_MAP[self.type].lower()) else: return self def save(self, *args, **kwargs): if not self.type: inv_map = dict((v, k) for k, v in self.SUBCLS_MAP.items()) self.type = inv_map.get(type(self).__name__, "") # apply_transaction implementations may need to create foreign-key # relationships pointing at this transaction, so save() before calling will_apply = self.pk is None result = super().save(*args, **kwargs) if will_apply: self.apply_transaction() return result def apply_transaction(self): pass def destroy_sample_if_consumed(self): self.sample.destroy_if_consumed(date=self.date, comment=self.comment, data=self.data) def __str__(self): return "%s %s %s" % (self.SUBCLS_MAP[self.type], self.date, self.comment)
class Sample(models.Model): cls = models.ForeignKey(SampleClass, related_name="+", on_delete=models.PROTECT) subtype = models.ForeignKey(SampleSubtype, on_delete=models.PROTECT) specimen_id = models.CharField(max_length=30, unique=True, db_index=True) location = models.ForeignKey(SampleLocation, null=True, blank=True, on_delete=models.SET_NULL, related_name="sample") stored_in = models.ForeignKey(SampleStoredIn, on_delete=models.PROTECT) treatment = models.ForeignKey(SampleTreatment, null=True, blank=True, on_delete=models.SET_NULL) # Behaviour applies to tissue only behaviour = models.ForeignKey(SampleBehaviour, null=True, blank=True, on_delete=models.SET_NULL) # extraction protocol applies to dna only dna_extraction_protocol = models.ForeignKey(DnaExtractionProtocol, null=True, blank=True, on_delete=models.SET_NULL) amount = models.FloatField(help_text="Quantity of sample in units", default=0.0) display_unit = models.CharField(max_length=10, choices=DISPLAY_UNIT_CHOICES, blank=True) concentration = models.FloatField( default=1.0, help_text="Concentration of DNA, in ng/µL") comments = models.TextField(max_length=1000) data = JSONField(null=True, blank=True) def format_id(self): return self.specimen_id or self.make_id(self.id) @staticmethod def make_id(id): return "B-%07d" % id def make_derived_sample(self, sample_attrs, xact_attrs): "make a derive sample, and note this in the transaction log" s = Sample(cls=self.cls, subtype=self.subtype, display_unit=self.display_unit, location=None, stored_in=self.stored_in, treatment=self.treatment, behaviour=self.behaviour, concentration=self.concentration, comments=self.comments, data=self.data, **sample_attrs) s.save() xact = self.transactions.order_by("pk") event_id = xact.values_list("samplecollection__event", flat=True).first() if event_id: t = SampleCollection(sample=s, event_id=event_id, **xact_attrs) t.save() return s def destroy_if_consumed(self, **attrs): if self.amount <= 0.0: d = SampleDestroy(sample=self, **attrs) d.save() @init_record_id("specimen_id") def save(self, *args, **kwargs): if not self.display_unit and self.cls: self.display_unit = self.cls.display_unit super().save(*args, **kwargs) def get_owner(self): ids = self.transactions.values_list("samplecollection__event__person") return Person.objects.filter(id__in=ids).first() def get_event_type_id(self): lookup = "samplecollection__event__type_id" return self.transactions.values_list(lookup, flat=True).first() def __str__(self): return self.specimen_id
class ReportSearch(BaseModel): """ A search is a saved query expression. """ RESOURCE_CHOICES = ( ("person", "Person"), ("event", "Event"), ("sample", "Sample"), ("user", "User"), ) study = models.ForeignKey(Study, null=True, blank=True) resource = models.CharField(max_length=30, choices=RESOURCE_CHOICES) name = models.CharField(max_length=200) desc = models.TextField(blank=True) owner = models.ForeignKey(User, related_name="+") query = JSONField( help_text="Query expression parsed from the Turtleweb query syntax", null=True, blank=True) order_by = models.CharField( max_length=200, blank=True, help_text="Whitespace separated list of fields") list_columns = models.CharField( max_length=200, blank=True, help_text="Whitespace separated list of fields") class Meta(BaseModel.Meta): abstract = True def __str__(self): return self.name def get_q(self): study_id = None if self.study is None else self.study.id if self.resource == "person": return person_query_expr(self.query, study_id) elif self.resource == "event": return event_query_expr(self.query, study_id) elif self.resource == "sample": return sample_query_expr(self.query, study_id) elif self.resource == "user": return user_query_expr(self.query) else: return models.Q(id__in=[]) @property def model_cls(self): mapping = { "person": people.Person, "event": events.Event, "sample": biobank.Sample, "user": User } return mapping.get(self.resource, people.Person) def get_qs(self): q = self.get_q() qs = self._order_qs(self.model_cls.objects.filter(q)) return qs def _order_qs(self, qs): return qs.order_by(*self.get_order_by()) def get_order_by(self): return self._order_by_fields(self.order_by) def _order_by_fields(self, order_by): m = { "id": ["id"], "surname": ["last_name"], "given": ["first_name", "second_name"], "sex": ["sex"], "dob": ["dob"], "dob.year": ["dob"], "dob.month": ["dob"], "address.suburb": ["addresses__contact__suburb"], "address.state": ["addresses__contact__suburb__state"], # fixme: study and project lookup is pretty naff "study": ["studies"], "project": ["studies__project"], } def conv(s): return m[s] return list(chain(*map(conv, order_by.split())))