Exemple #1
0
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}
Exemple #2
0
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={})
Exemple #3
0
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()
Exemple #4
0
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"
Exemple #5
0
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")]
Exemple #6
0
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
Exemple #7
0
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)
Exemple #8
0
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
Exemple #9
0
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")]
Exemple #10
0
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
Exemple #11
0
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)
Exemple #12
0
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
Exemple #13
0
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]
Exemple #14
0
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)
Exemple #15
0
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
Exemple #16
0
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())))