Completed
Push — master ( 4f7ee6...646424 )
by Paolo
08:30 queued 06:53
created

uid.models.Animal.get_attributes()   A

Complexity

Conditions 1

Size

Total Lines 44
Code Lines 24

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 24
dl 0
loc 44
rs 9.304
c 0
b 0
f 0
cc 1
nop 1
1
2
import logging
3
import os
4
import shlex
5
6
from django.contrib.auth.models import User
7
from django.contrib.contenttypes.fields import GenericRelation
8
from django.db import models
9
from django.db.models import Func, Value, F
10
from django.db.models.signals import post_save
11
from django.dispatch import receiver
12
from django.urls import reverse
13
14
from common.fields import ProtectedFileField
15
from common.constants import (
16
    OBO_URL, STATUSES, CONFIDENCES, NAME_STATUSES, ACCURACIES, WAITING, LOADED,
17
    MISSING, DATA_TYPES, TIME_UNITS, SAMPLE_STORAGE, SAMPLE_STORAGE_PROCESSING)
18
from common.helpers import format_attribute
19
20
from .mixins import BaseMixin, BioSampleMixin
21
22
# Get an instance of a logger
23
logger = logging.getLogger(__name__)
24
25
26
class Replace(Func):
27
    function = 'REPLACE'
28
29
30
# --- Abstract classes
31
32
33
# helper classes
34
class DictBase(BaseMixin, models.Model):
35
    """
36
    Abstract class to be inherited to all dictionary tables. It models fields
37
    like ``label`` (the revised term like submitter or blood) and
38
    ``term`` (the ontology id as the final part of the URI link)
39
40
    The fixed part of the URI could be customized from :py:class:`Ontology`
41
    by setting ``library_name`` class attribute accordingly::
42
43
        class DictRole(DictBase):
44
            library_name = 'EFO'
45
    """
46
47
    library_name = None
48
49
    # if not defined, this table will have an own primary key
50
    label = models.CharField(
51
            max_length=255,
52
            blank=False,
53
            help_text="Example: submitter")
54
55
    term = models.CharField(
56
            max_length=255,
57
            blank=False,
58
            null=True,
59
            help_text="Example: EFO_0001741")
60
61
    class Meta:
62
        # Abstract base classes are useful when you want to put some common
63
        # information into a number of other models
64
        abstract = True
65
66
    def __str__(self):
67
        return "{label} ({term})".format(
68
                label=self.label,
69
                term=self.term)
70
71
    def format_attribute(self):
72
        """
73
        Format an object instance as a dictionary used by biosample, for
74
        example::
75
76
            [{
77
                'value': 'submitter',
78
                'terms': [{'url': 'http://www.ebi.ac.uk/efo/EFO_0001741'}]
79
            }]
80
81
        the fixed part of URI link is defined by ``library_name`` class
82
        attribute
83
        """
84
85
        if self.library_name is None:
86
            logger.warning("library_name not defined")
87
            library_uri = OBO_URL
88
89
        else:
90
            library = Ontology.objects.get(library_name=self.library_name)
91
            library_uri = library.library_uri
92
93
        return format_attribute(
94
            value=self.label,
95
            library_uri=library_uri,
96
            terms=self.term)
97
98
99
class Confidence(BaseMixin, models.Model):
100
    """
101
    Abstract class which add :ref:`confidence <Common confidences>`
102
    to models
103
    """
104
105
    # confidence field (enum)
106
    confidence = models.SmallIntegerField(
107
        choices=[x.value for x in CONFIDENCES],
108
        help_text='example: Manually Curated',
109
        null=True)
110
111
    class Meta:
112
        # Abstract base classes are useful when you want to put some common
113
        # information into a number of other models
114
        abstract = True
115
116
117
class Name(BaseMixin, models.Model):
118
    """Model UID names: define a name (sample or animal) unique for each
119
    data submission"""
120
121
    __validationresult = None
122
123
    # two different animal may have the same name. Its unicity depens on
124
    # data source name and version
125
    name = models.CharField(
126
            max_length=255,
127
            blank=False,
128
            null=False)
129
130
    submission = models.ForeignKey(
131
        'Submission',
132
        related_name='%(class)s_set',
133
        on_delete=models.CASCADE)
134
135
    # This will be assigned after submission
136
    biosample_id = models.CharField(
137
        max_length=255,
138
        blank=True,
139
        null=True,
140
        unique=True)
141
142
    # '+' instructs Django that we don’t need this reverse relationship
143
    owner = models.ForeignKey(
144
        User,
145
        related_name='+',
146
        on_delete=models.CASCADE)
147
148
    # a column to track submission status
149
    status = models.SmallIntegerField(
150
            choices=[x.value for x in STATUSES if x.name in NAME_STATUSES],
151
            help_text='example: Submitted',
152
            default=LOADED)
153
154
    last_changed = models.DateTimeField(
155
        auto_now_add=True,
156
        blank=True,
157
        null=True)
158
159
    last_submitted = models.DateTimeField(
160
        blank=True,
161
        null=True)
162
163
    publication = models.ForeignKey(
164
        'Publication',
165
        null=True,
166
        blank=True,
167
        on_delete=models.SET_NULL)
168
169
    # alternative id will store the internal id in data source
170
    alternative_id = models.CharField(
171
        max_length=255,
172
        blank=True,
173
        null=True)
174
175
    description = models.CharField(
176
        max_length=255,
177
        blank=True,
178
        null=True)
179
180
    validationresults = GenericRelation(
181
        'validation.ValidationResult',
182
        related_query_name='%(class)ss')
183
184
    # https://www.machinelearningplus.com/python/python-property/
185
    # https://stackoverflow.com/questions/7837330/generic-one-to-one-relation-in-django
186
    @property
187
    def validationresult(self):
188
        """return the first validationresult object (should be uinique)"""
189
190
        if not self.__validationresult:
191
            self.__validationresult = self.validationresults.first()
192
193
        return self.__validationresult
194
195
    @validationresult.setter
196
    def validationresult(self, validationresult):
197
        """return the first validationresult object (should be uinique)"""
198
199
        # destroying relathionship when passin a None object
200
        if not validationresult:
201
            del(self.validationresult)
202
203
        else:
204
            # bind object to self
205
            validationresult.submission = self.submission
206
            validationresult.content_object = self
207
            validationresult.save()
208
209
            # update cache (object already bound with save)
210
            self.__validationresult = validationresult
211
212
    @validationresult.deleter
213
    def validationresult(self):
214
        """return the first validationresult object (should be uinique)"""
215
216
        if not self.__validationresult:
217
            self.__validationresult = self.validationresults.first()
218
219
        # for a genericrelation, removin an object mean destroy it
220
        self.validationresults.remove(self.__validationresult)
221
        self.__validationresult = None
222
223
    class Meta:
224
        # Abstract base classes are useful when you want to put some common
225
        # information into a number of other models
226
        abstract = True
227
228
229
# --- dictionary tables
230
231
232
class DictRole(DictBase):
233
    """A class to model roles defined as childs of
234
    http://www.ebi.ac.uk/efo/EFO_0002012"""
235
236
    library_name = 'EFO'
237
238
    class Meta:
239
        # db_table will be <app_name>_<classname>
240
        verbose_name = "role"
241
        unique_together = (("label", "term"),)
242
243
244
class DictCountry(DictBase, Confidence):
245
    """A class to model contries defined by NCI Thesaurus OBO Edition
246
    https://www.ebi.ac.uk/ols/ontologies/ncit"""
247
248
    library_name = 'NCIT'
249
250
    class Meta:
251
        # db_table will be <app_name>_<classname>
252
        verbose_name = "country"
253
        verbose_name_plural = "countries"
254
        unique_together = (("label", "term"),)
255
        ordering = ['label']
256
257
258
class DictSex(DictBase):
259
    """A class to model sex as defined in PATO"""
260
261
    library_name = "PATO"
262
263
    class Meta:
264
        verbose_name = 'sex'
265
        verbose_name_plural = 'sex'
266
        unique_together = (("label", "term"),)
267
268
269
class DictUberon(DictBase, Confidence):
270
    """A class to model anatomies modeled in uberon"""
271
272
    library_name = "UBERON"
273
274
    class Meta:
275
        verbose_name = 'organism part'
276
        unique_together = (("label", "term"),)
277
278
279
class DictDevelStage(DictBase, Confidence):
280
    """A class to developmental stages defined as descendants of
281
    descendants of EFO_0000399"""
282
283
    library_name = 'EFO'
284
285
    class Meta:
286
        # db_table will be <app_name>_<classname>
287
        verbose_name = "developmental stage"
288
        unique_together = (("label", "term"),)
289
290
291
class DictPhysioStage(DictBase, Confidence):
292
    """A class to physiological stages defined as descendants of
293
    descendants of PATO_0000261"""
294
295
    library_name = 'PATO'
296
297
    class Meta:
298
        # db_table will be <app_name>_<classname>
299
        verbose_name = "physiological stage"
300
        unique_together = (("label", "term"),)
301
302
303
class DictSpecie(DictBase, Confidence):
304
    """A class to model species defined by NCBI organismal classification
305
    http://www.ebi.ac.uk/ols/ontologies/ncbitaxon"""
306
307
    library_name = "NCBITaxon"
308
309
    # set general breed to dictspecie objects
310
    general_breed_label = models.CharField(
311
        max_length=255,
312
        blank=True,
313
        null=True,
314
        help_text="Example: cattle breed",
315
        verbose_name="general breed label")
316
317
    general_breed_term = models.CharField(
318
        max_length=255,
319
        blank=True,
320
        null=True,
321
        help_text="Example: LBO_0000001",
322
        verbose_name="general breed term")
323
324
    @property
325
    def taxon_id(self):
326
        if not self.term or self.term == '':
327
            return None
328
329
        return int(self.term.split("_")[-1])
330
331
    class Meta:
332
        # db_table will be <app_name>_<classname>
333
        verbose_name = "specie"
334
        unique_together = (("label", "term"),)
335
336
    @classmethod
337
    def get_by_synonym(cls, synonym, language):
338
        """return an instance by synonym in supplied language or default one"""
339
340
        # get a queryset with speciesynonym
341
        qs = cls.objects.prefetch_related('speciesynonym_set')
342
343
        # annotate queryset by removing spaces from speciesynonym word
344
        qs = qs.annotate(
345
            new_word=Replace('speciesynonym__word', Value(" "), Value("")),
346
            language=F('speciesynonym__language__label'))
347
348
        # now remove spaces from synonym
349
        synonym = synonym.replace(" ", "")
350
351
        try:
352
            specie = qs.get(
353
                new_word=synonym,
354
                language=language)
355
356
        except cls.DoesNotExist:
357
            specie = qs.get(
358
                new_word=synonym,
359
                language="United Kingdom")
360
361
        return specie
362
363
    @classmethod
364
    def get_specie_check_synonyms(cls, species_label, language):
365
        """get a DictSpecie object. Species are in latin names, but I can
366
        find also a common name in translation tables"""
367
368
        try:
369
            specie = cls.objects.get(label=species_label)
370
371
        except cls.DoesNotExist:
372
            logger.info("Search %s in %s synonyms" % (species_label, language))
373
            # search for language synonym (if I arrived here a synonym should
374
            # exists)
375
            specie = cls.get_by_synonym(
376
                synonym=species_label,
377
                language=language)
378
379
        return specie
380
381
382
class DictBreed(Confidence):
383
    """A class to deal with breed objects and their ontologies"""
384
385
    library_name = "LBO"
386
387
    # this was the description field in cryoweb v_breeds_species tables
388
    supplied_breed = models.CharField(max_length=255, blank=False)
389
390
    # those can't be null like other DictBase classes
391
    # HINT: if every breed should have a mapped breed referring a specie
392
    # at least, could I inherit from DictBase class?
393
    label = models.CharField(
394
        max_length=255,
395
        blank=False,
396
        null=True,
397
        verbose_name="mapped breed")
398
399
    # old mapped_breed_term
400
    term = models.CharField(
401
        max_length=255,
402
        blank=False,
403
        null=True,
404
        help_text="Example: LBO_0000347",
405
        verbose_name="mapped breed term")
406
407
    # using a constraint for country.
408
    country = models.ForeignKey(
409
        'DictCountry',
410
        on_delete=models.PROTECT)
411
412
    # using a constraint for specie
413
    specie = models.ForeignKey(
414
        'DictSpecie',
415
        on_delete=models.PROTECT)
416
417
    class Meta:
418
        verbose_name = 'breed'
419
        unique_together = (("supplied_breed", "specie", "country"),)
420
421
    def __str__(self):
422
        return "{supplied} - {country} ({mapped}, {specie})".format(
423
            country=self.country.label,
424
            supplied=self.supplied_breed,
425
            mapped=self.mapped_breed,
426
            specie=self.specie.label)
427
428
    @property
429
    def mapped_breed(self):
430
        """Alias for label attribute. Return general label if no term is
431
        found"""
432
433
        if self.label and self.label != '':
434
            return self.label
435
436
        elif (self.specie.general_breed_label and
437
              self.specie.general_breed_label != ''):
438
            return self.specie.general_breed_label
439
440
        else:
441
            return None
442
443
    @mapped_breed.setter
444
    def mapped_breed(self, label):
445
        """Alias for label attribute"""
446
447
        self.label = label
448
449
    @property
450
    def mapped_breed_term(self):
451
        """Alias for term attribute. Return general term if no term is found"""
452
453
        if self.term and self.term != '':
454
            return self.term
455
456
        elif (self.specie.general_breed_term and
457
              self.specie.general_breed_term != ''):
458
            return self.specie.general_breed_term
459
460
        else:
461
            return None
462
463
    @mapped_breed_term.setter
464
    def mapped_breed_term(self, term):
465
        """Alias for label attribute"""
466
467
        self.term = term
468
469
    def format_attribute(self):
470
        """Format mapped_breed attribute (with its ontology). Return None if
471
        no mapped_breed"""
472
473
        if not self.mapped_breed or not self.mapped_breed_term:
474
            return None
475
476
        library = Ontology.objects.get(library_name=self.library_name)
477
        library_uri = library.library_uri
478
479
        return format_attribute(
480
            value=self.mapped_breed,
481
            library_uri=library_uri,
482
            terms=self.mapped_breed_term)
483
484
485
# --- Other tables tables
486
487
488
class Animal(BioSampleMixin, Name):
489
    """
490
    Class to model Animal (Organism). Inherits from :py:class:`BioSampleMixin`,
491
    related to :py:class:`Name` through ``OneToOne`` relationship to model
492
    Animal name (Data source id), and with the same relationship to model
493
    ``mother`` and ``father`` of such animal. In case that parents are unknown,
494
    could be linked with Unkwnon animals for cryoweb data or doens't have
495
    relationship.Linked to :py:class:`DictBreed` dictionary
496
    table to model info on species and breed. Linked to
497
    :py:class:`Sample` to model Samples (Specimen from organims)::
498
499
        from uid.models import Animal
500
501
        # get animal using primary key
502
        animal = Animal.objects.get(pk=1)
503
504
        # get animal name
505
        data_source_id = animal.name
506
507
        # get animal's parents
508
        mother = animal.mother
509
        father = animal.father
510
511
        # get breed and species info
512
        print(animal.breed.supplied_breed)
513
        print(animal.breed.specie.label)
514
515
        # get all samples (specimen) for this animals
516
        samples = animal.sample_set.all()
517
    """
518
519
    material = models.CharField(
520
        max_length=255,
521
        default="Organism",
522
        editable=False)
523
524
    breed = models.ForeignKey(
525
        'DictBreed',
526
        db_index=True,
527
        on_delete=models.PROTECT)
528
529
    # species is in DictBreed table
530
531
    # using a constraint for sex
532
    sex = models.ForeignKey(
533
        'DictSex',
534
        null=True,
535
        on_delete=models.PROTECT)
536
537
    # check that father and mother are defined using Foreign Keys
538
    # HINT: mother and father are not mandatory in all datasource
539
    father = models.ForeignKey(
540
        'self',
541
        on_delete=models.CASCADE,
542
        null=True,
543
        related_name='father_of')
544
545
    mother = models.ForeignKey(
546
        'self',
547
        on_delete=models.CASCADE,
548
        null=True,
549
        related_name='mother_of')
550
551
    birth_date = models.DateField(
552
        blank=True,
553
        null=True,
554
        help_text='example: 2019-04-01')
555
556
    birth_location = models.CharField(
557
        max_length=255,
558
        blank=True,
559
        null=True)
560
561
    birth_location_latitude = models.FloatField(blank=True, null=True)
562
    birth_location_longitude = models.FloatField(blank=True, null=True)
563
564
    # accuracy field (enum)
565
    birth_location_accuracy = models.SmallIntegerField(
566
        choices=[x.value for x in ACCURACIES],
567
        help_text='example: unknown accuracy level, country level',
568
        null=False,
569
        blank=False,
570
        default=MISSING)
571
572
    class Meta:
573
        unique_together = (("name", "breed", "owner"),)
574
575
    @property
576
    def specie(self):
577
        return self.breed.specie
578
579
    @property
580
    def biosample_alias(self):
581
        return 'IMAGEA{0:09d}'.format(self.id)
582
583
    def get_attributes(self):
584
        """Return attributes like biosample needs"""
585
586
        attributes = super().get_attributes()
587
588
        attributes["Material"] = format_attribute(
589
            value="organism", terms="OBI_0100026")
590
591
        # TODO: how to model derived from (mother/father)?
592
593
        attributes['Supplied breed'] = format_attribute(
594
            value=self.breed.supplied_breed)
595
596
        # HINT: Ideally, I could retrieve an ontology id for countries
597
        attributes['EFABIS Breed country'] = format_attribute(
598
            value=self.breed.country.label)
599
600
        attributes['Mapped breed'] = self.breed.format_attribute()
601
602
        attributes['Sex'] = self.sex.format_attribute()
603
604
        # a datetime object should be not be converted in string here,
605
        # otherwise will not be filtered if NULL
606
        attributes['Birth date'] = format_attribute(
607
            value=self.birth_date, units="YYYY-MM-DD")
608
609
        attributes["Birth location"] = format_attribute(
610
            value=self.birth_location)
611
612
        attributes["Birth location longitude"] = format_attribute(
613
            value=self.birth_location_longitude,
614
            units="decimal degrees")
615
616
        attributes["Birth location latitude"] = format_attribute(
617
            value=self.birth_location_latitude,
618
            units="decimal degrees")
619
620
        attributes["Birth location accuracy"] = format_attribute(
621
            value=self.get_birth_location_accuracy_display())
622
623
        # filter out empty values
624
        attributes = {k: v for k, v in attributes.items() if v is not None}
625
626
        return attributes
627
628
    def get_relationship(self, nature="derived from"):
629
        """Get a relationship to this animal (call this method from a related
630
        object to get a connection to this element)
631
632
        Args:
633
            nature (str): set relationship nature (child of, derived_from, ...)
634
        """
635
636
        # if animal is already uploaded I will use accession as
637
        # relationship key. This biosample id could be tested in validation
638
        if self.biosample_id and self.biosample_id != '':
639
            return {
640
                "accession": self.biosample_id,
641
                "relationshipNature": nature,
642
            }
643
        else:
644
            return {
645
                "alias": self.biosample_alias,
646
                "relationshipNature": nature,
647
            }
648
649
    def get_father_relationship(self):
650
        """Get a relationship with father if possible"""
651
652
        # get father of this animal
653
        if self.father is None:
654
            return None
655
656
        return self.father.get_relationship(nature="child of")
657
658
    def get_mother_relationship(self):
659
        """Get a relationship with mother if possible"""
660
661
        # get mother of this animal
662
        if self.mother is None:
663
            return None
664
665
        return self.mother.get_relationship(nature="child of")
666
667
    def to_biosample(self, release_date=None):
668
        """get a json from animal for biosample submission"""
669
670
        # call methods defined in BioSampleMixin and get result
671
        # with USI mandatory keys and attributes
672
        result = super().to_biosample(release_date)
673
674
        # define relationship with mother and father (if possible)
675
        result['sampleRelationships'] = []
676
677
        father_relationship = self.get_father_relationship()
678
679
        if father_relationship is not None:
680
            result['sampleRelationships'].append(father_relationship)
681
682
        mother_relationship = self.get_mother_relationship()
683
684
        if mother_relationship is not None:
685
            result['sampleRelationships'].append(mother_relationship)
686
687
        return result
688
689
    def get_absolute_url(self):
690
        return reverse("animals:detail", kwargs={"pk": self.pk})
691
692
693
class Sample(BioSampleMixin, Name):
694
    material = models.CharField(
695
        max_length=255,
696
        default="Specimen from Organism",
697
        editable=False)
698
699
    animal = models.ForeignKey(
700
        'Animal',
701
        on_delete=models.CASCADE)
702
703
    # HINT: should this be a protocol?
704
    protocol = models.CharField(
705
        max_length=255,
706
        blank=True,
707
        null=True)
708
709
    collection_date = models.DateField(
710
        blank=True,
711
        null=True,
712
        help_text='example: 2019-04-01')
713
714
    collection_place_latitude = models.FloatField(blank=True, null=True)
715
    collection_place_longitude = models.FloatField(blank=True, null=True)
716
    collection_place = models.CharField(max_length=255, blank=True, null=True)
717
718
    # accuracy field (enum)
719
    collection_place_accuracy = models.SmallIntegerField(
720
        choices=[x.value for x in ACCURACIES],
721
        help_text='example: unknown accuracy level, country level',
722
        null=False,
723
        blank=False,
724
        default=MISSING)
725
726
    # using a constraint for organism (DictUberon)
727
    organism_part = models.ForeignKey(
728
        'DictUberon',
729
        null=True,
730
        on_delete=models.PROTECT)
731
732
    # using a constraint for developmental stage (DictDevelStage)
733
    developmental_stage = models.ForeignKey(
734
        'DictDevelStage',
735
        null=True,
736
        blank=True,
737
        on_delete=models.PROTECT)
738
739
    physiological_stage = models.ForeignKey(
740
        'DictPhysioStage',
741
        null=True,
742
        blank=True,
743
        on_delete=models.PROTECT)
744
745
    animal_age_at_collection = models.IntegerField(
746
        null=True,
747
        blank=True)
748
749
    animal_age_at_collection_units = models.SmallIntegerField(
750
        choices=[x.value for x in TIME_UNITS],
751
        help_text='example: years',
752
        null=True,
753
        blank=True)
754
755
    availability = models.CharField(
756
        max_length=255,
757
        blank=True,
758
        null=True,
759
        help_text=(
760
            "Either a link to a web page giving information on who to contact "
761
            "or an e-mail address to contact about availability. If neither "
762
            "available, please use the value no longer available")
763
    )
764
765
    storage = models.SmallIntegerField(
766
        choices=[x.value for x in SAMPLE_STORAGE],
767
        help_text='How the sample was stored',
768
        null=True,
769
        blank=True)
770
771
    storage_processing = models.SmallIntegerField(
772
        choices=[x.value for x in SAMPLE_STORAGE_PROCESSING],
773
        help_text='How the sample was prepared for storage',
774
        null=True,
775
        blank=True)
776
777
    preparation_interval = models.IntegerField(
778
        blank=True,
779
        null=True)
780
781
    preparation_interval_units = models.SmallIntegerField(
782
        choices=[x.value for x in TIME_UNITS],
783
        help_text='example: years',
784
        null=True,
785
        blank=True)
786
787
    class Meta:
788
        unique_together = (("name", "animal", "owner"),)
789
790
    @property
791
    def specie(self):
792
        return self.animal.breed.specie
793
794
    @property
795
    def biosample_alias(self):
796
        return 'IMAGES{0:09d}'.format(self.id)
797
798
    def get_attributes(self):
799
        """Return attributes like biosample needs"""
800
801
        attributes = super().get_attributes()
802
803
        attributes["Material"] = format_attribute(
804
            value="specimen from organism", terms="OBI_0001479")
805
806
        # The data source id or alternative id of the animal from which
807
        # the sample was collected (see Animal.to_biosample())
808
        attributes['Derived from'] = format_attribute(
809
            value=self.animal.name)
810
811
        attributes["Specimen collection protocol"] = format_attribute(
812
            value=self.protocol)
813
814
        # a datetime object should be not be converted in string here,
815
        # otherwise will not be filtered if NULL
816
        attributes['Collection date'] = format_attribute(
817
            value=self.collection_date, units="YYYY-MM-DD")
818
819
        attributes['Collection place'] = format_attribute(
820
            value=self.collection_place)
821
822
        attributes["Collection place longitude"] = format_attribute(
823
            value=self.collection_place_longitude,
824
            units="decimal degrees")
825
826
        attributes["Collection place latitude"] = format_attribute(
827
            value=self.collection_place_latitude,
828
            units="decimal degrees")
829
830
        attributes["Collection place accuracy"] = format_attribute(
831
            value=self.get_collection_place_accuracy_display())
832
833
        # this will point to a correct term dictionary table
834
        if self.organism_part:
835
            attributes['Organism part'] = self.organism_part.format_attribute()
836
837
        if self.developmental_stage:
838
            attributes['Developmental stage'] = \
839
                self.developmental_stage.format_attribute()
840
841
        if self.physiological_stage:
842
            attributes['Physiological stage'] = \
843
                self.physiological_stage.format_attribute()
844
845
        attributes['Animal age at collection'] = format_attribute(
846
            value=self.animal_age_at_collection,
847
            units=self.get_animal_age_at_collection_units_display())
848
849
        attributes['Availability'] = format_attribute(
850
            value=self.availability)
851
852
        attributes['Sample storage'] = format_attribute(
853
            value=self.get_storage_display())
854
855
        attributes['Sample storage processing'] = format_attribute(
856
            value=self.get_storage_processing_display())
857
858
        attributes['Sampling to preparation interval'] = format_attribute(
859
            value=self.preparation_interval,
860
            units=self.get_preparation_interval_units_display())
861
862
        # filter out empty values
863
        attributes = {k: v for k, v in attributes.items() if v is not None}
864
865
        return attributes
866
867
    def to_biosample(self, release_date=None):
868
        """get a json from sample for biosample submission"""
869
870
        # call methods defined in BioSampleMixin and get result
871
        # with USI mandatory keys and attributes
872
        result = super().to_biosample(release_date)
873
874
        # define relationship to the animal where this sample come from
875
        result['sampleRelationships'] = [self.animal.get_relationship()]
876
877
        return result
878
879
    def get_absolute_url(self):
880
        return reverse("samples:detail", kwargs={"pk": self.pk})
881
882
883
class Person(BaseMixin, models.Model):
884
    user = models.OneToOneField(
885
        User,
886
        on_delete=models.CASCADE,
887
        related_name='person')
888
889
    initials = models.CharField(max_length=255, blank=True, null=True)
890
891
    # HINT: with a OneToOneField relation, there will be only one user for
892
    # each organization
893
    affiliation = models.ForeignKey(
894
        'Organization',
895
        null=True,
896
        on_delete=models.PROTECT,
897
        help_text="The institution you belong to")
898
899
    # last_name, first_name and email come from User model
900
901
    role = models.ForeignKey(
902
        'DictRole',
903
        on_delete=models.PROTECT,
904
        null=True)
905
906
    def __str__(self):
907
        return "{name} {surname} ({affiliation})".format(
908
                name=self.user.first_name,
909
                surname=self.user.last_name,
910
                affiliation=self.affiliation)
911
912
913
class Organization(BaseMixin, models.Model):
914
    # id = models.IntegerField(primary_key=True)  # AutoField?
915
    name = models.CharField(max_length=255)
916
    address = models.CharField(
917
        max_length=255, blank=True, null=True,
918
        help_text='One line, comma separated')
919
920
    country = models.ForeignKey(
921
        'DictCountry',
922
        on_delete=models.PROTECT)
923
924
    URI = models.URLField(
925
        max_length=500, blank=True, null=True,
926
        help_text='Web site')
927
928
    role = models.ForeignKey(
929
        'DictRole',
930
        on_delete=models.PROTECT)
931
932
    def __str__(self):
933
        return "%s (%s)" % (self.name, self.country.label)
934
935
    class Meta:
936
        ordering = ['name', 'country']
937
938
939
class Publication(BaseMixin, models.Model):
940
    # this is a non mandatory fields in ruleset
941
    doi = models.CharField(
942
        max_length=255,
943
        help_text='Valid Digital Object Identifier')
944
945
    def __str__(self):
946
        return self.doi
947
948
949
class Ontology(BaseMixin, models.Model):
950
    library_name = models.CharField(
951
        max_length=255,
952
        help_text='Each value must be unique',
953
        unique=True)
954
955
    library_uri = models.URLField(
956
        max_length=500, blank=True, null=True,
957
        help_text='Each value must be unique ' +
958
                  'and with a valid URL')
959
960
    comment = models.CharField(
961
            max_length=255, blank=True, null=True)
962
963
    def __str__(self):
964
        return self.library_name
965
966
    class Meta:
967
        verbose_name_plural = "ontologies"
968
969
970
class Submission(BaseMixin, models.Model):
971
    title = models.CharField(
972
        "Submission title",
973
        max_length=255,
974
        help_text='Example: Roslin Sheep Atlas')
975
976
    project = models.CharField(
977
        max_length=25,
978
        default="IMAGE",
979
        editable=False)
980
981
    description = models.CharField(
982
        max_length=255,
983
        help_text='Example: The Roslin Institute ' +
984
                  'Sheep Gene Expression Atlas Project')
985
986
    # gene bank fields
987
    gene_bank_name = models.CharField(
988
        max_length=255,
989
        blank=False,
990
        null=False,
991
        help_text='example: CryoWeb')
992
993
    gene_bank_country = models.ForeignKey(
994
        'DictCountry',
995
        on_delete=models.PROTECT)
996
997
    # datasource field
998
    datasource_type = models.SmallIntegerField(
999
        "Data source type",
1000
        choices=[x.value for x in DATA_TYPES],
1001
        help_text='example: CryoWeb')
1002
1003
    datasource_version = models.CharField(
1004
        "Data source version",
1005
        max_length=255,
1006
        blank=False,
1007
        null=False,
1008
        help_text='examples: "2018-04-27", "version 1.5"')
1009
1010
    organization = models.ForeignKey(
1011
        'Organization',
1012
        on_delete=models.PROTECT,
1013
        help_text="Who owns the data")
1014
1015
    # custom fields for datasource
1016
    upload_dir = 'data_source/'
1017
1018
    # File will be stored to PROTECTED_MEDIA_ROOT + upload_to
1019
    # https://gist.github.com/cobusc/ea1d01611ef05dacb0f33307e292abf4
1020
    uploaded_file = ProtectedFileField(upload_to=upload_dir)
1021
1022
    # when submission is created
1023
    created_at = models.DateTimeField(auto_now_add=True)
1024
1025
    # https://simpleisbetterthancomplex.com/tips/2016/05/23/django-tip-4-automatic-datetime-fields.html
1026
    updated_at = models.DateTimeField(auto_now=True)
1027
1028
    # a column to track submission status
1029
    status = models.SmallIntegerField(
1030
            choices=[x.value for x in STATUSES],
1031
            help_text='example: Waiting',
1032
            default=WAITING)
1033
1034
    # a field to track errors in UID loading. Should be blank if no errors
1035
    # are found
1036
    message = models.TextField(
1037
        null=True,
1038
        blank=True)
1039
1040
    owner = models.ForeignKey(
1041
        User,
1042
        on_delete=models.CASCADE)
1043
1044
    class Meta:
1045
        # HINT: can I put two files for my cryoweb instance? May they have two
1046
        # different version
1047
        unique_together = ((
1048
            "gene_bank_name",
1049
            "gene_bank_country",
1050
            "datasource_type",
1051
            "datasource_version",
1052
            "owner"),)
1053
1054
    def __str__(self):
1055
        return "%s (%s, %s)" % (
1056
            self.gene_bank_name,
1057
            self.gene_bank_country.label,
1058
            self.datasource_version
1059
        )
1060
1061
    def get_uploaded_file_basename(self):
1062
        return os.path.basename(self.uploaded_file.name)
1063
1064
    def get_uploaded_file_path(self):
1065
        """Return uploaded file path in docker container"""
1066
1067
        # this is the full path in docker container
1068
        fullpath = self.uploaded_file.file
1069
1070
        # get a string and quote fullpath
1071
        return shlex.quote(str(fullpath))
1072
1073
    def get_absolute_url(self):
1074
        return reverse("submissions:detail", kwargs={"pk": self.pk})
1075
1076
    def __can_I(self, names):
1077
        """Return True id self.status in statuses"""
1078
1079
        statuses = [x.value[0] for x in STATUSES if x.name in names]
1080
1081
        if self.status not in statuses:
1082
            return True
1083
1084
        else:
1085
            return False
1086
1087
    def can_edit(self):
1088
        """Returns True if I can edit a submission"""
1089
1090
        names = ['waiting', 'submitted']
1091
1092
        return self.__can_I(names)
1093
1094
    def can_validate(self):
1095
        names = ['error', 'waiting', 'submitted', 'completed']
1096
1097
        return self.__can_I(names)
1098
1099
    def can_submit(self):
1100
        names = ['ready']
1101
1102
        # this is the opposite of self.__can_I
1103
        statuses = [x.value[0] for x in STATUSES if x.name in names]
1104
1105
        # self.status need to be in statuses for submitting
1106
        if self.status in statuses:
1107
            return True
1108
1109
        else:
1110
            return False
1111
1112
1113
# --- Custom functions
1114
1115
1116
# https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html#onetoone
1117
# we will now define signals so our Person model will be automatically
1118
# created/updated when we create/update User instances.
1119
# Basically we are hooking the create_user_person and save_user_person
1120
# methods to the User model, whenever a save event occurs. This kind of signal
1121
# is called post_save.
1122
# TODO: add default values when creating a superuser
1123
@receiver(post_save, sender=User)
1124
def create_user_person(sender, instance, created, **kwargs):
1125
    if created:
1126
        Person.objects.create(user=instance)
1127
1128
1129
@receiver(post_save, sender=User)
1130
def save_user_person(sender, instance, **kwargs):
1131
    instance.person.save()
1132
1133
1134
# A method to truncate database
1135
def truncate_database():
1136
    """Truncate image database"""
1137
1138
    logger.warning("Truncating ALL image tables")
1139
1140
    # call each class and truncate its table by calling truncate method
1141
    Animal.truncate()
1142
    DictBreed.truncate()
1143
    DictCountry.truncate()
1144
    DictRole.truncate()
1145
    DictSex.truncate()
1146
    DictSpecie.truncate()
1147
    DictUberon.truncate()
1148
    DictDevelStage.truncate()
1149
    DictPhysioStage.truncate()
1150
    Ontology.truncate()
1151
    Organization.truncate()
1152
    Person.truncate()
1153
    Publication.truncate()
1154
    Sample.truncate()
1155
    Submission.truncate()
1156
1157
    logger.warning("All cryoweb tables were truncated")
1158
1159
1160
def truncate_filled_tables():
1161
    """Truncate filled tables by import processes"""
1162
1163
    logger.warning("Truncating filled tables tables")
1164
1165
    # call each class and truncate its table by calling truncate method
1166
    Animal.truncate()
1167
    Publication.truncate()
1168
    Sample.truncate()
1169
    Submission.truncate()
1170
1171
    logger.warning("All filled tables were truncated")
1172
1173
1174
def uid_report(user):
1175
    """Performs a statistic on UID database to find issues. require user as
1176
    request.user"""
1177
1178
    report = {}
1179
1180
    # get n_of_animals
1181
    report['n_of_animals'] = Animal.objects.filter(
1182
        owner=user).count()
1183
1184
    # get n_of_samples
1185
    report['n_of_samples'] = Sample.objects.filter(
1186
        owner=user).count()
1187
1188
    # merging dictionaries: https://stackoverflow.com/a/26853961
1189
    # HINT: have they sense in a per user statistic?
1190
    report = {**report, **missing_terms()}
1191
1192
    return report
1193
1194
1195
def missing_terms():
1196
    """Get informations about dictionary terms without ontologies"""
1197
1198
    # a list with all dictionary classes
1199
    dict_classes = [
1200
        DictBreed, DictCountry, DictSpecie, DictUberon, DictDevelStage,
1201
        DictPhysioStage
1202
    ]
1203
1204
    # get a dictionary to report data
1205
    report = {}
1206
1207
    for dict_class in dict_classes:
1208
        # get a queryset with missing terms
1209
        missing = dict_class.objects.filter(term=None)
1210
1211
        # set a key for report dictionary
1212
        key = "%s_without_ontology" % (
1213
            dict_class._meta.verbose_name_plural.replace(" ", "_"))
1214
1215
        # track counts
1216
        report[key] = missing.count()
1217
1218
        # add the total value
1219
        total = dict_class.objects.count()
1220
1221
        # set a key for report dictionary
1222
        key = "%s_total" % (
1223
            dict_class._meta.verbose_name_plural.replace(" ", "_"))
1224
1225
        # track counts
1226
        report[key] = total
1227
1228
    return report
1229
1230
1231
# A method to discover is image database has data or not
1232
def db_has_data():
1233
    # Test only tables I read data to fill UID
1234
    if (Submission.objects.exists() and
1235
            Animal.objects.exists() and
1236
            Sample.objects.exists()):
1237
        return True
1238
1239
    else:
1240
        return False
1241