Completed
Pull Request — master (#51)
by Paolo
07:25
created

image_app.models.missing_terms()   A

Complexity

Conditions 2

Size

Total Lines 34
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

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