uid.models.truncate_filled_tables()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 12
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 7
dl 0
loc 12
rs 10
c 0
b 0
f 0
cc 1
nop 0
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
    READY)
19
from common.helpers import format_attribute
20
21
from .mixins import BaseMixin, BioSampleMixin
22
23
# Get an instance of a logger
24
logger = logging.getLogger(__name__)
25
26
27
class Replace(Func):
28
    function = 'REPLACE'
29
30
31
# --- Abstract classes
32
33
34
# helper classes
35
class DictBase(BaseMixin, models.Model):
36
    """
37
    Abstract class to be inherited to all dictionary tables. It models fields
38
    like ``label`` (the revised term like submitter or blood) and
39
    ``term`` (the ontology id as the final part of the URI link)
40
41
    The fixed part of the URI could be customized from :py:class:`Ontology`
42
    by setting ``library_name`` class attribute accordingly::
43
44
        class DictRole(DictBase):
45
            library_name = 'EFO'
46
    """
47
48
    library_name = None
49
50
    # if not defined, this table will have an own primary key
51
    label = models.CharField(
52
            max_length=255,
53
            blank=False,
54
            help_text="Example: submitter")
55
56
    term = models.CharField(
57
            max_length=255,
58
            blank=False,
59
            null=True,
60
            help_text="Example: EFO_0001741")
61
62
    class Meta:
63
        # Abstract base classes are useful when you want to put some common
64
        # information into a number of other models
65
        abstract = True
66
67
    def __str__(self):
68
        return "{label} ({term})".format(
69
                label=self.label,
70
                term=self.term)
71
72
    def format_attribute(self):
73
        """
74
        Format an object instance as a dictionary used by biosample, for
75
        example::
76
77
            [{
78
                'value': 'submitter',
79
                'terms': [{'url': 'http://www.ebi.ac.uk/efo/EFO_0001741'}]
80
            }]
81
82
        the fixed part of URI link is defined by ``library_name`` class
83
        attribute
84
        """
85
86
        if self.library_name is None:
87
            logger.warning("library_name not defined")
88
            library_uri = OBO_URL
89
90
        else:
91
            library = Ontology.objects.get(library_name=self.library_name)
92
            library_uri = library.library_uri
93
94
        return format_attribute(
95
            value=self.label,
96
            library_uri=library_uri,
97
            terms=self.term)
98
99
100
class Confidence(BaseMixin, models.Model):
101
    """
102
    Abstract class which add :ref:`confidence <Common confidences>`
103
    to models
104
    """
105
106
    # confidence field (enum)
107
    confidence = models.SmallIntegerField(
108
        choices=[x.value for x in CONFIDENCES],
109
        help_text='example: Manually Curated',
110
        null=True)
111
112
    class Meta:
113
        # Abstract base classes are useful when you want to put some common
114
        # information into a number of other models
115
        abstract = True
116
117
118
class Name(BaseMixin, models.Model):
119
    """Model UID names: define a name (sample or animal) unique for each
120
    data submission"""
121
122
    __validationresult = None
123
124
    # two different animal may have the same name. Its unicity depens on
125
    # data source name and version
126
    name = models.CharField(
127
            max_length=255,
128
            blank=False,
129
            null=False)
130
131
    submission = models.ForeignKey(
132
        'Submission',
133
        related_name='%(class)s_set',
134
        on_delete=models.CASCADE)
135
136
    # This will be assigned after submission
137
    biosample_id = models.CharField(
138
        max_length=255,
139
        blank=True,
140
        null=True,
141
        unique=True)
142
143
    # '+' instructs Django that we don’t need this reverse relationship
144
    owner = models.ForeignKey(
145
        User,
146
        related_name='+',
147
        on_delete=models.CASCADE)
148
149
    # a column to track submission status
150
    status = models.SmallIntegerField(
151
            choices=[x.value for x in STATUSES if x.name in NAME_STATUSES],
152
            help_text='example: Submitted',
153
            default=LOADED)
154
155
    last_changed = models.DateTimeField(
156
        auto_now_add=True,
157
        blank=True,
158
        null=True)
159
160
    last_submitted = models.DateTimeField(
161
        blank=True,
162
        null=True)
163
164
    publication = models.ForeignKey(
165
        'Publication',
166
        null=True,
167
        blank=True,
168
        on_delete=models.SET_NULL)
169
170
    # alternative id will store the internal id in data source
171
    alternative_id = models.CharField(
172
        max_length=255)
173
174
    description = models.CharField(
175
        max_length=255,
176
        blank=True,
177
        null=True)
178
179
    validationresults = GenericRelation(
180
        'validation.ValidationResult',
181
        related_query_name='%(class)ss')
182
183
    # https://www.machinelearningplus.com/python/python-property/
184
    # https://stackoverflow.com/questions/7837330/generic-one-to-one-relation-in-django
185
    @property
186
    def validationresult(self):
187
        """return the first validationresult object (should be uinique)"""
188
189
        if not self.__validationresult:
190
            self.__validationresult = self.validationresults.first()
191
192
        return self.__validationresult
193
194
    @validationresult.setter
195
    def validationresult(self, validationresult):
196
        """return the first validationresult object (should be uinique)"""
197
198
        # destroying relathionship when passin a None object
199
        if not validationresult:
200
            del(self.validationresult)
201
202
        else:
203
            # bind object to self
204
            validationresult.submission = self.submission
205
            validationresult.content_object = self
206
            validationresult.save()
207
208
            # update cache (object already bound with save)
209
            self.__validationresult = validationresult
210
211
    @validationresult.deleter
212
    def validationresult(self):
213
        """return the first validationresult object (should be uinique)"""
214
215
        if not self.__validationresult:
216
            self.__validationresult = self.validationresults.first()
217
218
        # for a genericrelation, removin an object mean destroy it
219
        self.validationresults.remove(self.__validationresult)
220
        self.__validationresult = None
221
222
    class Meta:
223
        # Abstract base classes are useful when you want to put some common
224
        # information into a number of other models
225
        abstract = True
226
227
228
# --- dictionary tables
229
230
231
class DictRole(DictBase):
232
    """A class to model roles defined as childs of
233
    http://www.ebi.ac.uk/efo/EFO_0002012"""
234
235
    library_name = 'EFO'
236
237
    class Meta:
238
        # db_table will be <app_name>_<classname>
239
        verbose_name = "role"
240
        unique_together = (("label", "term"),)
241
        ordering = ['label', ]
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
        on_delete=models.PROTECT)
535
536
    # check that father and mother are defined using Foreign Keys
537
    # HINT: mother and father are not mandatory in all datasource
538
    father = models.ForeignKey(
539
        'self',
540
        on_delete=models.CASCADE,
541
        null=True,
542
        related_name='father_of')
543
544
    mother = models.ForeignKey(
545
        'self',
546
        on_delete=models.CASCADE,
547
        null=True,
548
        related_name='mother_of')
549
550
    birth_date = models.DateField(
551
        blank=True,
552
        null=True,
553
        help_text='example: 2019-04-01')
554
555
    birth_location = models.CharField(
556
        max_length=255,
557
        blank=True,
558
        null=True)
559
560
    birth_location_latitude = models.FloatField(blank=True, null=True)
561
    birth_location_longitude = models.FloatField(blank=True, null=True)
562
563
    # accuracy field (enum)
564
    birth_location_accuracy = models.SmallIntegerField(
565
        choices=[x.value for x in ACCURACIES],
566
        help_text='example: unknown accuracy level, country level',
567
        null=False,
568
        blank=False,
569
        default=MISSING)
570
571
    class Meta:
572
        unique_together = (("name", "breed", "owner"),)
573
574
    @property
575
    def specie(self):
576
        return self.breed.specie
577
578
    @property
579
    def biosample_alias(self):
580
        return 'IMAGEA{0:09d}'.format(self.id)
581
582
    def get_attributes(self):
583
        """Return attributes like biosample needs"""
584
585
        attributes = super().get_attributes()
586
587
        attributes["Material"] = format_attribute(
588
            value="organism", terms="OBI_0100026")
589
590
        # TODO: how to model derived from (mother/father)?
591
592
        attributes['Supplied breed'] = format_attribute(
593
            value=self.breed.supplied_breed)
594
595
        # HINT: Ideally, I could retrieve an ontology id for countries
596
        attributes['EFABIS Breed country'] = format_attribute(
597
            value=self.breed.country.label)
598
599
        attributes['Mapped breed'] = self.breed.format_attribute()
600
601
        attributes['Sex'] = self.sex.format_attribute()
602
603
        # a datetime object should be not be converted in string here,
604
        # otherwise will not be filtered if NULL
605
        attributes['Birth date'] = format_attribute(
606
            value=self.birth_date, units="YYYY-MM-DD")
607
608
        attributes["Birth location"] = format_attribute(
609
            value=self.birth_location)
610
611
        attributes["Birth location longitude"] = format_attribute(
612
            value=self.birth_location_longitude,
613
            units="decimal degrees")
614
615
        attributes["Birth location latitude"] = format_attribute(
616
            value=self.birth_location_latitude,
617
            units="decimal degrees")
618
619
        attributes["Birth location accuracy"] = format_attribute(
620
            value=self.get_birth_location_accuracy_display())
621
622
        # filter out empty values
623
        attributes = {k: v for k, v in attributes.items() if v is not None}
624
625
        return attributes
626
627
    def get_relationship(self, nature="derived from"):
628
        """Get a relationship to this animal (call this method from a related
629
        object to get a connection to this element)
630
631
        Args:
632
            nature (str): set relationship nature (child of, derived_from, ...)
633
        """
634
635
        # if animal is already uploaded I will use accession as
636
        # relationship key. This biosample id could be tested in validation
637
        if self.biosample_id and self.biosample_id != '':
638
            return {
639
                "accession": self.biosample_id,
640
                "relationshipNature": nature,
641
            }
642
        else:
643
            return {
644
                "alias": self.biosample_alias,
645
                "relationshipNature": nature,
646
            }
647
648
    def get_father_relationship(self):
649
        """Get a relationship with father if possible"""
650
651
        # get father of this animal
652
        if self.father is None:
653
            return None
654
655
        return self.father.get_relationship(nature="child of")
656
657
    def get_mother_relationship(self):
658
        """Get a relationship with mother if possible"""
659
660
        # get mother of this animal
661
        if self.mother is None:
662
            return None
663
664
        return self.mother.get_relationship(nature="child of")
665
666
    def to_biosample(self, release_date=None):
667
        """get a json from animal for biosample submission"""
668
669
        # call methods defined in BioSampleMixin and get result
670
        # with USI mandatory keys and attributes
671
        result = super().to_biosample(release_date)
672
673
        # define relationship with mother and father (if possible)
674
        result['sampleRelationships'] = []
675
676
        father_relationship = self.get_father_relationship()
677
678
        if father_relationship is not None:
679
            result['sampleRelationships'].append(father_relationship)
680
681
        mother_relationship = self.get_mother_relationship()
682
683
        if mother_relationship is not None:
684
            result['sampleRelationships'].append(mother_relationship)
685
686
        return result
687
688
    def get_absolute_url(self):
689
        return reverse("animals:detail", kwargs={"pk": self.pk})
690
691
692
class Sample(BioSampleMixin, Name):
693
    material = models.CharField(
694
        max_length=255,
695
        default="Specimen from Organism",
696
        editable=False)
697
698
    animal = models.ForeignKey(
699
        'Animal',
700
        on_delete=models.CASCADE)
701
702
    # HINT: should this be a protocol?
703
    protocol = models.CharField(
704
        max_length=255,
705
        blank=True,
706
        null=True)
707
708
    collection_date = models.DateField(
709
        blank=True,
710
        null=True,
711
        help_text='example: 2019-04-01')
712
713
    collection_place_latitude = models.FloatField(blank=True, null=True)
714
    collection_place_longitude = models.FloatField(blank=True, null=True)
715
    collection_place = models.CharField(max_length=255, blank=True, null=True)
716
717
    # accuracy field (enum)
718
    collection_place_accuracy = models.SmallIntegerField(
719
        choices=[x.value for x in ACCURACIES],
720
        help_text='example: unknown accuracy level, country level',
721
        null=False,
722
        blank=False,
723
        default=MISSING)
724
725
    # using a constraint for organism (DictUberon)
726
    organism_part = models.ForeignKey(
727
        'DictUberon',
728
        on_delete=models.PROTECT)
729
730
    # using a constraint for developmental stage (DictDevelStage)
731
    developmental_stage = models.ForeignKey(
732
        'DictDevelStage',
733
        null=True,
734
        blank=True,
735
        on_delete=models.PROTECT)
736
737
    physiological_stage = models.ForeignKey(
738
        'DictPhysioStage',
739
        null=True,
740
        blank=True,
741
        on_delete=models.PROTECT)
742
743
    animal_age_at_collection = models.IntegerField(
744
        null=True,
745
        blank=True)
746
747
    animal_age_at_collection_units = models.SmallIntegerField(
748
        choices=[x.value for x in TIME_UNITS],
749
        help_text='example: years',
750
        null=True,
751
        blank=True)
752
753
    availability = models.CharField(
754
        max_length=255,
755
        blank=True,
756
        null=True,
757
        help_text=(
758
            "Either a link to a web page giving information on who to contact "
759
            "or an e-mail address to contact about availability. If neither "
760
            "available, please use the value no longer available")
761
    )
762
763
    storage = models.SmallIntegerField(
764
        choices=[x.value for x in SAMPLE_STORAGE],
765
        help_text='How the sample was stored',
766
        null=True,
767
        blank=True)
768
769
    storage_processing = models.SmallIntegerField(
770
        choices=[x.value for x in SAMPLE_STORAGE_PROCESSING],
771
        help_text='How the sample was prepared for storage',
772
        null=True,
773
        blank=True)
774
775
    preparation_interval = models.IntegerField(
776
        blank=True,
777
        null=True)
778
779
    preparation_interval_units = models.SmallIntegerField(
780
        choices=[x.value for x in TIME_UNITS],
781
        help_text='example: years',
782
        null=True,
783
        blank=True)
784
785
    class Meta:
786
        unique_together = (("name", "animal", "owner"),)
787
788
    @property
789
    def specie(self):
790
        return self.animal.breed.specie
791
792
    @property
793
    def biosample_alias(self):
794
        return 'IMAGES{0:09d}'.format(self.id)
795
796
    def get_attributes(self):
797
        """Return attributes like biosample needs"""
798
799
        attributes = super().get_attributes()
800
801
        attributes["Material"] = format_attribute(
802
            value="specimen from organism", terms="OBI_0001479")
803
804
        # The data source id or alternative id of the animal from which
805
        # the sample was collected (see Animal.to_biosample())
806
        attributes['Derived from'] = format_attribute(
807
            value=self.animal.name)
808
809
        attributes["Specimen collection protocol"] = format_attribute(
810
            value=self.protocol)
811
812
        # a datetime object should be not be converted in string here,
813
        # otherwise will not be filtered if NULL
814
        attributes['Collection date'] = format_attribute(
815
            value=self.collection_date, units="YYYY-MM-DD")
816
817
        attributes['Collection place'] = format_attribute(
818
            value=self.collection_place)
819
820
        attributes["Collection place longitude"] = format_attribute(
821
            value=self.collection_place_longitude,
822
            units="decimal degrees")
823
824
        attributes["Collection place latitude"] = format_attribute(
825
            value=self.collection_place_latitude,
826
            units="decimal degrees")
827
828
        attributes["Collection place accuracy"] = format_attribute(
829
            value=self.get_collection_place_accuracy_display())
830
831
        # this will point to a correct term dictionary table
832
        if self.organism_part:
833
            attributes['Organism part'] = self.organism_part.format_attribute()
834
835
        if self.developmental_stage:
836
            attributes['Developmental stage'] = \
837
                self.developmental_stage.format_attribute()
838
839
        if self.physiological_stage:
840
            attributes['Physiological stage'] = \
841
                self.physiological_stage.format_attribute()
842
843
        attributes['Animal age at collection'] = format_attribute(
844
            value=self.animal_age_at_collection,
845
            units=self.get_animal_age_at_collection_units_display())
846
847
        attributes['Availability'] = format_attribute(
848
            value=self.availability)
849
850
        attributes['Sample storage'] = format_attribute(
851
            value=self.get_storage_display())
852
853
        attributes['Sample storage processing'] = format_attribute(
854
            value=self.get_storage_processing_display())
855
856
        attributes['Sampling to preparation interval'] = format_attribute(
857
            value=self.preparation_interval,
858
            units=self.get_preparation_interval_units_display())
859
860
        # filter out empty values
861
        attributes = {k: v for k, v in attributes.items() if v is not None}
862
863
        return attributes
864
865
    def to_biosample(self, release_date=None):
866
        """get a json from sample for biosample submission"""
867
868
        # call methods defined in BioSampleMixin and get result
869
        # with USI mandatory keys and attributes
870
        result = super().to_biosample(release_date)
871
872
        # define relationship to the animal where this sample come from
873
        result['sampleRelationships'] = [self.animal.get_relationship()]
874
875
        return result
876
877
    def get_absolute_url(self):
878
        return reverse("samples:detail", kwargs={"pk": self.pk})
879
880
881
class Person(BaseMixin, models.Model):
882
    user = models.OneToOneField(
883
        User,
884
        on_delete=models.CASCADE,
885
        related_name='person')
886
887
    initials = models.CharField(
888
        max_length=255,
889
        blank=True,
890
        null=True)
891
892
    # HINT: with a OneToOneField relation, there will be only one user for
893
    # each organization
894
    affiliation = models.ForeignKey(
895
        'Organization',
896
        null=True,
897
        on_delete=models.PROTECT,
898
        help_text="The institution you belong to")
899
900
    # last_name, first_name and email come from User model
901
902
    # role can be null in order to load fixtures and create user before adding
903
    # person data. The same applies on organization
904
    role = models.ForeignKey(
905
        'DictRole',
906
        on_delete=models.PROTECT,
907
        help_text="Your role, for example 'submitter'",
908
        null=True)
909
910
    def __str__(self):
911
        return "{name} {surname} ({affiliation})".format(
912
                name=self.user.first_name,
913
                surname=self.user.last_name,
914
                affiliation=self.affiliation)
915
916
917
class Organization(BaseMixin, models.Model):
918
    # id = models.IntegerField(primary_key=True)  # AutoField?
919
    name = models.CharField(max_length=255)
920
921
    address = models.CharField(
922
        max_length=255, blank=True, null=True,
923
        help_text='One line, comma separated')
924
925
    country = models.ForeignKey(
926
        'DictCountry',
927
        on_delete=models.PROTECT)
928
929
    URI = models.URLField(
930
        max_length=500, blank=True, null=True,
931
        help_text='Web site')
932
933
    role = models.ForeignKey(
934
        'DictRole',
935
        on_delete=models.PROTECT,
936
        help_text="The organization role, for example 'submitter'")
937
938
    def __str__(self):
939
        return "%s (%s)" % (self.name, self.country.label)
940
941
    class Meta:
942
        ordering = ['name', 'country']
943
944
945
class Publication(BaseMixin, models.Model):
946
    # this is a non mandatory fields in ruleset
947
    doi = models.CharField(
948
        max_length=255,
949
        help_text='Valid Digital Object Identifier')
950
951
    def __str__(self):
952
        return self.doi
953
954
955
class Ontology(BaseMixin, models.Model):
956
    library_name = models.CharField(
957
        max_length=255,
958
        help_text='Each value must be unique',
959
        unique=True)
960
961
    library_uri = models.URLField(
962
        max_length=500, blank=True, null=True,
963
        help_text='Each value must be unique ' +
964
                  'and with a valid URL')
965
966
    comment = models.CharField(
967
            max_length=255, blank=True, null=True)
968
969
    def __str__(self):
970
        return self.library_name
971
972
    class Meta:
973
        verbose_name_plural = "ontologies"
974
975
976
class Submission(BaseMixin, models.Model):
977
    title = models.CharField(
978
        "Submission title",
979
        max_length=255,
980
        help_text='Example: Roslin Sheep Atlas')
981
982
    project = models.CharField(
983
        max_length=25,
984
        default="IMAGE",
985
        editable=False)
986
987
    description = models.CharField(
988
        max_length=255,
989
        help_text='Example: The Roslin Institute ' +
990
                  'Sheep Gene Expression Atlas Project')
991
992
    # gene bank fields
993
    gene_bank_name = models.CharField(
994
        max_length=255,
995
        blank=False,
996
        null=False,
997
        help_text='example: CryoWeb')
998
999
    gene_bank_country = models.ForeignKey(
1000
        'DictCountry',
1001
        on_delete=models.PROTECT)
1002
1003
    # datasource field
1004
    datasource_type = models.SmallIntegerField(
1005
        "Data source type",
1006
        choices=[x.value for x in DATA_TYPES],
1007
        help_text='example: CryoWeb')
1008
1009
    datasource_version = models.CharField(
1010
        "Data source version",
1011
        max_length=255,
1012
        blank=False,
1013
        null=False,
1014
        help_text='examples: "2018-04-27", "version 1.5"')
1015
1016
    organization = models.ForeignKey(
1017
        'Organization',
1018
        on_delete=models.PROTECT,
1019
        help_text="Who owns the data")
1020
1021
    # custom fields for datasource
1022
    upload_dir = 'data_source/'
1023
1024
    # File will be stored to PROTECTED_MEDIA_ROOT + upload_to
1025
    # https://gist.github.com/cobusc/ea1d01611ef05dacb0f33307e292abf4
1026
    uploaded_file = ProtectedFileField(upload_to=upload_dir)
1027
1028
    # when submission is created
1029
    created_at = models.DateTimeField(auto_now_add=True)
1030
1031
    # https://simpleisbetterthancomplex.com/tips/2016/05/23/django-tip-4-automatic-datetime-fields.html
1032
    updated_at = models.DateTimeField(auto_now=True)
1033
1034
    # a column to track submission status
1035
    status = models.SmallIntegerField(
1036
            choices=[x.value for x in STATUSES],
1037
            help_text='example: Waiting',
1038
            default=WAITING)
1039
1040
    # a field to track errors in UID loading. Should be blank if no errors
1041
    # are found
1042
    message = models.TextField(
1043
        null=True,
1044
        blank=True)
1045
1046
    owner = models.ForeignKey(
1047
        User,
1048
        on_delete=models.CASCADE)
1049
1050
    class Meta:
1051
        # HINT: can I put two files for my cryoweb instance? May they have two
1052
        # different version
1053
        unique_together = ((
1054
            "gene_bank_name",
1055
            "gene_bank_country",
1056
            "datasource_type",
1057
            "datasource_version",
1058
            "owner"),)
1059
1060
    def __str__(self):
1061
        return "%s (%s, %s)" % (
1062
            self.gene_bank_name,
1063
            self.gene_bank_country.label,
1064
            self.datasource_version
1065
        )
1066
1067
    def get_uploaded_file_basename(self):
1068
        return os.path.basename(self.uploaded_file.name)
1069
1070
    def get_uploaded_file_path(self):
1071
        """Return uploaded file path in docker container"""
1072
1073
        # this is the full path in docker container
1074
        fullpath = self.uploaded_file.file
1075
1076
        # get a string and quote fullpath
1077
        return shlex.quote(str(fullpath))
1078
1079
    def get_absolute_url(self):
1080
        return reverse("submissions:detail", kwargs={"pk": self.pk})
1081
1082
    def __status_not_in(self, statuses):
1083
        """Return True id self.status not in statuses"""
1084
1085
        statuses = [x.value[0] for x in STATUSES if x.name in statuses]
1086
1087
        if self.status not in statuses:
1088
            return True
1089
1090
        else:
1091
            return False
1092
1093
    def can_edit(self):
1094
        """Returns True if I can edit a submission"""
1095
1096
        # if there are no data I can't edit and do stuff
1097
        if self.animal_set.count() == 0 and self.sample_set.count() == 0:
1098
            return False
1099
1100
        # if I have data, apply the same condition of can_delete
1101
        return self.can_delete()
1102
1103
    def can_delete(self):
1104
        """return True if I can delete the submission"""
1105
1106
        statuses = ['waiting', 'submitted']
1107
1108
        return self.__status_not_in(statuses)
1109
1110
    def can_validate(self):
1111
        statuses = ['error', 'waiting', 'submitted', 'completed', 'ready']
1112
1113
        return self.__status_not_in(statuses)
1114
1115
    def can_submit(self):
1116
        """Return yes if I can submit a submission to BioSamples"""
1117
1118
        # I can submit only with READY status
1119
        if self.status == READY:
1120
            return True
1121
1122
        else:
1123
            return False
1124
1125
1126
# --- Custom functions
1127
1128
1129
# https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html#onetoone
1130
# we will now define signals so our Person model will be automatically
1131
# created/updated when we create/update User instances.
1132
# Basically we are hooking the create_user_person and save_user_person
1133
# methods to the User model, whenever a save event occurs. This kind of signal
1134
# is called post_save.
1135
# TODO: add default values when creating a superuser
1136
@receiver(post_save, sender=User)
1137
def create_user_person(sender, instance, created, **kwargs):
1138
    if created:
1139
        Person.objects.create(user=instance)
1140
1141
1142
@receiver(post_save, sender=User)
1143
def save_user_person(sender, instance, **kwargs):
1144
    instance.person.save()
1145
1146
1147
# A method to truncate database
1148
def truncate_database():
1149
    """Truncate image database"""
1150
1151
    logger.warning("Truncating ALL image tables")
1152
1153
    # call each class and truncate its table by calling truncate method
1154
    Animal.truncate()
1155
    DictBreed.truncate()
1156
    DictCountry.truncate()
1157
    DictRole.truncate()
1158
    DictSex.truncate()
1159
    DictSpecie.truncate()
1160
    DictUberon.truncate()
1161
    DictDevelStage.truncate()
1162
    DictPhysioStage.truncate()
1163
    Ontology.truncate()
1164
    Organization.truncate()
1165
    Person.truncate()
1166
    Publication.truncate()
1167
    Sample.truncate()
1168
    Submission.truncate()
1169
1170
    logger.warning("All cryoweb tables were truncated")
1171
1172
1173
def truncate_filled_tables():
1174
    """Truncate filled tables by import processes"""
1175
1176
    logger.warning("Truncating filled tables tables")
1177
1178
    # call each class and truncate its table by calling truncate method
1179
    Animal.truncate()
1180
    Publication.truncate()
1181
    Sample.truncate()
1182
    Submission.truncate()
1183
1184
    logger.warning("All filled tables were truncated")
1185
1186
1187
def uid_report(user):
1188
    """Performs a statistic on UID database to find issues. require user as
1189
    request.user"""
1190
1191
    report = {}
1192
1193
    # get n_of_animals
1194
    report['n_of_animals'] = Animal.objects.filter(
1195
        owner=user).count()
1196
1197
    # get n_of_samples
1198
    report['n_of_samples'] = Sample.objects.filter(
1199
        owner=user).count()
1200
1201
    # merging dictionaries: https://stackoverflow.com/a/26853961
1202
    # HINT: have they sense in a per user statistic?
1203
    report = {**report, **missing_terms()}
1204
1205
    return report
1206
1207
1208
def missing_terms():
1209
    """Get informations about dictionary terms without ontologies"""
1210
1211
    # a list with all dictionary classes
1212
    dict_classes = [
1213
        DictBreed, DictCountry, DictSpecie, DictUberon, DictDevelStage,
1214
        DictPhysioStage
1215
    ]
1216
1217
    # get a dictionary to report data
1218
    report = {}
1219
1220
    for dict_class in dict_classes:
1221
        # get a queryset with missing terms
1222
        missing = dict_class.objects.filter(term=None)
1223
1224
        # set a key for report dictionary
1225
        key = "%s_without_ontology" % (
1226
            dict_class._meta.verbose_name_plural.replace(" ", "_"))
1227
1228
        # track counts
1229
        report[key] = missing.count()
1230
1231
        # add the total value
1232
        total = dict_class.objects.count()
1233
1234
        # set a key for report dictionary
1235
        key = "%s_total" % (
1236
            dict_class._meta.verbose_name_plural.replace(" ", "_"))
1237
1238
        # track counts
1239
        report[key] = total
1240
1241
    return report
1242
1243
1244
# A method to discover is image database has data or not
1245
def db_has_data():
1246
    # Test only tables I read data to fill UID
1247
    if (Submission.objects.exists() and
1248
            Animal.objects.exists() and
1249
            Sample.objects.exists()):
1250
        return True
1251
1252
    else:
1253
        return False
1254