models.py 63.1 KB
Newer Older
1
2
3
4
"""
Models used in iPPI-DB
"""

Hervé  MENAGER's avatar
Hervé MENAGER committed
5
from __future__ import unicode_literals
6

7
import operator
8
import re
9
import sys
Hervé  MENAGER's avatar
Hervé MENAGER committed
10

11
from django.conf import settings
12
from django.contrib.auth import get_user_model
13
from django.core.exceptions import ValidationError
14
from django.db import models, transaction
15
from django.db.models import FloatField, IntegerField, BooleanField
16
from django.db.models import Max, Count, F, Q, Case, When, Subquery, OuterRef
17
from django.db.models.functions import Cast
18
from django.urls import reverse
19
from django.utils.translation import ugettext_lazy as _
20
21
22
from django_celery_results.models import TaskResult
from django.dispatch import receiver
from django.db.models.signals import post_save
23
from polymorphic.models import PolymorphicModel
Hervé  MENAGER's avatar
Hervé MENAGER committed
24

25
from .utils import FingerPrinter, smi2inchi, smi2inchikey
26
27
28
29
30
31
32
33
from .ws import (
    get_pubmed_info,
    get_google_patent_info,
    get_uniprot_info,
    get_taxonomy_info,
    get_go_info,
    get_pfam_info,
    get_doi_info,
34
35
    get_chembl_id,
    get_pubchem_id,
36
    get_ligand_id,
37
    EntryNotFoundError,
38
)
39

Hervé  MENAGER's avatar
Hervé MENAGER committed
40

41
42
class AutoFillableModel(models.Model):
    """
43
44
    AutoFillableModel abstract model to enable automated model fields
    filling from external sources in the autofill() method.
45
46
    The save method allows to either include autofill or not. in autofill kwarg
    is set to True, save() will first call autofill(), otherwise it won't
47
48
49
50
51
52
    """

    class Meta:
        abstract = True

    def save(self, *args, **kwargs):
53
        auto_fill_needed = not self.is_autofill_done()
54
        if kwargs.get("autofill") is True or auto_fill_needed:
55
            auto_fill_needed = True
56
            self.autofill()
57
58
        if "autofill" in kwargs:
            del kwargs["autofill"]
Hervé  MENAGER's avatar
Hervé MENAGER committed
59
        super(AutoFillableModel, self).save(*args, **kwargs)
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
60
        if auto_fill_needed:
61
            self.autofill_post_save()
62

63
64
65
    def autofill(self):
        raise NotImplementedError()

66
67
    def autofill_post_save(self):
        """
68
69
        Method called automatically after the save is done,
        usefull for setting m2m relations
70
71
72
        """
        pass

73
    def is_autofill_done(self):
74
75
76
        """
        test whether or not the model has been already autofilled
        """
77
78
        return True

79
80

class Bibliography(AutoFillableModel):
Hervé  MENAGER's avatar
Hervé MENAGER committed
81
    """
82
    Bibliography reference
Hervé  MENAGER's avatar
Hervé MENAGER committed
83
    (publications or patents)
Hervé  MENAGER's avatar
Hervé MENAGER committed
84
    """
85
86

    SOURCES = (("PM", "PubMed ID"), ("PT", "Patent"), ("DO", "DOI"))
87
    id_source_validators = dict(
88
89
90
        PM=re.compile(r"^[0-9]+$"),
        PT=re.compile(r"^.*$"),
        DO=re.compile(r"^10.\d{4,9}/.+$"),
91
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
92
    source = models.CharField(
93
94
        "Bibliographic type", max_length=2, choices=SOURCES, default=SOURCES[0][0]
    )
95
    id_source = models.TextField("Bibliographic ID")
96
97
98
99
100
101
102
103
104
105
106
    title = models.TextField("Title")
    journal_name = models.TextField("Journal name", null=True, blank=True)
    authors_list = models.TextField("Authors list")
    biblio_year = models.PositiveSmallIntegerField("Year")
    cytotox = models.BooleanField("Cytotoxicity data", default=False)
    in_silico = models.BooleanField("in silico study", default=False)
    in_vitro = models.BooleanField("in vitro study", default=False)
    in_vivo = models.BooleanField("in vivo study", default=False)
    in_cellulo = models.BooleanField("in cellulo study", default=False)
    pharmacokinetic = models.BooleanField("pharmacokinetic study", default=False)
    xray = models.BooleanField("X-Ray data", default=False)
Hervé  MENAGER's avatar
Hervé MENAGER committed
107

108
    def autofill(self):
Hervé  MENAGER's avatar
Hervé MENAGER committed
109
110
111
112
        """
        fetch information from external services
        (Pubmed or Google patents)
        """
113
        if self.source == "PM":
114
            info = get_pubmed_info(self.id_source)
115
        elif self.source == "PT":
116
            info = get_google_patent_info(self.id_source)
117
        elif self.source == "DO":
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
118
119
120
            info = get_doi_info(self.id_source)
        else:
            raise NotImplementedError()
121
122
123
124
        self.title = info["title"]
        self.journal_name = info["journal_name"]
        self.authors_list = info["authors_list"]
        self.biblio_year = info["biblio_year"]
Hervé  MENAGER's avatar
Hervé MENAGER committed
125

126
127
128
    def is_autofill_done(self):
        return len(self.title) > 0

129
130
    def clean(self):
        super().clean()
131
132
133
        Bibliography.validate_source_id(self.id_source, self.source)

    def has_external_url(self):
134
        return self.source == "PM" or self.source == "DO"
135
136

    def get_external_url(self):
137
138
139
        """
        Get URL to the publication
        """
140
        if self.source == "PM":
141
142
143
144
145
            return f"https://www.ncbi.nlm.nih.gov/pubmed/{self.id_source}"
        elif self.source == "DO":
            return f"https://doi.org/{self.id_source}"
        elif self.source == "PT":
            return f"https://patentscope.wipo.int/search/en/detail.jsf?docId={self.id_source}"
146
147
148
149
150

    @staticmethod
    def validate_source_id(id_source, source):
        id_source_validator = Bibliography.id_source_validators[source]
        if not id_source_validator.match(id_source):
151
152
            raise ValidationError(
                dict(
153
154
                    id_source=_(
                        f"Must match pattern {id_source_validator.pattern}"
155
156
                        " for this selected source"
                    )
157
158
                )
            )
159
        return True
160

Hervé  MENAGER's avatar
Hervé MENAGER committed
161
    class Meta:
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
        verbose_name_plural = "Bibliographies"
        verbose_name = "Bibliography"

    def data_and_study(self):
        ret = []
        for f in [
            "cytotox",
            "xray",
            "in_silico",
            "in_vitro",
            "in_cellulo",
            "in_vivo",
            "pharmacokinetic",
        ]:
            if getattr(self, f, False):
                ret.append(self._meta.get_field(f).verbose_name.title())
        return ", ".join(ret)
Hervé  MENAGER's avatar
Hervé MENAGER committed
179

180
    def __str__(self):
181
        return "{}, {}".format(self.source, self.id_source)
182

183
    def get_absolute_url(self):
184
        return reverse("biblio-view", kwargs={"biblio_pk": self.pk})
185

Hervé  MENAGER's avatar
Hervé MENAGER committed
186

187
class Taxonomy(AutoFillableModel):
Hervé  MENAGER's avatar
Hervé MENAGER committed
188
    """
Hervé  MENAGER's avatar
Hervé MENAGER committed
189
    Taxonomy IDs (from NCBI Taxonomy)
Hervé  MENAGER's avatar
Hervé MENAGER committed
190
191
    and the corresponding human-readable name
    """
192

Hervé  MENAGER's avatar
Hervé MENAGER committed
193
    taxonomy_id = models.DecimalField(
194
195
196
        "NCBI TaxID", unique=True, max_digits=9, decimal_places=0
    )
    name = models.CharField("Organism name", max_length=200)
197

198
    def autofill(self):
Hervé  MENAGER's avatar
Hervé MENAGER committed
199
200
201
202
        """
        fetch information from external services
        (NCBI Entrez)
        """
203
        info = get_taxonomy_info(self.taxonomy_id)
204
        self.name = info["scientific_name"]
205

206
207
208
    def __str__(self):
        return self.name

Hervé  MENAGER's avatar
Hervé MENAGER committed
209
210
    class Meta:
        verbose_name_plural = "taxonomies"
Hervé  MENAGER's avatar
Hervé MENAGER committed
211

Hervé  MENAGER's avatar
Hervé MENAGER committed
212

213
class MolecularFunction(AutoFillableModel):
Hervé  MENAGER's avatar
Hervé MENAGER committed
214
    """
Hervé  MENAGER's avatar
Hervé MENAGER committed
215
    Molecular functions (from Gene Ontology)
Hervé  MENAGER's avatar
Hervé MENAGER committed
216
217
    and the corresponding human-readable description
    """
218
219

    go_id = models.CharField("Gene Ontology ID", unique=True, max_length=10)
Hervé  MENAGER's avatar
Hervé MENAGER committed
220
    # GO term id format: 'GO:0000000'
221
    description = models.CharField("description", max_length=500)
Hervé  MENAGER's avatar
Hervé MENAGER committed
222

223
    def autofill(self):
Hervé  MENAGER's avatar
Hervé MENAGER committed
224
225
226
227
        """
        fetch information from external services
        (EBI OLS)
        """
228
        info = get_go_info(self.go_id)
229
        self.description = info["label"]
230

231
232
233
    def is_autofill_done(self):
        return self.description is not None and len(self.description) > 0

234
235
    @property
    def name(self):
236
        return self.go_id + " " + self.description
237

238
239
240
    def __str__(self):
        return self.description

Hervé  MENAGER's avatar
Hervé MENAGER committed
241

242
class Protein(AutoFillableModel):
Hervé  MENAGER's avatar
Hervé MENAGER committed
243
    """
Hervé  MENAGER's avatar
Hervé MENAGER committed
244
    Protein information (from Uniprot)
Hervé  MENAGER's avatar
Hervé MENAGER committed
245
246
    and the corresponding human-readable name
    """
247
248

    uniprot_id = models.CharField("Uniprot ID", unique=True, max_length=10)
Hervé  MENAGER's avatar
Hervé MENAGER committed
249
    recommended_name_long = models.CharField(
250
        "Uniprot Recommended Name (long)", max_length=75, blank=True, null=True
251
252
    )
    short_name = models.CharField("Short name", max_length=50)
253
    gene_name = models.CharField("Gene name", max_length=30, blank=True, null=True)
254
255
    entry_name = models.CharField("Entry name", max_length=30)
    organism = models.ForeignKey("Taxonomy", models.CASCADE)
Hervé  MENAGER's avatar
Hervé MENAGER committed
256
    molecular_functions = models.ManyToManyField(MolecularFunction)
257
    domains = models.ManyToManyField("Domain")
Hervé  MENAGER's avatar
Hervé MENAGER committed
258

259
    @transaction.atomic
260
    def autofill(self):
Hervé  MENAGER's avatar
Hervé MENAGER committed
261
262
263
264
        """
        fetch information from external services
        (Uniprot) and create Taxonomy/Molecular Functions if needed
        """
265
        info = get_uniprot_info(self.uniprot_id)
266
        self.recommended_name_long = info["recommended_name"]
267

268
        gene_names = info["gene_names"]
269
        # put whatever name it find
270
271
272
273
        try:
            self.gene_name = gene_names[0]["name"]
        except IndexError:
            pass
274
275
276
277
278
279
        # then try to find the primary, if present
        for gene_name in gene_names:
            if gene_name["type"] == "primary":
                self.gene_name = gene_name["name"]
                break

280
281
        self.entry_name = info["entry_name"]
        self.short_name = info["short_name"]
282
        try:
283
            taxonomy = Taxonomy.objects.get(taxonomy_id=info["organism"])
284
285
        except Taxonomy.DoesNotExist:
            taxonomy = Taxonomy()
286
            taxonomy.taxonomy_id = info["organism"]
287
            taxonomy.save(autofill=True)
288
        self.organism = taxonomy
289
        self.__info = info
290

291
292
    def autofill_post_save(self):
        info = self.__info
293
294
        for go_id in info["molecular_functions"]:
            mol_function, created = MolecularFunction.objects.get_or_create(go_id=go_id)
295
            self.molecular_functions.add(mol_function)
296

297
        for domain_id in info["domains"]:
298
            domain, created = Domain.objects.get_or_create(pfam_acc=domain_id)
299
300
            self.domains.add(domain)

301
    def is_autofill_done(self):
302
        return len(self.entry_name) > 0
303

304
    def __str__(self):
305
306
307
        return "{} ({})".format(
            self.uniprot_id, self.recommended_name_long or self.short_name
        )
308

Hervé  MENAGER's avatar
Hervé MENAGER committed
309

310
class Domain(AutoFillableModel):
Hervé  MENAGER's avatar
Hervé MENAGER committed
311
    """
Hervé  MENAGER's avatar
Hervé MENAGER committed
312
    Domain (i.e. Protein domain) information (from PFAM)
Hervé  MENAGER's avatar
Hervé MENAGER committed
313
    """
314
315
316
317
318
319
320

    pfam_acc = models.CharField("Pfam Accession", max_length=10, unique=True)
    pfam_id = models.CharField("Pfam Family Identifier", max_length=20)
    pfam_description = models.CharField("Pfam Description", max_length=100)
    domain_family = models.CharField(
        "Domain family", max_length=25, blank=True, default=""
    )
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
321

Hervé  MENAGER's avatar
Hervé MENAGER committed
322
323
    # TODO: what is this field? check database
    # contents
324

325
    def autofill(self):
Hervé  MENAGER's avatar
Hervé MENAGER committed
326
327
328
329
        """
        fetch information from external services
        (PFAM)
        """
330
        info = get_pfam_info(self.pfam_acc)
331
332
        self.pfam_id = info["id"]
        self.pfam_description = info["description"]
Hervé  MENAGER's avatar
Hervé MENAGER committed
333

334
335
336
    def is_autofill_done(self):
        return self.pfam_id is not None and len(self.pfam_id) > 0

337
338
339
340
    @property
    def name(self):
        return self.pfam_id

341
    def __str__(self):
342
        return f"{self.pfam_acc} ({self.pfam_id}-{self.pfam_description})"
343

Hervé  MENAGER's avatar
Hervé MENAGER committed
344

345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
class ProteinDomainComplexGroup(models.Model):
    """
    Protein-Domain association group
    """

    protein = models.ForeignKey("Protein", models.CASCADE)
    domain = models.ForeignKey("Domain", models.CASCADE, null=True, blank=True)

    class Meta:
        verbose_name_plural = "protein domain complex groups"

    def __str__(self):
        return "{} {}-{}".format(
            self.protein.short_name,
            self.protein.uniprot_id,
            self.domain.pfam_id if self.domain else "unknow",
        )

    def name(self):
        return str(self)


367
class ProteinDomainComplex(PolymorphicModel):
Hervé  MENAGER's avatar
Hervé MENAGER committed
368
    """
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
369
    Protein-Domain association
Hervé  MENAGER's avatar
Hervé MENAGER committed
370
    """
371
372
373
374

    protein = models.ForeignKey("Protein", models.CASCADE)
    domain = models.ForeignKey("Domain", models.CASCADE, null=True, blank=True)
    ppc_copy_nb = models.IntegerField("Number of copies of the protein in the complex")
375
376
377
    group = models.ForeignKey(
        "ProteinDomainComplexGroup", models.SET_NULL, null=True, blank=True
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
378

Hervé  MENAGER's avatar
Hervé MENAGER committed
379
380
    class Meta:
        verbose_name_plural = "complexes"
381

382
    def __str__(self):
383
384
385
386
387
        return "{} {}-{} ({})".format(
            self.protein.short_name,
            self.protein.uniprot_id,
            self.domain.pfam_id if self.domain else "unknow",
            self.ppc_copy_nb,
388
        )
389

390
391
    def name(self):
        return self.protein.short_name
Hervé  MENAGER's avatar
Hervé MENAGER committed
392

393

394
395
396
397
398
399
400
401
class ProteinDomainBoundComplexGroup(ProteinDomainComplexGroup):
    """
    Protein-Domain Bound Complexes group
    """

    pass


402
class ProteinDomainBoundComplex(ProteinDomainComplex):
Hervé  MENAGER's avatar
Hervé MENAGER committed
403
404
405
    """
    Protein-Domain association with a "bound complex" role
    """
406
407

    ppp_copy_nb_per_p = models.IntegerField(_("ppp_copy_nb_per_p"))
Hervé  MENAGER's avatar
Hervé MENAGER committed
408

409
410
411
412
    @property
    def complex_type(self):
        return "Bound"

413
414
    class Meta:
        verbose_name_plural = "bound complexes"
Hervé  MENAGER's avatar
Hervé MENAGER committed
415

416
417
418
419
420
421
422
423
424
425
426
427
428
    def set_group(self):
        self.group = ProteinDomainBoundComplexGroup.objects.get_or_create(
            protein=self.protein, domain=self.domain
        )[0]


class ProteinDomainPartnerComplexGroup(ProteinDomainComplexGroup):
    """
    Protein-Domain Bound Complexes group
    """

    pass

Hervé  MENAGER's avatar
Hervé MENAGER committed
429

430
class ProteinDomainPartnerComplex(ProteinDomainComplex):
Hervé  MENAGER's avatar
Hervé MENAGER committed
431
432
433
    """
    Protein-Domain association with a "partner complex" role
    """
Hervé  MENAGER's avatar
Hervé MENAGER committed
434

435
436
437
438
    @property
    def complex_type(self):
        return "Partner"

439
440
    class Meta:
        verbose_name_plural = "partner complexes"
Hervé  MENAGER's avatar
Hervé MENAGER committed
441

442
443
444
445
446
    def set_group(self):
        self.group = ProteinDomainPartnerComplexGroup.objects.get_or_create(
            protein=self.protein, domain=self.domain
        )[0]

Hervé  MENAGER's avatar
Hervé MENAGER committed
447

448
class Symmetry(models.Model):
Hervé  MENAGER's avatar
Hervé MENAGER committed
449
450
451
    """
    Symmetry of a PPI
    """
452
453
454

    code = models.CharField("Symmetry code", max_length=2)
    description = models.CharField("Description", max_length=300)
Hervé  MENAGER's avatar
Hervé MENAGER committed
455

456
457
458
    class Meta:
        verbose_name_plural = "symmetries"

459
    def __str__(self):
460
        return "{} ({})".format(self.code, self.description)
461

462
463

class Disease(models.Model):
464
465
    name = models.CharField("Disease", max_length=256)
    identifier = models.CharField("Identifier", max_length=32, null=True, blank=True)
466
467

    def __str__(self):
468
        return "%s (%s)" % (self.name, self.identifier)
469

Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
470

Hervé  MENAGER's avatar
Hervé MENAGER committed
471
class PpiFamily(models.Model):
Hervé  MENAGER's avatar
Hervé MENAGER committed
472
473
474
    """
    PPI Family
    """
475
476

    name = models.CharField("Name", max_length=30, unique=True)
Hervé  MENAGER's avatar
Hervé MENAGER committed
477

478
479
480
    class Meta:
        verbose_name_plural = "PPI Families"

Hervé  MENAGER's avatar
Hervé MENAGER committed
481
482
    def __str__(self):
        return self.name
Hervé  MENAGER's avatar
Hervé MENAGER committed
483

Hervé  MENAGER's avatar
Hervé MENAGER committed
484

485
class Ppi(AutoFillableModel):
Hervé  MENAGER's avatar
Hervé MENAGER committed
486
487
488
    """
    PPI
    """
489
490

    pdb_id = models.CharField("PDB ID", max_length=4, null=True, blank=True)
Hervé  MENAGER's avatar
Hervé MENAGER committed
491
    pockets_nb = models.IntegerField(
492
493
        "Total number of pockets in the complex", default=1
    )
494
    symmetry = models.ForeignKey(Symmetry, models.CASCADE)
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
495
    diseases = models.ManyToManyField(Disease, blank=True)
496
    family = models.ForeignKey(PpiFamily, models.CASCADE, null=True, blank=True)
497
    name = models.TextField("PPI name")
Hervé  MENAGER's avatar
Hervé MENAGER committed
498

499
    def __str__(self):
500
        return "PPI #{} on {}".format(self.id, self.name)
501

502
    def get_absolute_url(self):
503
        return reverse("ppi-view", kwargs={"ppi_pk": self.pk})
504

Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
505
    def is_autofill_done(self):
506
        return self.name != ""
Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
507

508
509
510
    def autofill(self):
        # name is denormalized and stored in the database to reduce SQL queries in query mode
        self.name = self.compute_name_from_protein_names()
511
512
513
514
515

    def get_ppi_bound_complexes(self):
        """
        return bound ppi complexes belonging to this ppi
        """
516
        return PpiComplex.objects.filter(
517
518
            ppi=self, complex__in=ProteinDomainBoundComplex.objects.all()
        )
519

520
    def compute_name_from_protein_names(self):
Hervé  MENAGER's avatar
Hervé MENAGER committed
521
        all_protein_names = set(
522
523
            [
                ppi_complex.complex.protein.short_name
524
525
526
                for ppi_complex in self.ppicomplex_set.all()
            ]
        )
Hervé  MENAGER's avatar
Hervé MENAGER committed
527
        bound_protein_names = set(
528
529
            [
                ppi_complex.complex.protein.short_name
530
531
532
                for ppi_complex in self.get_ppi_bound_complexes()
            ]
        )
533
        partner_protein_names = all_protein_names - bound_protein_names
534
535
        bound_str = ",".join(sorted(bound_protein_names))
        partner_str = ",".join(sorted(partner_protein_names))
536
        name = bound_str
537
538
        if partner_str != "":
            name += " / " + partner_str
539
        return name
540

Hervé  MENAGER's avatar
Hervé MENAGER committed
541

Hervé  MENAGER's avatar
Hervé MENAGER committed
542
class PpiComplex(models.Model):
Hervé  MENAGER's avatar
Hervé MENAGER committed
543
544
545
    """
    PPI Complex
    """
546

547
548
    ppi = models.ForeignKey(Ppi, models.CASCADE)
    complex = models.ForeignKey(ProteinDomainComplex, models.CASCADE)
549
    cc_nb = models.IntegerField(verbose_name=_("cc_nb_verbose_name"), default=1)
Hervé  MENAGER's avatar
Hervé MENAGER committed
550
551
552
553

    class Meta:
        verbose_name_plural = "Ppi complexes"

554
    def __str__(self):
555
556
        return "PPI {}, Complex {} ({})".format(self.ppi, self.complex, self.cc_nb)

557

558
class CompoundsManager(models.Manager):
Hervé  MENAGER's avatar
Hervé MENAGER committed
559
560
561
562
563
    """
    Model manager for the `Compound` class
    provides selections to `validated` or `user-visible` compounds
    """

564
    def for_user(self, current_user):
565
566
567
568
569
        """
        Get compounds visible to a given user
        i.e. validated or created by the user or
        all of them if the user is an admin
        """
570
571
572
573
574
575
576
577
578
        qs = self.get_queryset()
        if current_user.is_anonymous:
            qs = qs.exclude(compoundaction__ppi__contribution__validated=False)
        elif not current_user.is_superuser:
            qs = qs.exclude(
                Q(compoundaction__ppi__contribution__validated=False),
                ~Q(compoundaction__ppi__contribution__contributor=current_user),
            )
        return qs
579

580
581
582
583
584
585
586
587
588
589
    def validated(self):
        """
        Get validated compounds
        """
        return (
            super()
            .get_queryset()
            .exclude(compoundaction__ppi__contribution__validated=False)
        )

Bryan  BRANCOTTE's avatar
Bryan BRANCOTTE committed
590

591
class Compound(AutoFillableModel):
Hervé  MENAGER's avatar
Hervé MENAGER committed
592
593
594
    """
    Chemical compound
    """
595

596
    objects = CompoundsManager()
597

598
    canonical_smile = models.TextField(verbose_name="Canonical Smiles", unique=True)
599
    is_macrocycle = models.BooleanField(
600
601
        verbose_name=_("is_macrocycle_verbose_name"),
        help_text=_("is_macrocycle_help_text"),
602
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
603
    aromatic_ratio = models.DecimalField(
604
        verbose_name="Aromatic ratio",
605
606
        max_digits=3,
        decimal_places=2,
607
608
        blank=True,
        null=True,
609
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
610
    balaban_index = models.DecimalField(
611
        verbose_name="Balaban index",
612
613
        max_digits=3,
        decimal_places=2,
614
615
        blank=True,
        null=True,
616
617
    )
    fsp3 = models.DecimalField(
618
        verbose_name="Fsp3", max_digits=3, decimal_places=2, blank=True, null=True
619
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
620
    gc_molar_refractivity = models.DecimalField(
621
        verbose_name="GC Molar Refractivity",
622
623
        max_digits=5,
        decimal_places=2,
624
625
        blank=True,
        null=True,
626
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
627
    log_d = models.DecimalField(
628
        verbose_name="LogD (Partition coefficient octanol-1/water, with pKa information)",
629
630
        max_digits=4,
        decimal_places=2,
631
632
        blank=True,
        null=True,
633
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
634
    a_log_p = models.DecimalField(
635
        verbose_name="ALogP (Partition coefficient octanol-1/water)",
636
637
        max_digits=4,
        decimal_places=2,
638
639
        blank=True,
        null=True,
640
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
641
    mean_atom_vol_vdw = models.DecimalField(
642
        verbose_name="Mean atom volume computed with VdW radii",
643
644
        max_digits=4,
        decimal_places=2,
645
646
        blank=True,
        null=True,
647
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
648
    molecular_weight = models.DecimalField(
649
        verbose_name="Molecular weight",
650
651
        max_digits=6,
        decimal_places=2,
652
653
        blank=True,
        null=True,
654
655
    )
    nb_acceptor_h = models.IntegerField(
656
        verbose_name="Number of hydrogen bond acceptors", blank=True, null=True
657
658
    )
    nb_aliphatic_amines = models.IntegerField(
659
        verbose_name="Number of aliphatics amines", blank=True, null=True
660
661
    )
    nb_aromatic_bonds = models.IntegerField(
662
        verbose_name="Number of aromatic bonds", blank=True, null=True
663
664
    )
    nb_aromatic_ether = models.IntegerField(
665
        verbose_name="Number of aromatic ethers", blank=True, null=True
666
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
667
    nb_aromatic_sssr = models.IntegerField(
668
        verbose_name="Number of aromatic Smallest Set of System Rings (SSSR)",
669
670
        blank=True,
        null=True,
671
    )
672
    nb_atom = models.IntegerField(verbose_name="Number of atoms", blank=True, null=True)
673
    nb_atom_non_h = models.IntegerField(
674
        verbose_name="Number of non hydrogen atoms", blank=True, null=True
675
676
    )
    nb_benzene_like_rings = models.IntegerField(
677
        verbose_name="Number of benzene-like rings", blank=True, null=True
678
679
    )
    nb_bonds = models.IntegerField(
680
        verbose_name="Number of bonds", blank=True, null=True
681
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
682
    nb_bonds_non_h = models.IntegerField(
683
        verbose_name="Number of bonds not involving a hydrogen", blank=True, null=True
684
685
    )
    nb_br = models.IntegerField(
686
        verbose_name="Number of Bromine atoms", blank=True, null=True
687
688
    )
    nb_c = models.IntegerField(
689
        verbose_name="Number of Carbon atoms", blank=True, null=True
690
691
    )
    nb_chiral_centers = models.IntegerField(
692
        verbose_name="Number of chiral centers", blank=True, null=True
693
694
    )
    nb_circuits = models.IntegerField(
695
        verbose_name="Number of circuits", blank=True, null=True
696
697
    )
    nb_cl = models.IntegerField(
698
        verbose_name="Number of Chlorine atoms", blank=True, null=True
699
700
    )
    nb_csp2 = models.IntegerField(
701
        verbose_name="Number of sp2-hybridized carbon atoms", blank=True, null=True
702
703
    )
    nb_csp3 = models.IntegerField(
704
        verbose_name="Number of sp3-hybridized carbon atoms", blank=True, null=True
705
706
    )
    nb_donor_h = models.IntegerField(
707
        verbose_name="Number of hydrogen bond donors", blank=True, null=True
708
709
    )
    nb_double_bonds = models.IntegerField(
710
        verbose_name="Number of double bonds", blank=True, null=True
711
712
    )
    nb_f = models.IntegerField(
713
        verbose_name="Number of fluorine atoms", blank=True, null=True
714
715
    )
    nb_i = models.IntegerField(
716
        verbose_name="Number of iodine atoms", blank=True, null=True
717
718
    )
    nb_multiple_bonds = models.IntegerField(
719
        verbose_name="Number of multiple bonds", blank=True, null=True
720
721
    )
    nb_n = models.IntegerField(
722
        verbose_name="Number of nitrogen atoms", blank=True, null=True
723
724
    )
    nb_o = models.IntegerField(
725
        verbose_name="Number of oxygen atoms", blank=True, null=True
726
727
    )
    nb_rings = models.IntegerField(
728
        verbose_name="Number of rings", blank=True, null=True
729
730
    )
    nb_rotatable_bonds = models.IntegerField(
731
        verbose_name="Number of rotatable bonds", blank=True, null=True
732
    )
733
734
    inchi = models.TextField(verbose_name="InChi", blank=True, null=True)
    inchikey = models.TextField(verbose_name="InChiKey", blank=True, null=True)
Hervé  MENAGER's avatar
Hervé MENAGER committed
735
    randic_index = models.DecimalField(
736
        verbose_name="Randic index",
737
738
        max_digits=4,
        decimal_places=2,
739
740
        blank=True,
        null=True,
741
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
742
    rdf070m = models.DecimalField(
743
        verbose_name="RDF070m, radial distribution function weighted by the atomic masses at 7Å",
744
745
        max_digits=5,
        decimal_places=2,
746
747
        blank=True,
        null=True,
748
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
749
    rotatable_bond_fraction = models.DecimalField(
750
        verbose_name="Fraction of rotatable bonds",
751
752
        max_digits=3,
        decimal_places=2,
753
754
        blank=True,
        null=True,
755
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
756
    sum_atom_polar = models.DecimalField(
757
        verbose_name="Sum of atomic polarizabilities",
758
759
        max_digits=5,
        decimal_places=2,
760
761
        blank=True,
        null=True,
762
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
763
    sum_atom_vol_vdw = models.DecimalField(
764
        verbose_name="Sum of atom volumes computed with VdW radii",
765
766
        max_digits=6,
        decimal_places=2,
767
768
        blank=True,
        null=True,
769
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
770
    tpsa = models.DecimalField(
771
        verbose_name="Topological Polar Surface Area (TPSA)",
772
773
        max_digits=5,
        decimal_places=2,
774
775
        blank=True,
        null=True,
776
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
777
    ui = models.DecimalField(
778
        verbose_name="Unsaturation index",
779
780
        max_digits=4,
        decimal_places=2,
781
782
        blank=True,
        null=True,
783
784
    )
    wiener_index = models.IntegerField(
785
        verbose_name="Wiener index", blank=True, null=True
786
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
787
    common_name = models.CharField(
788
        verbose_name="Common name", max_length=20, blank=True, null=True
789
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
790
    pubchem_id = models.CharField(
791
        verbose_name="Pubchem ID", max_length=10, blank=True, null=True
792
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
793
    chemspider_id = models.CharField(
794
        verbose_name="Chemspider ID", unique=True, max_length=10, blank=True, null=True
795
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
796
    chembl_id = models.CharField(
797
        verbose_name="Chembl ID", max_length=30, blank=True, null=True
798
    )
Hervé  MENAGER's avatar
Hervé MENAGER committed
799
    iupac_name = models.TextField(verbose_name="IUPAC name", blank=True, null=True)
800
    ligand_id = models.CharField("PDB Ligand ID", max_length=3, blank=True, null=True)
801
    drugbank_id = models.TextField("Drugbank ID", blank=True, null=True)
802
    pubs = models.IntegerField(
803
804
        verbose_name="Number of publications", null=True, blank=True
    )
805
    best_activity = models.DecimalField(
806
807
808
809
810
        "Best activity", max_digits=12, decimal_places=10, null=True, blank=True
    )
    best_activity_ppi_family_name = models.CharField(
        "Best activity PPI family name", max_length=30, null=True, blank=True
    )
811
812
    families = models.TextField("PPI family names", null=True, blank=True)

813
814
815
816
817
818
819
820
821
822
823
824
    le = models.FloatField(verbose_name="Ligand efficiency", null=True, blank=True)
    lle = models.FloatField(verbose_name="Lipophilic efficiency", null=True, blank=True)
    lipinsky_mw = models.BooleanField("MW ok for Lipinsky", null=True, blank=True)
    lipinsky_hba = models.BooleanField(
        "Hydrogen bond acceptors ok for Lipinsky", null=True, blank=True
    )
    lipinsky_hbd = models.BooleanField(
        "Hydrogen bond donors ok for Lipinsky", null=True, blank=True
    )
    lipinsky_a_log_p = models.BooleanField(
        "A log P ok for Lipinsky", null=True, blank=True
    )
825
    lipinsky_score = models.IntegerField(
826
        verbose_name="Lipinsky score", null=True, blank=True
827
    )