Completed
Pull Request — devel (#72)
by Paolo
07:01
created

uid.models   F

Complexity

Total Complexity 80

Size/Duplication

Total Lines 1238
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 80
eloc 694
dl 0
loc 1238
rs 1.906
c 0
b 0
f 0

35 Methods

Rating   Name   Duplication   Size   Complexity  
A Name.validationresult() 0 8 2
A DictSpecie.get_by_synonym() 0 26 2
A DictSpecie.taxon_id() 0 6 3
A DictBase.__str__() 0 4 1
A DictSpecie.get_specie_check_synonyms() 0 17 2
A DictBase.format_attribute() 0 26 2
A Submission.__str__() 0 5 1
A Submission.can_validate() 0 4 1
B Sample.get_attributes() 0 68 4
A Animal.get_father_relationship() 0 8 2
A DictBreed.__str__() 0 6 1
A Submission.can_submit() 0 12 2
A Ontology.__str__() 0 2 1
A Organization.__str__() 0 2 1
A Submission.get_uploaded_file_path() 0 8 1
A DictBreed.mapped_breed_term() 0 13 1
A Submission.__can_I() 0 10 2
A Sample.get_absolute_url() 0 2 1
A Submission.get_uploaded_file_basename() 0 2 1
A Person.__str__() 0 5 1
A Animal.biosample_alias() 0 3 1
A Animal.get_attributes() 0 44 1
A Publication.__str__() 0 2 1
A Animal.specie() 0 3 1
A Animal.get_mother_relationship() 0 8 2
A DictBreed.mapped_breed() 0 14 1
A Animal.get_absolute_url() 0 2 1
A Sample.specie() 0 3 1
A Animal.get_relationship() 0 15 3
A DictBreed.format_attribute() 0 14 3
A Animal.to_biosample() 0 21 3
A Submission.get_absolute_url() 0 2 1
A Submission.can_edit() 0 6 1
A Sample.to_biosample() 0 11 1
A Sample.biosample_alias() 0 3 1

7 Functions

Rating   Name   Duplication   Size   Complexity  
A truncate_filled_tables() 0 12 1
A db_has_data() 0 9 4
A save_user_person() 0 3 1
A truncate_database() 0 23 1
A uid_report() 0 19 1
A missing_terms() 0 34 2
A create_user_person() 0 4 2

How to fix   Complexity   

Complexity

Complex classes like uid.models often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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