Completed
Push — master ( 5644bc...b87258 )
by Paolo
17s queued 13s
created

image_app.models.BioSampleMixin.specie()   A

Complexity

Conditions 1

Size

Total Lines 4
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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