Passed
Pull Request — master (#72)
by Paolo
16:34
created

uid.models.Sample.get_attributes()   B

Complexity

Conditions 4

Size

Total Lines 68
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 42
dl 0
loc 68
rs 8.872
c 0
b 0
f 0
cc 4
nop 1

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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} ({mapped}, {specie})".format(
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
        null=True,
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
    # TODO: need to set this value? How?
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):
629
        """Get a relationship to this animal (call this method from a related
630
        object to get a connection to this element)"""
631
632
        # if animal is already uploaded I will use accession as
633
        # relationship key. This biosample id could be tested in validation
634
        if self.biosample_id and self.biosample_id != '':
635
            return {
636
                "accession": self.biosample_id,
637
                "relationshipNature": "derived from",
638
            }
639
        else:
640
            return {
641
                "alias": self.biosample_alias,
642
                "relationshipNature": "derived from",
643
            }
644
645
    def get_father_relationship(self):
646
        """Get a relationship with father if possible"""
647
648
        # get father of this animal
649
        if self.father is None:
650
            return None
651
652
        return self.father.get_relationship()
653
654
    def get_mother_relationship(self):
655
        """Get a relationship with mother if possible"""
656
657
        # get mother of this animal
658
        if self.mother is None:
659
            return None
660
661
        return self.mother.get_relationship()
662
663
    def to_biosample(self, release_date=None):
664
        """get a json from animal for biosample submission"""
665
666
        # call methods defined in BioSampleMixin and get result
667
        # with USI mandatory keys and attributes
668
        result = super().to_biosample(release_date)
669
670
        # define relationship with mother and father (if possible)
671
        result['sampleRelationships'] = []
672
673
        father_relationship = self.get_father_relationship()
674
675
        if father_relationship is not None:
676
            result['sampleRelationships'].append(father_relationship)
677
678
        mother_relationship = self.get_mother_relationship()
679
680
        if mother_relationship is not None:
681
            result['sampleRelationships'].append(mother_relationship)
682
683
        return result
684
685
    def get_absolute_url(self):
686
        return reverse("animals:detail", kwargs={"pk": self.pk})
687
688
689
class Sample(BioSampleMixin, Name):
690
    material = models.CharField(
691
        max_length=255,
692
        default="Specimen from Organism",
693
        editable=False)
694
695
    animal = models.ForeignKey(
696
        'Animal',
697
        on_delete=models.CASCADE)
698
699
    # HINT: should this be a protocol?
700
    protocol = models.CharField(
701
        max_length=255,
702
        blank=True,
703
        null=True)
704
705
    collection_date = models.DateField(
706
        blank=True,
707
        null=True,
708
        help_text='example: 2019-04-01')
709
710
    collection_place_latitude = models.FloatField(blank=True, null=True)
711
    collection_place_longitude = models.FloatField(blank=True, null=True)
712
    collection_place = models.CharField(max_length=255, blank=True, null=True)
713
714
    # accuracy field (enum)
715
    collection_place_accuracy = models.SmallIntegerField(
716
        choices=[x.value for x in ACCURACIES],
717
        help_text='example: unknown accuracy level, country level',
718
        null=False,
719
        blank=False,
720
        default=MISSING)
721
722
    # using a constraint for organism (DictUberon)
723
    organism_part = models.ForeignKey(
724
        'DictUberon',
725
        null=True,
726
        on_delete=models.PROTECT)
727
728
    # using a constraint for developmental stage (DictDevelStage)
729
    developmental_stage = models.ForeignKey(
730
        'DictDevelStage',
731
        null=True,
732
        blank=True,
733
        on_delete=models.PROTECT)
734
735
    physiological_stage = models.ForeignKey(
736
        'DictPhysioStage',
737
        null=True,
738
        blank=True,
739
        on_delete=models.PROTECT)
740
741
    animal_age_at_collection = models.IntegerField(
742
        null=True,
743
        blank=True)
744
745
    animal_age_at_collection_units = models.SmallIntegerField(
746
        choices=[x.value for x in TIME_UNITS],
747
        help_text='example: years',
748
        null=True,
749
        blank=True)
750
751
    availability = models.CharField(
752
        max_length=255,
753
        blank=True,
754
        null=True,
755
        help_text=(
756
            "Either a link to a web page giving information on who to contact "
757
            "or an e-mail address to contact about availability. If neither "
758
            "available, please use the value no longer available")
759
    )
760
761
    storage = models.SmallIntegerField(
762
        choices=[x.value for x in SAMPLE_STORAGE],
763
        help_text='How the sample was stored',
764
        null=True,
765
        blank=True)
766
767
    storage_processing = models.SmallIntegerField(
768
        choices=[x.value for x in SAMPLE_STORAGE_PROCESSING],
769
        help_text='How the sample was prepared for storage',
770
        null=True,
771
        blank=True)
772
773
    preparation_interval = models.IntegerField(
774
        blank=True,
775
        null=True)
776
777
    preparation_interval_units = models.SmallIntegerField(
778
        choices=[x.value for x in TIME_UNITS],
779
        help_text='example: years',
780
        null=True,
781
        blank=True)
782
783
    class Meta:
784
        unique_together = (("name", "animal", "owner"),)
785
786
    @property
787
    def specie(self):
788
        return self.animal.breed.specie
789
790
    @property
791
    def biosample_alias(self):
792
        return 'IMAGES{0:09d}'.format(self.id)
793
794
    def get_attributes(self):
795
        """Return attributes like biosample needs"""
796
797
        attributes = super().get_attributes()
798
799
        attributes["Material"] = format_attribute(
800
            value="specimen from organism", terms="OBI_0001479")
801
802
        # The data source id or alternative id of the animal from which
803
        # the sample was collected (see Animal.to_biosample())
804
        attributes['Derived from'] = format_attribute(
805
            value=self.animal.name)
806
807
        attributes["Specimen collection protocol"] = format_attribute(
808
            value=self.protocol)
809
810
        # a datetime object should be not be converted in string here,
811
        # otherwise will not be filtered if NULL
812
        attributes['Collection date'] = format_attribute(
813
            value=self.collection_date, units="YYYY-MM-DD")
814
815
        attributes['Collection place'] = format_attribute(
816
            value=self.collection_place)
817
818
        attributes["Collection place longitude"] = format_attribute(
819
            value=self.collection_place_longitude,
820
            units="decimal degrees")
821
822
        attributes["Collection place latitude"] = format_attribute(
823
            value=self.collection_place_latitude,
824
            units="decimal degrees")
825
826
        attributes["Collection place accuracy"] = format_attribute(
827
            value=self.get_collection_place_accuracy_display())
828
829
        # this will point to a correct term dictionary table
830
        if self.organism_part:
831
            attributes['Organism part'] = self.organism_part.format_attribute()
832
833
        if self.developmental_stage:
834
            attributes['Developmental stage'] = \
835
                self.developmental_stage.format_attribute()
836
837
        if self.physiological_stage:
838
            attributes['Physiological stage'] = \
839
                self.physiological_stage.format_attribute()
840
841
        attributes['Animal age at collection'] = format_attribute(
842
            value=self.animal_age_at_collection,
843
            units=self.get_animal_age_at_collection_units_display())
844
845
        attributes['Availability'] = format_attribute(
846
            value=self.availability)
847
848
        attributes['Sample storage'] = format_attribute(
849
            value=self.get_storage_display())
850
851
        attributes['Sample storage processing'] = format_attribute(
852
            value=self.get_storage_processing_display())
853
854
        attributes['Sampling to preparation interval'] = format_attribute(
855
            value=self.preparation_interval,
856
            units=self.get_preparation_interval_units_display())
857
858
        # filter out empty values
859
        attributes = {k: v for k, v in attributes.items() if v is not None}
860
861
        return attributes
862
863
    def to_biosample(self, release_date=None):
864
        """get a json from sample for biosample submission"""
865
866
        # call methods defined in BioSampleMixin and get result
867
        # with USI mandatory keys and attributes
868
        result = super().to_biosample(release_date)
869
870
        # define relationship to the animal where this sample come from
871
        result['sampleRelationships'] = [self.animal.get_relationship()]
872
873
        return result
874
875
    def get_absolute_url(self):
876
        return reverse("samples:detail", kwargs={"pk": self.pk})
877
878
879
class Person(BaseMixin, models.Model):
880
    user = models.OneToOneField(
881
        User,
882
        on_delete=models.CASCADE,
883
        related_name='person')
884
885
    initials = models.CharField(max_length=255, blank=True, null=True)
886
887
    # HINT: with a OneToOneField relation, there will be only one user for
888
    # each organization
889
    affiliation = models.ForeignKey(
890
        'Organization',
891
        null=True,
892
        on_delete=models.PROTECT,
893
        help_text="The institution you belong to")
894
895
    # last_name, first_name and email come from User model
896
897
    role = models.ForeignKey(
898
        'DictRole',
899
        on_delete=models.PROTECT,
900
        null=True)
901
902
    def __str__(self):
903
        return "{name} {surname} ({affiliation})".format(
904
                name=self.user.first_name,
905
                surname=self.user.last_name,
906
                affiliation=self.affiliation)
907
908
909
class Organization(BaseMixin, models.Model):
910
    # id = models.IntegerField(primary_key=True)  # AutoField?
911
    name = models.CharField(max_length=255)
912
    address = models.CharField(
913
        max_length=255, blank=True, null=True,
914
        help_text='One line, comma separated')
915
916
    country = models.ForeignKey(
917
        'DictCountry',
918
        on_delete=models.PROTECT)
919
920
    URI = models.URLField(
921
        max_length=500, blank=True, null=True,
922
        help_text='Web site')
923
924
    role = models.ForeignKey(
925
        'DictRole',
926
        on_delete=models.PROTECT)
927
928
    def __str__(self):
929
        return "%s (%s)" % (self.name, self.country.label)
930
931
    class Meta:
932
        ordering = ['name', 'country']
933
934
935
class Publication(BaseMixin, models.Model):
936
    # this is a non mandatory fields in ruleset
937
    doi = models.CharField(
938
        max_length=255,
939
        help_text='Valid Digital Object Identifier')
940
941
    def __str__(self):
942
        return self.doi
943
944
945
class Ontology(BaseMixin, models.Model):
946
    library_name = models.CharField(
947
        max_length=255,
948
        help_text='Each value must be unique',
949
        unique=True)
950
951
    library_uri = models.URLField(
952
        max_length=500, blank=True, null=True,
953
        help_text='Each value must be unique ' +
954
                  'and with a valid URL')
955
956
    comment = models.CharField(
957
            max_length=255, blank=True, null=True)
958
959
    def __str__(self):
960
        return self.library_name
961
962
    class Meta:
963
        verbose_name_plural = "ontologies"
964
965
966
class Submission(BaseMixin, models.Model):
967
    title = models.CharField(
968
        "Submission title",
969
        max_length=255,
970
        help_text='Example: Roslin Sheep Atlas')
971
972
    project = models.CharField(
973
        max_length=25,
974
        default="IMAGE",
975
        editable=False)
976
977
    description = models.CharField(
978
        max_length=255,
979
        help_text='Example: The Roslin Institute ' +
980
                  'Sheep Gene Expression Atlas Project')
981
982
    # gene bank fields
983
    gene_bank_name = models.CharField(
984
        max_length=255,
985
        blank=False,
986
        null=False,
987
        help_text='example: CryoWeb')
988
989
    gene_bank_country = models.ForeignKey(
990
        'DictCountry',
991
        on_delete=models.PROTECT)
992
993
    # datasource field
994
    datasource_type = models.SmallIntegerField(
995
        "Data source type",
996
        choices=[x.value for x in DATA_TYPES],
997
        help_text='example: CryoWeb')
998
999
    datasource_version = models.CharField(
1000
        "Data source version",
1001
        max_length=255,
1002
        blank=False,
1003
        null=False,
1004
        help_text='examples: "2018-04-27", "version 1.5"')
1005
1006
    organization = models.ForeignKey(
1007
        'Organization',
1008
        on_delete=models.PROTECT,
1009
        help_text="Who owns the data")
1010
1011
    # custom fields for datasource
1012
    upload_dir = 'data_source/'
1013
1014
    # File will be stored to PROTECTED_MEDIA_ROOT + upload_to
1015
    # https://gist.github.com/cobusc/ea1d01611ef05dacb0f33307e292abf4
1016
    uploaded_file = ProtectedFileField(upload_to=upload_dir)
1017
1018
    # when submission is created
1019
    created_at = models.DateTimeField(auto_now_add=True)
1020
1021
    # https://simpleisbetterthancomplex.com/tips/2016/05/23/django-tip-4-automatic-datetime-fields.html
1022
    updated_at = models.DateTimeField(auto_now=True)
1023
1024
    # a column to track submission status
1025
    status = models.SmallIntegerField(
1026
            choices=[x.value for x in STATUSES],
1027
            help_text='example: Waiting',
1028
            default=WAITING)
1029
1030
    # a field to track errors in UID loading. Should be blank if no errors
1031
    # are found
1032
    message = models.TextField(
1033
        null=True,
1034
        blank=True)
1035
1036
    owner = models.ForeignKey(
1037
        User,
1038
        on_delete=models.CASCADE)
1039
1040
    class Meta:
1041
        # HINT: can I put two files for my cryoweb instance? May they have two
1042
        # different version
1043
        unique_together = ((
1044
            "gene_bank_name",
1045
            "gene_bank_country",
1046
            "datasource_type",
1047
            "datasource_version",
1048
            "owner"),)
1049
1050
    def __str__(self):
1051
        return "%s (%s, %s)" % (
1052
            self.gene_bank_name,
1053
            self.gene_bank_country.label,
1054
            self.datasource_version
1055
        )
1056
1057
    def get_uploaded_file_basename(self):
1058
        return os.path.basename(self.uploaded_file.name)
1059
1060
    def get_uploaded_file_path(self):
1061
        """Return uploaded file path in docker container"""
1062
1063
        # this is the full path in docker container
1064
        fullpath = self.uploaded_file.file
1065
1066
        # get a string and quote fullpath
1067
        return shlex.quote(str(fullpath))
1068
1069
    def get_absolute_url(self):
1070
        return reverse("submissions:detail", kwargs={"pk": self.pk})
1071
1072
    def __can_I(self, names):
1073
        """Return True id self.status in statuses"""
1074
1075
        statuses = [x.value[0] for x in STATUSES if x.name in names]
1076
1077
        if self.status not in statuses:
1078
            return True
1079
1080
        else:
1081
            return False
1082
1083
    def can_edit(self):
1084
        """Returns True if I can edit a submission"""
1085
1086
        names = ['waiting', 'submitted']
1087
1088
        return self.__can_I(names)
1089
1090
    def can_validate(self):
1091
        names = ['error', 'waiting', 'submitted', 'completed']
1092
1093
        return self.__can_I(names)
1094
1095
    def can_submit(self):
1096
        names = ['ready']
1097
1098
        # this is the opposite of self.__can_I
1099
        statuses = [x.value[0] for x in STATUSES if x.name in names]
1100
1101
        # self.status need to be in statuses for submitting
1102
        if self.status in statuses:
1103
            return True
1104
1105
        else:
1106
            return False
1107
1108
1109
# --- Custom functions
1110
1111
1112
# https://simpleisbetterthancomplex.com/tutorial/2016/07/22/how-to-extend-django-user-model.html#onetoone
1113
# we will now define signals so our Person model will be automatically
1114
# created/updated when we create/update User instances.
1115
# Basically we are hooking the create_user_person and save_user_person
1116
# methods to the User model, whenever a save event occurs. This kind of signal
1117
# is called post_save.
1118
# TODO: add default values when creating a superuser
1119
@receiver(post_save, sender=User)
1120
def create_user_person(sender, instance, created, **kwargs):
1121
    if created:
1122
        Person.objects.create(user=instance)
1123
1124
1125
@receiver(post_save, sender=User)
1126
def save_user_person(sender, instance, **kwargs):
1127
    instance.person.save()
1128
1129
1130
# A method to truncate database
1131
def truncate_database():
1132
    """Truncate image database"""
1133
1134
    logger.warning("Truncating ALL image tables")
1135
1136
    # call each class and truncate its table by calling truncate method
1137
    Animal.truncate()
1138
    DictBreed.truncate()
1139
    DictCountry.truncate()
1140
    DictRole.truncate()
1141
    DictSex.truncate()
1142
    DictSpecie.truncate()
1143
    DictUberon.truncate()
1144
    DictDevelStage.truncate()
1145
    DictPhysioStage.truncate()
1146
    Ontology.truncate()
1147
    Organization.truncate()
1148
    Person.truncate()
1149
    Publication.truncate()
1150
    Sample.truncate()
1151
    Submission.truncate()
1152
1153
    logger.warning("All cryoweb tables were truncated")
1154
1155
1156
def truncate_filled_tables():
1157
    """Truncate filled tables by import processes"""
1158
1159
    logger.warning("Truncating filled tables tables")
1160
1161
    # call each class and truncate its table by calling truncate method
1162
    Animal.truncate()
1163
    Publication.truncate()
1164
    Sample.truncate()
1165
    Submission.truncate()
1166
1167
    logger.warning("All filled tables were truncated")
1168
1169
1170
def uid_report(user):
1171
    """Performs a statistic on UID database to find issues. require user as
1172
    request.user"""
1173
1174
    report = {}
1175
1176
    # get n_of_animals
1177
    report['n_of_animals'] = Animal.objects.filter(
1178
        owner=user).count()
1179
1180
    # get n_of_samples
1181
    report['n_of_samples'] = Sample.objects.filter(
1182
        owner=user).count()
1183
1184
    # merging dictionaries: https://stackoverflow.com/a/26853961
1185
    # HINT: have they sense in a per user statistic?
1186
    report = {**report, **missing_terms()}
1187
1188
    return report
1189
1190
1191
def missing_terms():
1192
    """Get informations about dictionary terms without ontologies"""
1193
1194
    # a list with all dictionary classes
1195
    dict_classes = [
1196
        DictBreed, DictCountry, DictSpecie, DictUberon, DictDevelStage,
1197
        DictPhysioStage
1198
    ]
1199
1200
    # get a dictionary to report data
1201
    report = {}
1202
1203
    for dict_class in dict_classes:
1204
        # get a queryset with missing terms
1205
        missing = dict_class.objects.filter(term=None)
1206
1207
        # set a key for report dictionary
1208
        key = "%s_without_ontology" % (
1209
            dict_class._meta.verbose_name_plural.replace(" ", "_"))
1210
1211
        # track counts
1212
        report[key] = missing.count()
1213
1214
        # add the total value
1215
        total = dict_class.objects.count()
1216
1217
        # set a key for report dictionary
1218
        key = "%s_total" % (
1219
            dict_class._meta.verbose_name_plural.replace(" ", "_"))
1220
1221
        # track counts
1222
        report[key] = total
1223
1224
    return report
1225
1226
1227
# A method to discover is image database has data or not
1228
def db_has_data():
1229
    # Test only tables I read data to fill UID
1230
    if Submission.objects.exists():
1231
        return True
1232
1233
    else:
1234
        return False
1235