Completed
Push — master ( 8a2447...e2760f )
by Paolo
06:42
created

uid.models.Submission.can_delete()   A

Complexity

Conditions 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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