diff --git a/Dockerfile b/Dockerfile index 08cf1600..713b3c4f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -30,13 +30,15 @@ RUN mkdir -p -m 0700 /root/.ssh \ && ssh-keyscan git.envipath.com >> /root/.ssh/known_hosts # We'll need access to private repos, let docker make use of host ssh agent and use it like: -# docker build --ssh default -t envipath/envipy:1.0 . +# docker build --ssh default -t envipath/envipy-bayer:1.0 . RUN --mount=type=ssh \ uv sync --locked --extra ms-login --extra pepper-plugin # Now copy source and do a final sync to install the project itself # Ensure .dockerignore is reasonable +COPY bb4g bb4g COPY biotransformer biotransformer +COPY bayer bayer COPY bridge bridge COPY envipath envipath COPY epapi epapi diff --git a/bayer/__init__.py b/bayer/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bayer/admin.py b/bayer/admin.py new file mode 100644 index 00000000..f0b257bb --- /dev/null +++ b/bayer/admin.py @@ -0,0 +1,19 @@ +from django.contrib import admin + +# Register your models here. +from .models import ( + PESCompound, + PESStructure +) + + +class PESCompoundAdmin(admin.ModelAdmin): + pass + + +class PESStructureAdmin(admin.ModelAdmin): + pass + + +admin.site.register(PESCompound, PESCompoundAdmin) +admin.site.register(PESStructure, PESStructureAdmin) diff --git a/bayer/apps.py b/bayer/apps.py new file mode 100644 index 00000000..3821be18 --- /dev/null +++ b/bayer/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class BayerConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'bayer' diff --git a/bayer/epdb_hooks.py b/bayer/epdb_hooks.py new file mode 100644 index 00000000..a5279572 --- /dev/null +++ b/bayer/epdb_hooks.py @@ -0,0 +1,39 @@ +import logging + +from epdb.template_registry import register_template + +logger = logging.getLogger(__name__) + +# PES Create +register_template( + "epdb.actions.collections.compound", + "actions/collections/new_pes.html", +) +register_template( + "modals.collections.compound", + "modals/collections/new_pes_modal.html", +) +register_template( + "epdb.actions.objects.pathway.add", + "actions/objects/pathway_add_pes.html", +) +register_template( + "epdb.modals.objects.pathway.add", + "modals/objects/add_pathway_pes_node_modal.html" +) + +# PES Viz +register_template( + "epdb.objects.compound.viz", + "objects/compound_viz.html", +) + +register_template( + "epdb.objects.compound_structure.viz", + "objects/compound_structure_viz.html", +) + +register_template( + "epdb.objects.node.viz", + "objects/node_viz.html", +) \ No newline at end of file diff --git a/bayer/migrations/0001_initial.py b/bayer/migrations/0001_initial.py new file mode 100644 index 00000000..a9d11d9a --- /dev/null +++ b/bayer/migrations/0001_initial.py @@ -0,0 +1,35 @@ +# Generated by Django 5.2.7 on 2026-03-06 10:51 + +import django.utils.timezone +import model_utils.fields +import uuid +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='Package', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')), + ('classification_level', models.IntegerField(choices=[(0, 'Internal'), (10, 'Restricted'), (20, 'Secret')], default=10)), + ], + options={ + 'db_table': 'epdb_package', + }, + ), + ] diff --git a/bayer/migrations/0002_initial.py b/bayer/migrations/0002_initial.py new file mode 100644 index 00000000..6a5a15d8 --- /dev/null +++ b/bayer/migrations/0002_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 5.2.7 on 2026-03-06 10:51 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('bayer', '0001_initial'), + ('epdb', '0019_remove_scenario_additional_information_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='package', + name='license', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License'), + ), + ] diff --git a/bayer/migrations/0003_pescompound_pesstructure_package_data_pool.py b/bayer/migrations/0003_pescompound_pesstructure_package_data_pool.py new file mode 100644 index 00000000..066eec65 --- /dev/null +++ b/bayer/migrations/0003_pescompound_pesstructure_package_data_pool.py @@ -0,0 +1,41 @@ +# Generated by Django 6.0.3 on 2026-04-17 21:22 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('bayer', '0002_initial'), + ('epdb', '0023_alter_compoundstructure_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='PESCompound', + fields=[ + ('compound_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.compound')), + ], + options={ + 'abstract': False, + }, + bases=('epdb.compound',), + ), + migrations.CreateModel( + name='PESStructure', + fields=[ + ('compoundstructure_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.compoundstructure')), + ('pes_link', models.URLField(verbose_name='PES Link')), + ], + options={ + 'abstract': False, + }, + bases=('epdb.compoundstructure',), + ), + migrations.AddField( + model_name='package', + name='data_pool', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.group', verbose_name='Data pool'), + ), + ] diff --git a/bayer/migrations/__init__.py b/bayer/migrations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bayer/models.py b/bayer/models.py new file mode 100644 index 00000000..b4ac7632 --- /dev/null +++ b/bayer/models.py @@ -0,0 +1,236 @@ +from typing import List +import urllib.parse +import nh3 +from django.conf import settings as s +from django.db import models, transaction +from django.db.models import QuerySet +from django.urls import reverse + +from epdb.models import ( + EnviPathModel, + Compound, + CompoundStructure, + ParallelRule, + SequentialRule, + SimpleAmbitRule, + SimpleRDKitRule, +) +from utilities.chem import FormatConverter + + +class Package(EnviPathModel): + reviewed = models.BooleanField(verbose_name="Reviewstatus", default=False) + license = models.ForeignKey( + "epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License" + ) + + class Classification(models.IntegerChoices): + INTERNAL = 0, "Internal" + RESTRICTED = 10 , "Restricted" + SECRET = 20, "Secret" + + classification_level = models.IntegerField( + choices=Classification, + default=Classification.RESTRICTED, + ) + + data_pool = models.ForeignKey("epdb.Group", on_delete=models.SET_NULL, blank=True, null=True, + verbose_name="Data pool", default=None) + + def delete(self, *args, **kwargs): + # explicitly handle related Rules + for r in self.rules.all(): + r.delete() + super().delete(*args, **kwargs) + + def __str__(self): + return f"{self.name} (pk={self.pk})" + + @property + def compounds(self) -> QuerySet: + return self.compound_set.all() + + @property + def rules(self) -> QuerySet: + return self.rule_set.all() + + @property + def reactions(self) -> QuerySet: + return self.reaction_set.all() + + @property + def pathways(self) -> QuerySet: + return self.pathway_set.all() + + @property + def scenarios(self) -> QuerySet: + return self.scenario_set.all() + + @property + def models(self) -> QuerySet: + return self.epmodel_set.all() + + def _url(self): + return "{}/package/{}".format(s.SERVER_URL, self.uuid) + + def get_applicable_rules(self) -> List["Rule"]: + """ + Returns a ordered set of rules where the following applies: + 1. All Composite will be added to result + 2. All SimpleRules will be added if theres no CompositeRule present using the SimpleRule + Ordering is based on "url" field. + """ + rules = [] + rule_qs = self.rules + + reflected_simple_rules = set() + + for r in rule_qs: + if isinstance(r, ParallelRule) or isinstance(r, SequentialRule): + rules.append(r) + for sr in r.simple_rules.all(): + reflected_simple_rules.add(sr) + + for r in rule_qs: + if isinstance(r, SimpleAmbitRule) or isinstance(r, SimpleRDKitRule): + if r not in reflected_simple_rules: + rules.append(r) + + rules = sorted(rules, key=lambda x: x.url) + return rules + + class Meta: + db_table = "epdb_package" + + +class PESCompound(Compound): + + @staticmethod + @transaction.atomic + def create( + package: "Package", pes_data: dict, name: str = None, description: str = None, *args, **kwargs + ) -> "Compound": + + pes_url = pes_data["pes_url"] + + # Check if we find a direct match for a given pes_link + if PESStructure.objects.filter(pes_link=pes_url, compound__package=package).exists(): + # Due to normalization we might end up in having multiple structures + # All of them point to the same compound -> pick any + return PESStructure.objects.filter(pes_link=pes_url, compound__package=package).first().compound + + # Generate Compound + c = PESCompound() + c.package = package + + if name is not None: + # Clean for potential XSS + name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + + if name is None or name == "": + name = f"Compound {Compound.objects.filter(package=package).count() + 1}" + + c.name = name + + # We have a default here only set the value if it carries some payload + if description is not None and description.strip() != "": + c.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() + + c.save() + + molfile = pes_data.get("representativeStructures", [{}])[0].get("ctab") + + if molfile is None: + raise ValueError("PES data does not contain a valid mol file!") + + smiles = FormatConverter.to_smiles(FormatConverter.from_molfile(molfile)) + + standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True) + + is_standardized = standardized_smiles == smiles + + if not is_standardized: + _ = PESStructure.create( + c, + pes_url, + molfile, + standardized_smiles, + name="Normalized structure of {}".format(name), + description="{} (in its normalized form)".format(description), + normalized_structure=True, + ) + + + cs = PESStructure.create( + c, + pes_url, + molfile, + smiles, + name=name, + description=description, + normalized_structure=is_standardized + ) + + c.default_structure = cs + c.save() + + return c + + +class PESStructure(CompoundStructure): + pes_link = models.URLField(blank=False, null=False, verbose_name="PES Link") + + @staticmethod + @transaction.atomic + def create( + compound: Compound, + pes_link: str, + mol_file: str, + smiles: str, + name: str = None, + description: str = None, + *args, + **kwargs + ): + if compound.pk is None: + raise ValueError("Unpersisted Compound! Persist compound first!") + + cs = PESStructure() + # Clean for potential XSS + if name is not None: + cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip() + + if description is not None: + cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip() + + cs.smiles = smiles + cs.mol_file = mol_file + cs.pes_link = pes_link + cs.compound = compound + + if "normalized_structure" in kwargs: + cs.normalized_structure = kwargs["normalized_structure"] + + cs.save() + + return cs + + @transaction.atomic + def add_structure( + self, + smiles: str, + name: str = None, + description: str = None, + default_structure: bool = False, + *args, + **kwargs, + ) -> "CompoundStructure": + raise ValueError("Not supported!") + + def d3_json(self): + return { + "is_pes": True, + "pes_link": self.pes_link, + # Will overwrite image from Node + "image": f"{reverse("depict_pes")}?pesLink={urllib.parse.quote(self.pes_link)}" + } diff --git a/bayer/templates/actions/collections/new_pes.html b/bayer/templates/actions/collections/new_pes.html new file mode 100644 index 00000000..bdae544f --- /dev/null +++ b/bayer/templates/actions/collections/new_pes.html @@ -0,0 +1,9 @@ +{% if meta.can_edit %} + +{% endif %} \ No newline at end of file diff --git a/bayer/templates/actions/objects/pathway_add_pes.html b/bayer/templates/actions/objects/pathway_add_pes.html new file mode 100644 index 00000000..41490fb1 --- /dev/null +++ b/bayer/templates/actions/objects/pathway_add_pes.html @@ -0,0 +1,8 @@ +
  • + + Add PES +
  • \ No newline at end of file diff --git a/bayer/templates/modals/collections/new_package_modal.html b/bayer/templates/modals/collections/new_package_modal.html new file mode 100644 index 00000000..54e03fae --- /dev/null +++ b/bayer/templates/modals/collections/new_package_modal.html @@ -0,0 +1,175 @@ +{% load static %} + + + + + + + \ No newline at end of file diff --git a/bayer/templates/modals/collections/new_pes_modal.html b/bayer/templates/modals/collections/new_pes_modal.html new file mode 100644 index 00000000..1ebbdc34 --- /dev/null +++ b/bayer/templates/modals/collections/new_pes_modal.html @@ -0,0 +1,174 @@ +{% load static %} + + + + + + + \ No newline at end of file diff --git a/bayer/templates/modals/objects/add_pathway_pes_node_modal.html b/bayer/templates/modals/objects/add_pathway_pes_node_modal.html new file mode 100644 index 00000000..bf08083f --- /dev/null +++ b/bayer/templates/modals/objects/add_pathway_pes_node_modal.html @@ -0,0 +1,174 @@ +{% load static %} + + + + + + + \ No newline at end of file diff --git a/bayer/templates/objects/compound_structure_viz.html b/bayer/templates/objects/compound_structure_viz.html new file mode 100644 index 00000000..8924c63e --- /dev/null +++ b/bayer/templates/objects/compound_structure_viz.html @@ -0,0 +1,19 @@ +{% if compound_structure.pes_link %} + +
    + +
    Link to PES
    +
    {{ compound_structure.pes_link }}
    +
    + + +
    + +
    PES Image Representation
    +
    +
    + +
    +
    +
    +{% endif %} \ No newline at end of file diff --git a/bayer/templates/objects/compound_viz.html b/bayer/templates/objects/compound_viz.html new file mode 100644 index 00000000..9ff10e75 --- /dev/null +++ b/bayer/templates/objects/compound_viz.html @@ -0,0 +1,19 @@ +{% if compound.default_structure.pes_link %} + +
    + +
    Link to PES
    +
    {{ compound.default_structure.pes_link }}
    +
    + + +
    + +
    PES Image Representation
    +
    +
    + +
    +
    +
    +{% endif %} \ No newline at end of file diff --git a/bayer/templates/objects/node_viz.html b/bayer/templates/objects/node_viz.html new file mode 100644 index 00000000..4426726b --- /dev/null +++ b/bayer/templates/objects/node_viz.html @@ -0,0 +1,19 @@ +{% if node.default_node_label.pes_link %} + +
    + +
    Link to PES
    +
    {{ node.default_node_label.pes_link }}
    +
    + + +
    + +
    PES Image Representation
    +
    +
    + +
    +
    +
    +{% endif %} \ No newline at end of file diff --git a/bayer/templates/objects/package.html b/bayer/templates/objects/package.html new file mode 100644 index 00000000..c03be1d2 --- /dev/null +++ b/bayer/templates/objects/package.html @@ -0,0 +1,97 @@ +{% extends "framework_modern.html" %} +{% load static %} +{% block content %} + + {% block action_modals %} + {% include "modals/objects/edit_package_modal.html" %} + {% include "modals/objects/edit_package_permissions_modal.html" %} + {% include "modals/objects/publish_package_modal.html" %} + {% include "modals/objects/set_license_modal.html" %} + {% include "modals/objects/export_package_modal.html" %} + {% include "modals/objects/generic_delete_modal.html" %} + {% endblock action_modals %} + +
    + +
    +
    +
    +

    {{ package.name }} {% if meta.url_contains_package and meta.current_package.get_classification_level_display == "Restricted" %}{% elif meta.url_contains_package and meta.current_package.get_classification_level_display == "Secret" %}{% endif %}

    + +
    +

    {{ package.description|safe }}

    + +
    +
    +
    + + +{% endblock content %} diff --git a/bayer/templates/static/login.html b/bayer/templates/static/login.html new file mode 100644 index 00000000..65c54132 --- /dev/null +++ b/bayer/templates/static/login.html @@ -0,0 +1,154 @@ +{% extends "static/login_base.html" %} +{% load static %} +{% block title %}enviPath - Sign In{% endblock %} + +{% block extra_styles %} + +{% endblock %} + +{% block content %} +
    + + +
    +
    +

    +

    + +
    + + + + + +
    + +
    + + +
    +
    + {% csrf_token %} + + +
    + + +
    + +
    + + +
    + + + + + + +
    +
    + +{% endblock %} + +{% block extra_scripts %} + +{% endblock %} diff --git a/bayer/tests.py b/bayer/tests.py new file mode 100644 index 00000000..7ce503c2 --- /dev/null +++ b/bayer/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/bayer/tests/__init__.py b/bayer/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bayer/tests/pes/__init__.py b/bayer/tests/pes/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bayer/tests/pes/test_pes.py b/bayer/tests/pes/test_pes.py new file mode 100644 index 00000000..2ed868d4 --- /dev/null +++ b/bayer/tests/pes/test_pes.py @@ -0,0 +1,174 @@ +from django.test import TestCase + +from bayer.models import PESCompound, PESStructure +from epdb.logic import UserManager +from epdb.models import CompoundStructure, Compound + + +class PESTest(TestCase): + + @classmethod + def setUpTestData(cls): + cls.user = UserManager.create_user( + "test-user", + "test-user@test.com", + "TestPass123", + set_setting=False, + add_to_group=False, + is_active=True, + ) + + cls.pes_dummy = { + "representativeStructures": [ + { + "ctab": "\n RDKit 2D\n\n 14 15 0 0 0 0 0 0 0 0999 V2000\n 2.7760 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.2760 0.0000 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0\n 0.3943 1.2135 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7062 2.6807 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n -0.4086 3.6844 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0\n -0.0967 5.1517 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n -1.8351 3.2209 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n -2.1470 1.7537 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0\n -3.5736 1.2902 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n -1.0323 0.7500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n -1.0323 -0.7500 0.0000 N 0 0 0 0 0 0 0 0 0 0 0 0\n 0.3943 -1.2135 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n -2.9499 4.2246 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 2.1328 3.1443 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 1 0\n 2 3 1 0\n 3 4 1 0\n 4 5 1 0\n 5 6 1 0\n 5 7 1 0\n 7 8 1 0\n 8 9 1 0\n 8 10 1 0\n 10 11 1 0\n 11 12 2 0\n 7 13 2 0\n 4 14 2 0\n 12 2 1 0\n 10 3 2 0\nM END\n" + } + ], + "representations": [ + { + "renderingOptions": { + "molecularFormulaRenderingOption": { + "displayOnRepresentation": False, + "labelCoords": { + "x": 0, + "y": 0 + } + }, + "modificationRenderingOptions": [ + { + "localModificationId": 1, + "displayNominalMass": False, + "displaySumFormula": False + } + ], + "contouringType": "convexHull", + "colored": True, + "abbreviations": False, + "structureSize": 0, + "showDirection": None, + "showStereoFlags": False + }, + "type": "black-and-white", + "mimeType": "image/png", + "base64": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAACWCAYAAACb3McZAAAABmJLR0QA/wD/AP+gvaeTAAATvklEQVR4nO2daZhU1bWG32pGGSUQwfkiKhFQQeMUUZIrkkSDxAET0Sgar5IrTonKvdFEgvN0nW7iFBUNoiJqFGccEDXgGBxAiAiCKA6grczjlx9rV9fp6moKuqtqn6qz3+epp6pXnTr9naq9zp7WXhsCgUAgEAgEAoFAIBAIBAKBQCBQxjTxLSAQO64HegDTgVWetQQCsaILsAZzjE6etcSCKt8CArHiBKAp8CiwyLOWQCB2zAAEHOJbSCAQN/bDnGMhVosECE2sQIah7vkuYK1HHYFA7NgM+BqrQXbxrCUQiB3HYs4xxbeQQCCOPIs5yCm+hQQCcWMbrM+xHNjcs5bYETrpgROxiIqHgGrPWgKlRx1ArXLY24Fall5PrEgBs7HmVX/PWgJ+0FrQR6DWWfYZoBFeJMWHfphzfEyIy8tJUppYnYHzfYuIH9f+FDq9DdwJrPOtJuAFrQVdCVoF6hGxJ7wGURvQEtB6WNnNt5q4kpQaZBLwGHAzKOVZS1wYDLQBJkPLD32LiStJcRCA3wF7kgmpSDonuufRPkUEvKO1IBedqt+DvnAjWwluYqmrNa20FNTWt5o4k6QaBOBqYDHwR99CPHMSNsQ7DlJLfIsJlBz1Aw1zryM1CIAOch32xcmsQVQFmgcS6EDfagIlRW1Af3bNBzdqle0gALrXFZARoD6gl0Db+dFcanSwu/Y5YcAiUehA0Afux18NuhzUHPQaaP+sY7cCTQWd4JxDoE9B+/nRXko01l3vBb6VBEqC2oNucbWGQNNAe2zC5zuCJrrPrgJVcESr2oOWgdYlp8ZMNDoENN8V7hWgkaBmWcd8B7R9nvM0cTWO3OMWq30qDQ1z1/eMbyWBoqLNXSFOF+h/gHKshNPPQAtAU8wJ8p73GHeHFegV0JaF1+4TTXXXNsS3kkDR0EDQJ+6HXu462lmFX1uA7o840ItWk2zU+XuD5rrPfQLat/DX4AN1d9dUTc7o5kCZo86gByKF/iXQzjmOG4xNBsrVBiNsaHOT/lcn0LPuHCtBJxfmGnyiQdjQ9s2+lQQKz3Ew7AlXYL9xbemsIUp1AT2YVWvs2PB/qaaV1y9RC3P+QKWwNTABELAC3hyTe/RFg0GLIk2IU+o6UEPRCW4AwNVac7sU5rzFRLe42iJrNE/ngSZ5kRQoOIOxsBBhS0FPwcIjImgr0CORu/wToG0LL0V9QB/B8lnQdjYQ836JxoDWYHNAkf6ZLgS95U9XoBBsDzyDOYaAx7HEAlFSsNeJoG+dYywCHVdcWeoMuz5ETW1WEw0bQzQG9DDoc9DwiD04SBmTwmqJb7FC+BW509B0pSZNzWUvgyZYTVISmgKXk3HeW4AY9ks0BnQX6L9cn21rZw8OUqbsADxHpuBNALILfRVwFrDMHfMZtD28lCIjHIulyRHwMrZtgGdUBdrBmlQ1DlIFeh10nzsmOEiZUYXVEkupKfQcleO4btiqwLQDjcP//hV7AB9hehYA+5Tm36qpc4T+oDNdh/xlbPmsQLtkHARAe2EBm/2Dg5QX+wMvsuFC3xQ4k0yt8Snw8xJqzEcn4HlM20psnUWBUHNQLzdCdyE28fkOFi+meh4LsKDNiIMA6C+g90CjgoOUB/2wbH7plDOH5jimF/AatR1oI2fDS0qufkmzDX6iNi2BPsAxwMXAgzByvBuFyuUE67GZ/idBV4F+DdoX1D5zyjoO0sF12OcEBykP+mFpZlZRN91lM2AEdkcWMBc4uKTqGsZxZPolL1G3X9Ic6IkNX4/EHH46mRtF5HHgJDIh+BNB12NzO33ZqCWy2Q4CoKHunMFByoS5WIHonWW/wdnXATdimTfKhX2BTzD9nwB3AE9i17qeOo6AgNXA+8B44CLgl7DnrjQq66P+D3R1li0FetRG/QLlwG1YATkvy74N8DpwQMkVFYbvAm8A31DXET7ERuguB47HsqxsVnqJqgKdXbtZFogbR2MFZ6JvIY2gvhD6W7FrmwwcDuxMrLY101WuufU4mxzIGSgVHbD290qgdZ5j48oVwCvUru1aktmtaVcfovKj7V2nXaDLfasJ1M+rWEH6iW8hDaApNuwsas+BDHG2qT5EbTzqGxkyjnHYTLK5CCtM1/gW0gAOxbTPyrKn48iGlVzRJqOTyCxX3tu3mkBdDsQK07u+hTSA8dQdZEjv1rQCa0KWAbopMqS8tW81gdo0IzPakx2xG2c6Yn2nNdSOGbsAu5axPkQ1DDUDPe+c5A2Qh1G1wIZ4FCtUQz3r2BTOwDQ/FrGlgA+cvRwmNiOoI2i2c5K7fasJ1OZ0rFDd41vIJvAWpvnIiO0AZ1tAWe7WpB5YaLxsjiQQF7pjBetLyiOZ9q6Y3sVAi4j9Dme/2IeowqDDsaRyOdK1BnwyBytcm5AR0RvXYVpviNhak1no1d2HqMKhka4W+Qqu38m3mgAA3a+FC1+Cp4bnP9YrzYEvMEfoE7EPJTNzXuYoBWvvgz9NBmYS073TBa0EpwnuFzwluEWVu1OvBru71nO+leThCMwR3smyT3L2SplwawO8jV3TU8SsTyXoKHhPsEBwiWC4YLRgneBK3/qKgDq4du8qUJyjd9MpiM6M2LpikbpLgUrarWl7MrWlt3AUQRPBjoJBcpHdgtudc3TKOnaIWzTTz4/aolKTNzauncPOWETuaixiN80orBCN9qCp2PTF1uwIW4dfNARNBTsIBgpGCO4WvCFYGlkt9gNBM8Eywbk5zpFyNctfi6nVExrlvodrfSuph3OwgvJQxJYiM8BQiXctgN9ATaqjRoejCFoIdhf8QjBK8IBgumB1dNlk1mOe4GnBPq42keCn9Zz/HsE/GqszhugA931M962kHt7BCsrAiK2/s82hTlK7iuImqMkJsLHhKK2w9S7HAZcCDwP/Wgsf1+ME6wQfCh4TXCEYKthbWc1WQS93/P45/ieCmwT/bPCVZhGjNQpMwcJOeoC2hdTHvgVF2Bub//gc67SmGeqeR2MFqFI5Axu+/hEWg/ZDrOkF0A7YCUvd1BPo4Z6/R455rS/g9S2tmToHmIEtO54BTEtZPy4fX7jn+hx1Gyw7TiWiv7sbSgGzgxSE9B30qoitHZZtZT1WOCqdjsBs7Ht4D3gamO/+zvVY5Y4bh63BPxq7yTQo0Z5gM8Fu7vVMwd9yHNNB8I3gfxryP8oAneYc5F7fSiK0xLI8itoLoE5xtmd9iPLEbmRymEUdYTrmCNGlxA1aU+/6KD0FgwUjBeNcH2WtYL2greB4wRpFcqjJ5kXuEXwuc+ZKRDuRybUbl7CTX2IF4dUs+xRKMLoTQw7Has112MBFg+ZI3N3+B4KTBdfIJvvmbaCjvto5Sjf3+XMFywWfCd5yr2eqPKIxGoM+dN/J930rcTyNOcJvIradsULyDdYZTRrphW7LyX+3/i7WZxkG3Li5dcI/3YAjrBRME9wr+IPgKEEP5cg1JmgvGOCO+b6KMKEZp056monAqcAALDuIT7YGDsLWftwXsZ+EjVrdjxWSpJEOqVmNrb8HWyAW7aSnn2vt81gNrIcvq+yzs8l00tPPM1NWO+UlZTeopG1IqiPdzeQF30qA87E7ZbRP1ATLBikgAfuq52Qsdv1vYnMO6SQVuR7V2Pr827Em2SFPwn+osofFi4k2x9JuxiHsZCb2Iw+I2A5xtlkk80dOj95lO8LXWI1/N5YZcyA2upfE76jYaDjoIJDPJmBfyLkAapyzj/AhKgacDDUpYf8bmxvp7FVRstA5oDpxNlhu2SNKKOR2rCBcErF9B+uPrKO81tAXklew7+VXvoUkFM1yfZCsfUI0AXRrCYUMxzqNO2XZBDxRQh1xIj16t4TyyplcSWgWtg/GJ6B2EXupHSQXb2IOMtizDl9cil3/bb6FlIq4TMZlcyc2+nGRbyERemGTUF9hmViSRhUWeAiVGdqfkzjOg4AlXzsLeBJ0N6TezLylTljHcCk2B7EUGw9fbo9UdZE0pePDxpIJ1EsSA4BtsdRGlRhOnpO4OgiQmgh6CLgZFM192wMbSaoHQb3Oc9f7MLSd+3sJlmhhmfu7OvI6/d7yyN/HuH8wuhBXV4aklxPfSWVHLtcixg4C2NLWmdTe+68aeAAbj2/lHh0ir9thHcg2wBa1T7ewFfWsI9gI1mKbdr6Z57hKZHNsXmM9MMazlpIScwdJLQSNwnJNzQQWQuodLHR6A6gt5iytsR/XvW7TAlvHnP1eK6A95lSt3HP2e0uw0JMDsC3WksSx2EY/T2FRBAF/aBbo9MjfzUDvYptX+hzFugZrWiwiGes/InRKp4f9hW8lgToOAtheFr4dpArLxStgGuW76c8mol42L/XcVBq4xiNQEOTSeOonoB1zvP9j0J6l1VSHdljUqYAHSUScka5xE7d/9q0kwWggaB5oL99KNoLuZKJX/+BZS5FRU9DCmK3PSRrqDqp2P0L2jrdxZQA2qrWeip5V1yD3u7znW0lCUVvQdPcjPAgqpybLuVgtsoTYbtbZWPSw+21+51tJAlEKNN79AG+DyrHTm474nUvtbIsVgDq6NTlrQF18q0kg0TT76uZbTQNpia2WE7YMtUEpbeKJzna/zyO+lSQQHYZt1LIOlDOFZBnRhcwS3P/3rKWAaJpzkJ/7VpIwanXKcyyMKkv2ILMM9VTPWgqA9iCTfqmCasXYU9ad8nwchznIaizNTRmjG4l3IvFKRFVu0VM5d8rzcRVlH46i5qAv3e+0u281CaJme4PFZdwpz0cFhKPU7PaVxKhlX2w3EFbOxnaRqtR95NKUeThKTQLx0/MfGygEPYBvof23MHmYbzEloozDUdQedKrNgwSKTVssM4iAezxrKTXRcJQ861d8oetA5+ew/6+FmQSKSRWZTS+nkcwkzzEPR6kZbj8sy/4qKE4JMyqSi7HCsZiyHdEpCDEOR1E1aAroo9qjisFBis0grGmxFvixZy2+iXE4iqpBvwJ9ALoiYg8OUkS+h2USEXC2Zy1xIabhKKoGHe3W46wGuWZgcJBi0QHLm5TETnk+PIWjaDNQH9AQ0CUuguFh955zEAA9CnrZRVkHBykC2ZNkSeyU56OI4ShqAerpJvpGgsa5sJ611N3IaQWoSZaDbAdaCjo2OEiGQqb9uRg4FOuUH0Eyd17Kxxhgd2wjmfHY9tJzNvEcHYBdgB6wtBu07oM1a7ev5/g1wL+w4fb3sUnMmVgfMUJqPugy4DIs91iggByGfeFrgP/0rCXuVJFJgv0e9WdJ74DtFns8tnvsBOBD7Ht2m9ZUvxupFVZj+ztOAF0OOh60pzWx6iNag4CLw3rfnS/UIBSuBnkF2w75ceD5Ap2zUoneuXtid/dfY9ss9KCmdqBTPZ9fhtUAM2DSqzBogb1mDqQ2am+/+kmtdiEmExt3nsqhkHFCTdjIzRcTTk+s5liKBTPW9xt8iw14zKH2JpfvU6d51FDUG5gHqa+z7LsBiyD1KWhHSM0uzP8LBPJzNdZE+gvwW2xIfDqWMV5YbRKD3avUBHSb67j39q0mkAyaAgsxR4jmAEuPbsVsWwHd6voj80Bb5D8+EGgcAzFHmJ5lf9bZTym5og2iZqAXnZO8Qk32y0CgODyEOcI5Eds2WEjOciyjfMxQZ9B85yR3+lYTqFw6YjvkrgG2jNj/iDlNjPfdUG/QMuckp/lWE6hMzsIcIbq/YQqY7ewH+RC18ehILMP+GlCY6woUnH9ijhDd672fs32MDZPHHF1KJq9Ajgz8gUDD6AM12U6iHd3Rzj7Kg6YGoCrQI85JZoDa+1YUqAxuwBzhuoitDbbScD1QRlle1Bbb8UtYgoe4biUeKBOaA19iDhKdcDvJ2SZ50NRI1JVM7qwyqf0CceUozBHeyrJPdvahpRZUGNQf1q2CQS9Q0XukBIrKIrj7PnihF0RTHnXFmlZLscwvZcreJ2NOvpTatWMgkB9BZ8EawSpFEjZUWadcwB3+1BWMW7FrmUedfekDgQ0gOM8t1hgfsVWtg+mvweQ9oa9PfQWiGdaPErbsIYSjBDYOwQznIIdGbAOcbY7KLi1pvXQG5mNOEsJRAvkR7OMc4TNFFqYJxjr7BT71FYHeWF9EwHDPWgJxR3Czc4QrI7b2gmWCdYLtfOorEkcSll4H8iFoKfjaOciuEfswZ3vGp74icwnUZNQM4SiBugiGOEeYmmWf6uzH+NJWAqqARzAnmQGEcJRAbQTPOEcYFrF1d7ZqVX7OsLbAu5iT/B1zmkAABNsI1gpWyFL3pO1XOge52ae+EtINa2aJyCheIOEILnCOMDZiayr41Nn38amvxPTHklAEAiBICT5wjnBwxP4zZ5vpU18g4BXBAc4RFiiyAEow3tnP86kvEPCK4A7nCBdHbB0FK11M1lY+9QUCXnEd9N8rsgBKcIZzmsd8agsEYolgsnOQo3xrCWw6lRIs5xXZOP+hwH7YUtqPgPEpmO/mPI4ExqUsvWggkBwE7QQvudCSsYIbBFNcv2OIb32BgFcEt7o5jq5Z9lFusjDJO/wGkoxgM1dT/Lae974Q/MmHtkBhCDEyjWMXbAXda9lvpGAF8DaWEytQpgQHaRyt3fPiet7/krJOyhAIDtI40jszdann/a2o33kCZUBwkMYxC9sh6kfZb8i2MuhD1pqQQCBRCC51Q7w9IrYmbsntYkXS/QQCiUPQQnCvG82aJBgnmOtGsH7oW1+gcYSZ9AIha071xWbO5wBPp2yn2kAgEAgEAoFAIBAIBAKBQCAQCAQCfvg3Wm+b2+JWRCcAAAErelRYdHJka2l0UEtMIHJka2l0IDIwMjUuMDkuNgAAeJx7v2/tPQYgEABiJgYI4ANifiBuYGRjSADSjMzsDhpAmpmZDUKzwGi4OFQd0eIOGWCakYMBLMAEo7kZGBkYmRKYmBOYWRJYWBlY2BLY2BPYORjYORM4uRK4uDOY2Hg0mJh5NZi4GRM4mTKYnECuZmVk4uTiZgM6jJ2TSXwTyE4GmGd+20cc2Phl9X4Qp95M5sBnEcN9IPZ398L9PDsqwewfPz5YN1/YDFYTm7PP3uXSFDB7JYOMw+OtIgdA7DRNRod32mV2IPb1vR32DPGb7UFstyVn7BOqjR1AbKNvl/YdSJgCFl/Qf2r/HUs2sLh4u+mBDYKLweISz+MdvvJ9B5vz5hXrfqUtxmDzxQC050jYEGHAwQAAAYt6VFh0TU9MIHJka2l0IDIwMjUuMDkuNgAAeJx9U9tqwzAMfc9X6AdqdLMtP65NGWM0ga3bP+x9/8+klNYpmNlVUOTjI+VInSDWx/z+8wuPxfM0AeA/v9YafAsiThcIB47n17cFTteX4z1yWr+W6yeQAmW/4/sZ+3JdL/cIwQkOkqRawwwHSqKi7mHCbfW7DAscOKmicYEDJqrchAZICU5MTTmjhscqVsoAqY6MaJWskVxRDUeU2ZP7sdViHEAqXEodAIszepFSs2KUKyxlyFg3IJJRZogSUZEHONsyo+dtEiUgiYwSN+ejlJuXKE5sqGMZCW/qKDFK9iuUm3sjJIXizmmuo0WxrbKNvoU4OD2pFKn+mrhKq6MqSWAFSdrMhfZzdW4ZdcaHZ40yMwtR2cixWsUB9LzMTyN1G7Ljusx9yGJznyR1kz4uGtZnInbunSe30vvrZ1B7F9XNerPIrfWeaIT22ms8iHYaazyId1qyU5LsJIuCSXfS6AZ7BPKNVvaK7L8/3u9/UPenP7ZRvXseh087AAAAzXpUWHRTTUlMRVMgcmRraXQgMjAyNS4wOS42AAB4nCWPyQ3DQAgAW8nTljDiXEBWXi4gRezfFaT4sM53BMNw3Ty392efMrd73nLt93btk9+f13cTNA03OARVdMDJaBkj4WDkISPgJCQKda3FjCzhPAhdlHmsvaTIeFiZOLWLUExzjAcaC6lD67ykCaOnOIMgVciStUKHRo9IaP2JGaUuE4eUcjNFjXwa1NRWKI2UgtXEf+DV6bCKzHqlL3Cyw8ohoz6uaJX9QFd1Q7+7f3/5ITrPi2XjKgAAAABJRU5ErkJggg==" + }, + { + "renderingOptions": { + "molecularFormulaRenderingOption": { + "displayOnRepresentation": False, + "labelCoords": { + "x": 0, + "y": 0 + } + }, + "modificationRenderingOptions": [ + { + "localModificationId": 1, + "displayNominalMass": False, + "displaySumFormula": False + } + ], + "contouringType": "convexHull", + "colored": True, + "abbreviations": False, + "structureSize": 0, + "showDirection": None, + "showStereoFlags": False + }, + "type": "color", + "mimeType": "image/png", + "base64": "data:image/png;base64,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" + } + ], + "lifecycleStatus": "Published", + "corporateId": "PES-000126", + 'pes_url': 'https://pesregapp-test.cropkey-np.ag/entities/PES-000126', + "classificationLevel": "Restricted", + "dataPools": None, + } + + def test_smoke(self): + pes_compound = PESCompound.create( + self.user.default_package, + self.pes_dummy, + 'Test PES', + 'Test Description' + ) + + self.assertEqual(PESCompound.objects.count(), 1) + + obj = PESCompound.objects.first() + + self.assertEqual(obj.name, 'Test PES') + self.assertEqual(obj.description, 'Test Description') + self.assertTrue(isinstance(obj.default_structure, PESStructure)) + self.assertEqual(obj.default_structure.pes_link, 'https://pesregapp-test.cropkey-np.ag/entities/PES-000126') + self.assertEqual(obj.default_structure.smiles, "Cn1c2c(=O)n(C)c(=O)n(C)c2nc1") + self.assertTrue(isinstance(obj.normalized_structure, PESStructure)) + self.assertEqual(obj.normalized_structure.pes_link, 'https://pesregapp-test.cropkey-np.ag/entities/PES-000126') + self.assertEqual(obj.normalized_structure.smiles, "CN1C(=O)C2=C(N=CN2C)N(C)C1=O") + + self.assertEqual(CompoundStructure.objects.count(), 2) + self.assertEqual(PESStructure.objects.count(), 2) + + + def test_pes_dedup(self): + _ = PESCompound.create( + self.user.default_package, + self.pes_dummy, + 'Test PES', + 'Test Description' + ) + + self.assertEqual(PESCompound.objects.count(), 1) + + _ = PESCompound.create( + self.user.default_package, + self.pes_dummy, + 'Test PES 2', + 'Test Description 2' + ) + + # Assert deduplication works we only have one object + self.assertEqual(PESCompound.objects.count(), 1) + + obj = PESCompound.objects.first() + + # Assert name and description remain + self.assertEqual(obj.name, 'Test PES') + self.assertEqual(obj.description, 'Test Description') + + + def test_add_pes_and_compound_with_same_representative(self): + pes = PESCompound.create( + self.user.default_package, + self.pes_dummy, + 'Test PES', + 'Test Description' + ) + + regular = Compound.create( + self.user.default_package, + "CN1C(=O)C2=C(N=CN2C)N(C)C1=O", # Already normalized + 'Test PES', + 'Test Description' + ) + + self.assertNotEqual(pes, regular) + self.assertTrue(isinstance(pes, PESCompound)) + self.assertTrue(isinstance(regular, Compound)) + self.assertEqual(Compound.objects.count(), 2) + self.assertEqual(PESCompound.objects.count(), 1) + + self.assertEqual(CompoundStructure.objects.count(), 3) + + + + + # def test_api_add_pes(self): + # + # + # def test_my_endpoint(): + # client = TestClient(router, auth=MockMSAuth()) + # response = client.get("/my-endpoint", headers={"Authorization": "Bearer mock-token"}) + # assert response.status_code == 200 diff --git a/bayer/urls.py b/bayer/urls.py new file mode 100644 index 00000000..5f64cb86 --- /dev/null +++ b/bayer/urls.py @@ -0,0 +1,19 @@ +from django.urls import re_path + +from . import views as v + +UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}" + +urlpatterns = [ + re_path(r"^depict_pes$", v.visualize_pes, name="depict_pes"), + re_path( + rf"^package/(?P{UUID})/pes$", + v.create_pes, + name="create pes", + ), + re_path( + rf"^package/(?P{UUID})/pathway/(?P{UUID})/pes$", + v.create_pes_node, + name="create pes node", + ), +] diff --git a/bayer/views.py b/bayer/views.py new file mode 100644 index 00000000..0f1d5671 --- /dev/null +++ b/bayer/views.py @@ -0,0 +1,155 @@ +import base64 + +import requests +from django.conf import settings as s +from django.core.exceptions import BadRequest +from django.http import HttpResponse +from django.shortcuts import redirect + +from bayer.models import PESCompound +from epdb.logic import PackageManager +from epdb.models import Pathway, Node +from epdb.views import _anonymous_or_real +from utilities.decorators import package_permission_required + +Package = s.GET_PACKAGE_MODEL() + + +@package_permission_required() +def create_pes(request, package_uuid): + current_user = _anonymous_or_real(request) + current_package = PackageManager.get_package_by_id(current_user, package_uuid) + + if request.method == "POST": + + if current_package.classification_level == Package.Classification.INTERNAL: + raise BadRequest("Cannot create PESs for internal packages.") + + compound_name = request.POST.get('compound-name') + compound_description = request.POST.get('compound-description') + pes_link = request.POST.get('pes-link') + + if pes_link: + try: + pes_data = fetch_pes(request, pes_link) + except ValueError as e: + return BadRequest(f"Could not fetch PES data for {pes_link}") + + classification = pes_data.get("classificationLevel", "") + if "secret" == classification.lower(): + data_pools = pes_data.get("dataPools") + if data_pools: + if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools: + return BadRequest( + f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data") + + pes = PESCompound.create(current_package, pes_data, compound_name, compound_description) + + return redirect(pes.url) + else: + return BadRequest("Please provide a PES link.") + else: + pass + + +@package_permission_required() +def create_pes_node(request, package_uuid, pathway_uuid): + current_user = _anonymous_or_real(request) + current_package = PackageManager.get_package_by_id(current_user, package_uuid) + current_pathway = Pathway.objects.get(package=current_package, uuid=pathway_uuid) + + if request.method == "POST": + + if current_package.classification_level == Package.Classification.INTERNAL: + raise BadRequest("Cannot create PESs for internal packages.") + + compound_name = request.POST.get('compound-name') + compound_description = request.POST.get('compound-description') + pes_link = request.POST.get('pes-link') + + if pes_link: + try: + pes_data = fetch_pes(request, pes_link) + except ValueError as e: + return BadRequest(f"Could not fetch PES data for {pes_link}") + + classification = pes_data.get("classificationLevel", "") + if "secret" == classification.lower(): + data_pools = pes_data.get("dataPools") + if data_pools: + if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools: + return BadRequest( + f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data") + + pes = PESCompound.create(current_package, pes_data, compound_name, compound_description) + + n = Node() + n.stereo_removed = False + n.pathway = current_pathway + n.depth = 0 + + n.default_node_label = pes.default_structure + n.save() + + n.node_labels.add(pes.default_structure) + n.save() + + return redirect(current_pathway.url) + + else: + return BadRequest("Please provide a PES link.") + else: + pass + + +def fetch_pes(request, pes_url) -> dict: + from epauth.views import get_access_token_from_request + token = get_access_token_from_request(request) + + if token: + for k, v in s.PES_API_MAPPING.items(): + if pes_url.startswith(k): + pes_id = pes_url.split('/')[-1] + + if pes_id == 'dummy': + import json + res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json")) + res_data["pes_url"] = pes_url + return res_data + else: + headers = {"Authorization": f"Bearer {token['access_token']}"} + params = {"pes_reg_entity_corporate_id": pes_id} + + res = requests.get(v, headers=headers, params=params, proxies=s.PROXIES or None) + + try: + res.raise_for_status() + pes_data = res.json() + + if len(pes_data) == 0: + raise ValueError(f"PES with id {pes_id} not found") + + res_data = pes_data[0] + res_data["pes_url"] = pes_url + return res_data + + except requests.exceptions.HTTPError as e: + raise ValueError(f"Error fetching PES with id {pes_id}: {e}") + else: + raise ValueError(f"Unknown URL {pes_url}") + else: + raise ValueError("Could not fetch access token from request.") + + +def visualize_pes(request): + pes_link = request.GET.get('pesLink') + + if pes_link: + pes_data = fetch_pes(request, pes_link) + + representations = pes_data.get('representations') + + for rep in representations: + if rep.get('type') == 'color': + image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", "")) + return HttpResponse(image_data, content_type="image/png") diff --git a/bb4g/__init__.py b/bb4g/__init__.py new file mode 100644 index 00000000..1b686c08 --- /dev/null +++ b/bb4g/__init__.py @@ -0,0 +1,183 @@ +import json +import math +from datetime import datetime +from typing import List +import enum +import requests +from django.conf import settings as s +from envipy_additional_information import EnviPyModel, UIConfig, WidgetType +from envipy_additional_information import register + +from bridge.contracts import Classifier # noqa: I001 +from bridge.dto import ( + BuildResult, + EnviPyDTO, + EvaluationResult, + RunResult, + TransformationProductPrediction, +) # noqa: I001 + +class SamplingAlgorithm(enum.Enum): + EXACT = "exact" + + +@register("bb4gconfig") +class BB4GConfig(EnviPyModel): + sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT + cutoff: int = -5 + + class UI: + title = "BB4G Configuration" + sampling_algorithm = UIConfig( + widget=WidgetType.SELECT, + label="BB4G Sampling Algorithm", + order=1, + placeholder="If unset defaults to 'exact'" + ) + cutoff = UIConfig( + widget=WidgetType.NUMBER, + label="BB4G Cutoff", + order=2, + placeholder="If unset defaults to -5" + ) + + +# Once stable these will be exposed by enviPy-plugins lib +class BB4G(Classifier): + Config = BB4GConfig + + def __init__(self, config: BB4GConfig | None = None): + super().__init__(config) + self.url = f"{s.BB4G_URL}" + + self.token = self.acquire_token() + self.header = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + } + + def acquire_token(self): + BB4G_TENANT_ID = s.BB4G_TENANT_ID + BB4G_CLIENT_ID = s.BB4G_CLIENT_ID + BB4G_CLIENT_SECRET = s.BB4G_CLIENT_SECRET + BB4G_SCOPE = s.BB4G_SCOPE + + BB4G_TOKEN_URL = f"https://login.microsoftonline.com/{BB4G_TENANT_ID}/oauth2/v2.0/token" + + payload = { + "client_id": BB4G_CLIENT_ID, + "client_secret": BB4G_CLIENT_SECRET, + "scope": BB4G_SCOPE, + "grant_type": "client_credentials" + } + + # No Proxy required, URL is whitelisted + res = requests.post(BB4G_TOKEN_URL, data=payload) + + res.raise_for_status() + + return res.json()["access_token"] + + def start(self): + header = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + } + + started = False + retries = 0 + while not started and retries < 5: + res = requests.post(f"{self.url}/start", headers=header, data={}, proxies=s.PROXIES or None) + + if res.status_code == 200: + started = True + elif res.status_code in [500, 502]: + retries += 1 + import time + time.sleep(5) + else: + raise ValueError(f"Unexpected status code: {res.status_code}") + + @classmethod + def requires_rule_packages(cls) -> bool: + return False + + @classmethod + def requires_data_packages(cls) -> bool: + return False + + @classmethod + def identifier(cls) -> str: + return "bb4g" + + @classmethod + def name(cls) -> str: + return "BB4G Template Free Model" + + @classmethod + def display(cls) -> str: + return "BB4G Template Free Model" + + def build(self, eP: EnviPyDTO, *args, **kwargs) -> BuildResult | None: + return + + def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult: + + # Ensure Service is running + self.start() + + smiles = [c.smiles for c in eP.get_compounds()] + preds = self._post(smiles) + + results = [] + + for substrate in preds.keys(): + results.append( + TransformationProductPrediction( + substrate=substrate, + products=preds[substrate], + ) + ) + + return RunResult( + producer=eP.get_context().url, + description=f"Generated at {datetime.now()}", + result=results, + ) + + def evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult: + pass + + def build_and_evaluate(self, eP: EnviPyDTO, *args, **kwargs) -> EvaluationResult: + pass + + def _post(self, smiles: List[str]) -> dict[str, dict[str, float]]: + header = { + "Authorization": f"Bearer {self.token}", + "Content-Type": "application/json", + } + + result = {} + + for smi in smiles: + data = { + "smiles": smi, + "sampling_alg": self.config.sampling_algorithm.value, + "cutoff": self.config.cutoff, + } + + resp = requests.post(f"{self.url}/compute", headers=header, data=json.dumps(data), proxies=s.PROXIES or None) + + resp.raise_for_status() + + for substrate, predictions in resp.json().items(): + preds = {} + + for pred in predictions: + prod = pred["prediction"] + prob = math.exp(pred["log_likelihood"]) + preds[prod] = prob + + result[substrate] = preds + + return result diff --git a/bridge/contracts.py b/bridge/contracts.py index 0e74e9c0..9a5f998b 100644 --- a/bridge/contracts.py +++ b/bridge/contracts.py @@ -254,7 +254,15 @@ class Classifier(Plugin): def parse_config(cls, data: dict | None = None) -> EnviPyModel | None: if cls.Config is None: return None - return cls.Config(**(data or {})) + + # remove empty strings a.k.a unset params to not overwrite defaults + cpy = {} + if data is not None: + for k, v in data.items(): + if v != "": + cpy[k] = v + + return cls.Config(**cpy) @classmethod def create(cls, data: dict | None = None): diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 0c0cdac6..ad4174fa 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -1,26 +1,54 @@ services: db: image: postgres:18 - container_name: envipath-postgres + container_name: eppostgres environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: envipath + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB} ports: - "5432:5432" volumes: - - postgres_data:/var/lib/postgresql + - ep_bayer_postgres_data:/var/lib/postgresql healthcheck: - test: ["CMD-SHELL", "pg_isready -U postgres"] + test: [ "CMD-SHELL", "pg_isready -U postgres" ] interval: 5s timeout: 5s retries: 5 redis: image: redis:7-alpine - container_name: envipath-redis + container_name: epredis ports: - "6379:6379" + volumes: + - ep_bayer_redis_data:/data + + biotransformer3: + image: envipath/biotransformer3:1.0 + container_name: epbiotransformer3 + +# web: +# image: envipath/envipy-bayer:1.0 +# container_name: epdjango +# ports: +# - "127.0.0.1:8000:8000" +# env_file: +# - .env +# command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3 +# volumes: +# - ep_bayer_data:/opt/enviPy/ + + celery_worker: + image: envipath/envipy-bayer:1.0 + container_name: epcelery + env_file: + - .env.dev + command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads + volumes: + - ep_bayer_data:/opt/enviPy/ volumes: - postgres_data: + ep_bayer_postgres_data: + ep_bayer_redis_data: + ep_bayer_data: diff --git a/docker-compose.yml b/docker-compose.yml index 8f2f8e7a..088a6e00 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,7 +7,7 @@ services: POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} POSTGRES_DB: ${POSTGRES_DB} volumes: - - ep_postgres_data:/var/lib/postgresql + - ep_bayer_postgres_data:/var/lib/postgresql healthcheck: test: [ "CMD-SHELL", "pg_isready -U postgres" ] interval: 5s @@ -18,14 +18,14 @@ services: image: redis:7-alpine container_name: epredis volumes: - - ep_redis_data:/data + - ep_bayer_redis_data:/data biotransformer3: image: envipath/biotransformer3:1.0 container_name: epbiotransformer3 web: - image: envipath/envipy:1.0 + image: envipath/envipy-bayer:1.0 container_name: epdjango ports: - "127.0.0.1:8000:8000" @@ -33,18 +33,18 @@ services: - .env command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3 volumes: - - ep_data:/opt/enviPy/ + - ep_bayer_data:/opt/enviPy/ celery_worker: - image: envipath/envipy:1.0 + image: envipath/envipy-bayer:1.0 container_name: epcelery env_file: - .env command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads volumes: - - ep_data:/opt/enviPy/ + - ep_bayer_data:/opt/enviPy/ volumes: - ep_postgres_data: - ep_redis_data: - ep_data: + ep_bayer_postgres_data: + ep_bayer_redis_data: + ep_bayer_data: diff --git a/envipath/settings.py b/envipath/settings.py index 29a7f694..318686f6 100644 --- a/envipath/settings.py +++ b/envipath/settings.py @@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/4.2/ref/settings/ """ - +import json import os from pathlib import Path @@ -20,7 +20,7 @@ from sklearn.tree import DecisionTreeClassifier # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent -ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env") +ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env.dev") print(f"Loading env from {ENV_PATH}") load_dotenv(ENV_PATH, override=False) @@ -143,6 +143,12 @@ if os.environ.get("USE_TEMPLATE_DB", False) == "True": "TEMPLATE": os.environ["TEMPLATE_DB"], } +CACHES = { + "default": { + "BACKEND": "django.core.cache.backends.locmem.LocMemCache", + "LOCATION": "unique-snowflake", + } +} # Password validation # https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators @@ -442,3 +448,46 @@ BIOTRANSFORMER_ENABLED = os.environ.get("BIOTRANSFORMER_ENABLED", "False") == "T FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED if BIOTRANSFORMER_ENABLED: BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None) + +# PES +PES_API_MAPPING = os.environ.get("PES_API_MAPPING", None) +if PES_API_MAPPING: + import json + PES_API_MAPPING = json.loads(PES_API_MAPPING) +else: + PES_API_MAPPING = {} + +# Entra Groups +ENTRA_GROUPS = os.environ.get("ENTRA_GROUPS", None) +if ENTRA_GROUPS: + import json + ENTRA_GROUPS = json.loads(ENTRA_GROUPS) +else: + ENTRA_GROUPS = {} + +ENTRA_SECRET_GROUPS = os.environ.get("ENTRA_SECRET_GROUPS", None) +if ENTRA_SECRET_GROUPS: + import json + ENTRA_SECRET_GROUPS = json.loads(ENTRA_SECRET_GROUPS) +else: + ENTRA_SECRET_GROUPS = {} + +# PES Data Pools vs Entra Mapping +DATA_POOL_MAPPING = os.environ.get("DATA_POOL_MAPPING", None) +if DATA_POOL_MAPPING: + import json + DATA_POOL_MAPPING = json.loads(DATA_POOL_MAPPING) +else: + DATA_POOL_MAPPING = {} + +PROXIES = {} +if os.environ.get("HTTP_PROXY"): + PROXIES["http"] = os.environ.get("HTTP_PROXY") + PROXIES["https"] = os.environ.get("HTTPS_PROXY") + +# BB4g +BB4G_URL = os.environ.get("BB4G_URL") +BB4G_TENANT_ID = os.environ.get("BB4G_TENANT_ID") +BB4G_CLIENT_ID = os.environ.get("BB4G_CLIENT_ID") +BB4G_CLIENT_SECRET = os.environ.get("BB4G_CLIENT_SECRET") +BB4G_SCOPE = os.environ.get("BB4G_SCOPE") diff --git a/epauth/urls.py b/epauth/urls.py index c251d799..d777c9f1 100644 --- a/epauth/urls.py +++ b/epauth/urls.py @@ -5,4 +5,5 @@ from . import views urlpatterns = [ path("entra/login/", views.entra_login, name="entra_login"), path("auth/redirect/", views.entra_callback, name="entra_callback"), + path("auth/token/", views.get_token, name="get_token"), ] diff --git a/epauth/views.py b/epauth/views.py index 66b922c6..d0243f7d 100644 --- a/epauth/views.py +++ b/epauth/views.py @@ -2,9 +2,11 @@ import msal from django.conf import settings as s from django.contrib.auth import get_user_model from django.contrib.auth import login +from django.http import HttpResponse from django.shortcuts import redirect -from epdb.logic import UserManager +from epdb.logic import UserManager, GroupManager +from epdb.models import Group def get_msal_app_with_cache(request): @@ -80,6 +82,33 @@ def entra_callback(request): login(request, u) + # EDIT START + + # Ensure groups exists in eP + for id, name in s.ENTRA_SECRET_GROUPS.items(): + if not Group.objects.filter(uuid=id).exists(): + g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", + uuid=id) + else: + g = Group.objects.get(uuid=id) + # Ensure its secret + g.secret = True + g.save() + + for id, name in s.ENTRA_GROUPS.items(): + if not Group.objects.filter(uuid=id).exists(): + g = GroupManager.create_group(User.objects.get(username="admin"), name, f"Synced Entra Group {name} ", + uuid=id) + else: + g = Group.objects.get(uuid=id) + + for group_uuid in claims.get("groups", []): + if Group.objects.filter(uuid=group_uuid).exists(): + g = Group.objects.get(uuid=group_uuid) + g.user_member.add(u) + + # EDIT END + return redirect(s.SERVER_URL) # Handle errors @@ -87,6 +116,11 @@ def get_access_token_from_request(request, scopes=None): """ Get an access token from the request using MSAL token cache. """ + + # Check if auth via Access Token + if request.headers.get("Authorization"): + return {"access_token": request.headers.get("Authorization").split(" ")[1]} + if scopes is None: scopes = s.MS_ENTRA_SCOPES @@ -128,3 +162,9 @@ def get_access_token_from_request(request, scopes=None): return result return None + + +def get_token(request): + token = get_access_token_from_request(request) + msg = f"{token}" + return HttpResponse(msg, content_type='text/plain') diff --git a/epdb/legacy_api.py b/epdb/legacy_api.py index b340bd21..a8e62d7a 100644 --- a/epdb/legacy_api.py +++ b/epdb/legacy_api.py @@ -1,17 +1,20 @@ from collections import defaultdict from typing import Any, Dict, List, Optional +import jwt import nh3 +import requests from django.conf import settings as s from django.contrib.auth import get_user_model +from django.core.cache import cache from django.http import HttpResponse, JsonResponse from django.shortcuts import redirect +from jwt import InvalidIssuerError from ninja import Field, Form, Query, Router, Schema -from ninja.security import SessionAuth +from ninja.security import HttpBearer from utilities.chem import FormatConverter from utilities.misc import PackageExporter - from .logic import ( EPDBURLParser, GroupManager, @@ -46,6 +49,26 @@ from .models import ( Package = s.GET_PACKAGE_MODEL() +def get_cached_jwks(tenant_id: str, force=False) -> Dict: + """Get JWKS using Django cache""" + cache_key = f"jwks_{tenant_id}" + + jwks = cache.get(cache_key) + + if jwks is None or force: + # Cache miss, fetch new keys + jwks_uri = f"https://login.microsoftonline.com/{tenant_id}/discovery/v2.0/keys" + response = requests.get(jwks_uri) + response.raise_for_status() + + jwks = response.json() + + # Cache for 1 hour (3600 seconds) + cache.set(cache_key, jwks, 3600) + + return jwks + + def get_package_for_write(user, package_uuid): p = PackageManager.get_package_by_id(user, package_uuid) if not PackageManager.writable(user, p): @@ -59,7 +82,52 @@ def _anonymous_or_real(request): return get_user_model().objects.get(username="anonymous") -router = Router(auth=SessionAuth(csrf=False)) +def validate_token(token: str) -> dict: + TENANT_ID = s.MS_ENTRA_TENANT_ID + CLIENT_ID = s.MS_ENTRA_CLIENT_ID + + jwks = get_cached_jwks(TENANT_ID) + + header = jwt.get_unverified_header(token) + + public_key = jwt.algorithms.RSAAlgorithm.from_jwk( + next(k for k in jwks["keys"] if k["kid"] == header["kid"]) + ) + + # Handle V1 and V2 tokens + try: + claims = jwt.decode( + token, + public_key, + algorithms=["RS256"], + audience=[CLIENT_ID, f"api://{CLIENT_ID}"], + issuer=[ + f"https://sts.windows.net/{TENANT_ID}/", + f"https://login.microsoftonline.com/{TENANT_ID}/v2.0" + ] + ) + except Exception as e: + raise ValueError(f"Token verification failed! - {e}") + + return claims + + +class MSBearerTokenAuth(HttpBearer): + + def authenticate(self, request, token): + if token is None: + return None + + claims = validate_token(token) + + if not User.objects.filter(uuid=claims['oid']).exists(): + return None + + request.user = User.objects.get(uuid=claims['oid']) + return request.user + + +router = Router(auth=MSBearerTokenAuth()) class Error(Schema): @@ -153,21 +221,6 @@ class SimpleModel(SimpleObject): identifier: str = "relative-reasoning" -################ -# Login/Logout # -################ -@router.post("/", response={200: SimpleUser, 403: Error}, auth=None) -def login(request, loginusername: Form[str], loginpassword: Form[str]): - from django.contrib.auth import authenticate, login - - email = User.objects.get(username=loginusername).email - user = authenticate(username=email, password=loginpassword) - if user: - login(request, user) - return user - else: - return 403, {"message": "Invalid username and/or password"} - ######## # User # @@ -767,6 +820,7 @@ class CreateCompound(Schema): compoundName: str | None = None compoundDescription: str | None = None inchi: str | None = None + pesLink: str | None = None @router.post("/package/{uuid:package_uuid}/compound") @@ -778,9 +832,28 @@ def create_package_compound( try: p = get_package_for_write(request.user, package_uuid) # inchi is not used atm - c = Compound.create( - p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi - ) + + if c.pesLink is not None: + from bayer.views import fetch_pes + from bayer.models import PESCompound + + try: + pes_data = fetch_pes(request, c.pesLink) + except ValueError as e: + return 400, {"message": f"Could not fetch PES data for {c.pesLink}"} + + classification = pes_data.get("classificationLevel", "") + if "secret" == classification.lower(): + data_pools = pes_data.get("dataPools") + if data_pools: + if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools: + return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"} + + c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription) + else: + c = Compound.create( + p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi + ) return redirect(c.url) except ValueError as e: return 400, {"message": str(e)} @@ -1801,6 +1874,7 @@ class CreateNode(Schema): nodeName: str | None = None nodeReason: str | None = None nodeDepth: str | None = None + pesLink: str | None = None @router.post( @@ -1812,14 +1886,43 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]): p = get_package_for_write(request.user, package_uuid) pw = Pathway.objects.get(package=p, uuid=pathway_uuid) - if n.nodeDepth is not None and n.nodeDepth.strip() != "": - node_depth = int(n.nodeDepth) + if n.pesLink: + from bayer.views import fetch_pes + from bayer.models import PESCompound + + try: + pes_data = fetch_pes(request, c.pesLink) + except ValueError as e: + return 400, {"message": f"Could not fetch PES data for {c.pesLink}"} + + classification = pes_data.get("classificationLevel", "") + if "secret" == classification.lower(): + data_pools = pes_data.get("dataPools") + if data_pools: + if s.DATA_POOL_MAPPING[p.data_pool.name] not in data_pools: + return 400, { "messsage": f"PES data pool {s.DATA_POOL_MAPPING[p.data_pool.name]} not found in PES data"} + + c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription) + + node = Node() + node.stereo_removed = False + node.pathway = pw + node.depth = 0 + + node.default_node_label = c.default_structure + node.save() + + node.node_labels.add(c.default_structure) + node.save() else: - node_depth = -1 + if n.nodeDepth is not None and n.nodeDepth.strip() != "": + node_depth = int(n.nodeDepth) + else: + node_depth = -1 - n = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason) + node = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason) - return redirect(n.url) + return redirect(node.url) except ValueError: return 403, {"message": "Adding node failed!"} @@ -1929,13 +2032,16 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]): educts = [] products = [] + subclasses = CompoundStructure.__subclasses__() + if e.edgeAsSmirks: for ed in e.edgeAsSmirks.split(">>")[0].split("\\."): stand_ed = FormatConverter.standardize(ed, remove_stereo=True) educts.append( Node.objects.get( pathway=pw, - default_node_label=CompoundStructure.objects.get( + default_node_label=CompoundStructure.objects.not_instance_of(*subclasses). + get( compound__package=p, smiles=stand_ed ).compound.default_structure, ) @@ -1946,7 +2052,8 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]): products.append( Node.objects.get( pathway=pw, - default_node_label=CompoundStructure.objects.get( + default_node_label=CompoundStructure.objects.not_instance_of(*subclasses). + get( compound__package=p, smiles=stand_pr ).compound.default_structure, ) diff --git a/epdb/logic.py b/epdb/logic.py index 6de2b73a..d73766d2 100644 --- a/epdb/logic.py +++ b/epdb/logic.py @@ -7,6 +7,7 @@ import nh3 from django.conf import settings as s from django.contrib.auth import get_user_model from django.db import transaction +from django.db.models import QuerySet from pydantic import ValidationError from epdb.models import ( @@ -364,6 +365,14 @@ class PackageManager(object): groups = GroupManager.get_groups(user) + # EDIT START + + if package.classification_level == Package.Classification.SECRET: + if package.data_pool not in groups: + return False + + # EDIT END + perms = {"all": ["all"], "write": ["all", "write"], "read": ["all", "write", "read"]} valid_perms = perms.get(permission) @@ -406,6 +415,7 @@ class PackageManager(object): try: p = Package.objects.get(uuid=package_id) if PackageManager.readable(user, p): + p = PackageManager.check_package_classification(user, p) return p else: # FIXME: use custom exception to be translatable to 403 in API @@ -415,6 +425,37 @@ class PackageManager(object): except Package.DoesNotExist: raise ValueError("Package with ID {} does not exist!".format(package_id)) + # EDIT START + + @staticmethod + def check_package_classification(user, pack: Package): + if pack.classification_level == Package.Classification.SECRET: + if pack.data_pool.user_member.filter(id=user.id).exists(): + return pack + + raise ValueError("Package is secret and not accessible to user!") + + else: + return pack + + + @staticmethod + def check_package_classifications(user, package_qs: QuerySet[Package]): + non_secret = package_qs.exclude(classification_level=Package.Classification.SECRET) + secret = package_qs.filter(classification_level=Package.Classification.SECRET) + + # TODO we should be able to do via the db + accessible_secret = [] + + for s_package in secret: + if s_package.data_pool.user_member.filter(id=user.id).exists(): + accessible_secret.append(s_package.pk) + + # Cannot combine a unique query with a non-unique query -> we have to call distinct + return Package.objects.filter(pk__in=accessible_secret).distinct() | non_secret.distinct() + + # EDIT END + @staticmethod def get_all_readable_packages(user, include_reviewed=False): # UserPermission only exists if at least read is granted... @@ -441,6 +482,10 @@ class PackageManager(object): qs = qs.distinct() + # EDIT START + qs = PackageManager.check_package_classifications(user, qs) + # EDIT END + return qs @staticmethod @@ -487,11 +532,11 @@ class PackageManager(object): qs = qs.distinct() - return qs + # EDIT START + qs = PackageManager.check_package_classifications(user, qs) + # EDIT END - @staticmethod - def get_packages(): - return Package.objects.all() + return qs @staticmethod @transaction.atomic @@ -596,6 +641,25 @@ class PackageManager(object): else: pack.reviewed = False + # EDIT START + if data.get("classification"): + if data["classification"] == "INTERNAL": + pack.classification = Package.Classification.RESTRICTED + elif data["classification"] == "RESTRICTED": + pack.classification = Package.Classification.RESTRICTED + elif data["classification"] == "SECRET": + pack.classification = Package.Classification.SECRET + + if not "datapool" in data: + raise ValueError("Missing datapool in package") + + g = Group.objects.get(uuid=data["datapool"].split('/')[-1]) + pack.data_pool = g + else: + raise ValueError(f"Invalid classification {data['classification']}") + + # EDIT END + pack.description = data["description"] pack.save() @@ -681,7 +745,13 @@ class PackageManager(object): default_structure = None for structure in compound["structures"]: - struc = CompoundStructure() + if structure.get("pesLink"): + from bayer.models import PESStructure + struc = PESStructure() + struc.pes_link = structure["pesLink"] + else: + struc = CompoundStructure() + # struc.object_url = Command.get_id(structure, keep_ids) struc.compound = comp struc.uuid = UUID(structure["id"].split("/")[-1]) if keep_ids else uuid4() diff --git a/epdb/migrations/0001_initial.py b/epdb/migrations/0001_initial.py deleted file mode 100644 index e6cc4f69..00000000 --- a/epdb/migrations/0001_initial.py +++ /dev/null @@ -1,594 +0,0 @@ -# Generated by Django 5.2.1 on 2025-07-22 20:58 - -import datetime -import django.contrib.auth.models -import django.contrib.auth.validators -import django.contrib.postgres.fields -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('contenttypes', '0002_remove_content_type_name'), - ] - - operations = [ - migrations.CreateModel( - name='Compound', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), - ], - ), - migrations.CreateModel( - name='EPModel', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - ), - migrations.CreateModel( - name='Permission', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)), - ], - ), - migrations.CreateModel( - name='License', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('link', models.URLField(verbose_name='link')), - ('image_link', models.URLField(verbose_name='Image link')), - ], - ), - migrations.CreateModel( - name='Rule', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - ), - migrations.CreateModel( - name='User', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('email', models.EmailField(max_length=254, unique=True)), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='APIToken', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('hashed_key', models.CharField(max_length=128, unique=True)), - ('created', models.DateTimeField(auto_now_add=True)), - ('expires_at', models.DateTimeField(blank=True, default=datetime.datetime(2025, 10, 20, 20, 58, 48, 351675, tzinfo=datetime.timezone.utc), null=True)), - ('name', models.CharField(blank=True, help_text='Optional name for the token', max_length=100)), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.CreateModel( - name='CompoundStructure', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), - ('smiles', models.TextField(verbose_name='SMILES')), - ('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')), - ('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')), - ('normalized_structure', models.BooleanField(default=False)), - ('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='compound', - name='default_structure', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'), - ), - migrations.CreateModel( - name='Edge', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), - ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - ), - migrations.CreateModel( - name='EnviFormer', - fields=[ - ('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')), - ('threshold', models.FloatField(default=0.5)), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.epmodel',), - ), - migrations.CreateModel( - name='PluginModel', - fields=[ - ('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.epmodel',), - ), - migrations.CreateModel( - name='RuleBaseRelativeReasoning', - fields=[ - ('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.epmodel',), - ), - migrations.CreateModel( - name='Group', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(verbose_name='Group name')), - ('public', models.BooleanField(default=False, verbose_name='Public Group')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('group_member', models.ManyToManyField(related_name='groups_in_group', to='epdb.group', verbose_name='Group member')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')), - ('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='user', - name='default_group', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'), - ), - migrations.CreateModel( - name='Node', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), - ('depth', models.IntegerField(verbose_name='Node depth')), - ('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')), - ('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')), - ('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='edge', - name='end_nodes', - field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'), - ), - migrations.AddField( - model_name='edge', - name='start_nodes', - field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'), - ), - migrations.CreateModel( - name='Package', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')), - ('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='epmodel', - name='package', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'), - ), - migrations.AddField( - model_name='compound', - name='package', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'), - ), - migrations.AddField( - model_name='user', - name='default_package', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.package', verbose_name='Default Package'), - ), - migrations.CreateModel( - name='SequentialRule', - fields=[ - ('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.rule',), - ), - migrations.CreateModel( - name='SimpleRule', - fields=[ - ('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.rule',), - ), - migrations.AddField( - model_name='rule', - name='package', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'), - ), - migrations.AddField( - model_name='rule', - name='polymorphic_ctype', - field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'), - ), - migrations.CreateModel( - name='Pathway', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='node', - name='pathway', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'), - ), - migrations.AddField( - model_name='edge', - name='pathway', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'), - ), - migrations.CreateModel( - name='Reaction', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), - ('multi_step', models.BooleanField(verbose_name='Multistep Reaction')), - ('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')), - ('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')), - ('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')), - ('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='edge', - name='edge_label', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'), - ), - migrations.CreateModel( - name='Scenario', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('scenario_date', models.CharField(default='No date', max_length=256)), - ('scenario_type', models.CharField(default='Not specified', max_length=256)), - ('additional_information', models.JSONField(verbose_name='Additional Information')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')), - ('parent', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.scenario')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='rule', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='reaction', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='pathway', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='node', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='edge', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='compoundstructure', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='compound', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.CreateModel( - name='Setting', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('public', models.BooleanField(default=False)), - ('global_default', models.BooleanField(default=False)), - ('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')), - ('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')), - ('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')), - ('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')), - ('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to='epdb.package', verbose_name='Setting Rule Packages')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='pathway', - name='setting', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'), - ), - migrations.AddField( - model_name='user', - name='default_setting', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'), - ), - migrations.CreateModel( - name='MLRelativeReasoning', - fields=[ - ('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')), - ('threshold', models.FloatField(default=0.5)), - ('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')), - ('eval_results', models.JSONField(blank=True, default=dict, null=True)), - ('data_packages', models.ManyToManyField(related_name='data_packages', to='epdb.package', verbose_name='Data Packages')), - ('eval_packages', models.ManyToManyField(related_name='eval_packages', to='epdb.package', verbose_name='Evaluation Packages')), - ('rule_packages', models.ManyToManyField(related_name='rule_packages', to='epdb.package', verbose_name='Rule Packages')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.epmodel',), - ), - migrations.CreateModel( - name='ApplicabilityDomain', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('num_neighbours', models.FloatField(default=5)), - ('reliability_threshold', models.FloatField(default=0.5)), - ('local_compatibilty_threshold', models.FloatField(default=0.5)), - ('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning')), - ], - options={ - 'abstract': False, - }, - ), - migrations.CreateModel( - name='SimpleAmbitRule', - fields=[ - ('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')), - ('smirks', models.TextField(verbose_name='SMIRKS')), - ('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')), - ('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.simplerule',), - ), - migrations.CreateModel( - name='SimpleRDKitRule', - fields=[ - ('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')), - ('reaction_smarts', models.TextField(verbose_name='SMIRKS')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.simplerule',), - ), - migrations.CreateModel( - name='SequentialRuleOrdering', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order_index', models.IntegerField()), - ('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')), - ('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')), - ], - ), - migrations.AddField( - model_name='sequentialrule', - name='simple_rules', - field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'), - ), - migrations.CreateModel( - name='ParallelRule', - fields=[ - ('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')), - ('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.rule',), - ), - migrations.AlterUniqueTogether( - name='compound', - unique_together={('uuid', 'package')}, - ), - migrations.CreateModel( - name='GroupPackagePermission', - fields=[ - ('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')), - ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')), - ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')), - ], - options={ - 'unique_together': {('package', 'group')}, - }, - bases=('epdb.permission',), - ), - migrations.CreateModel( - name='UserPackagePermission', - fields=[ - ('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')), - ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')), - ], - options={ - 'unique_together': {('package', 'user')}, - }, - bases=('epdb.permission',), - ), - migrations.CreateModel( - name='UserSettingPermission', - fields=[ - ('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')), - ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')), - ('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')), - ], - options={ - 'unique_together': {('setting', 'user')}, - }, - bases=('epdb.permission',), - ), - ] diff --git a/epdb/migrations/0001_squashed_0003_applicabilitydomain_url_compound_url_and_more.py b/epdb/migrations/0001_squashed_0003_applicabilitydomain_url_compound_url_and_more.py deleted file mode 100644 index a6eb8f50..00000000 --- a/epdb/migrations/0001_squashed_0003_applicabilitydomain_url_compound_url_and_more.py +++ /dev/null @@ -1,1020 +0,0 @@ -# Generated by Django 5.2.1 on 2025-08-26 18:11 - -import django.contrib.auth.models -import django.contrib.auth.validators -import django.contrib.postgres.fields -import django.db.migrations.operations.special -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import uuid -from django.conf import settings -from django.db import migrations, models - - -def populate_url(apps, schema_editor): - MODELS = [ - 'User', - 'Group', - 'Package', - 'Compound', - 'CompoundStructure', - 'Pathway', - 'Edge', - 'Node', - 'Reaction', - 'SimpleAmbitRule', - 'SimpleRDKitRule', - 'ParallelRule', - 'SequentialRule', - 'Scenario', - 'Setting', - 'MLRelativeReasoning', - 'EnviFormer', - 'ApplicabilityDomain', - ] - for model in MODELS: - obj_cls = apps.get_model("epdb", model) - for obj in obj_cls.objects.all(): - obj.url = assemble_url(obj) - if obj.url is None: - raise ValueError(f"Could not assemble url for {obj}") - obj.save() - - -def assemble_url(obj): - from django.conf import settings as s - match obj.__class__.__name__: - case 'User': - return '{}/user/{}'.format(s.SERVER_URL, obj.uuid) - case 'Group': - return '{}/group/{}'.format(s.SERVER_URL, obj.uuid) - case 'Package': - return '{}/package/{}'.format(s.SERVER_URL, obj.uuid) - case 'Compound': - return '{}/compound/{}'.format(obj.package.url, obj.uuid) - case 'CompoundStructure': - return '{}/structure/{}'.format(obj.compound.url, obj.uuid) - case 'SimpleAmbitRule': - return '{}/simple-ambit-rule/{}'.format(obj.package.url, obj.uuid) - case 'SimpleRDKitRule': - return '{}/simple-rdkit-rule/{}'.format(obj.package.url, obj.uuid) - case 'ParallelRule': - return '{}/parallel-rule/{}'.format(obj.package.url, obj.uuid) - case 'SequentialRule': - return '{}/sequential-rule/{}'.format(obj.compound.url, obj.uuid) - case 'Reaction': - return '{}/reaction/{}'.format(obj.package.url, obj.uuid) - case 'Pathway': - return '{}/pathway/{}'.format(obj.package.url, obj.uuid) - case 'Node': - return '{}/node/{}'.format(obj.pathway.url, obj.uuid) - case 'Edge': - return '{}/edge/{}'.format(obj.pathway.url, obj.uuid) - case 'MLRelativeReasoning': - return '{}/model/{}'.format(obj.package.url, obj.uuid) - case 'EnviFormer': - return '{}/model/{}'.format(obj.package.url, obj.uuid) - case 'ApplicabilityDomain': - return '{}/model/{}/applicability-domain/{}'.format(obj.model.package.url, obj.model.uuid, obj.uuid) - case 'Scenario': - return '{}/scenario/{}'.format(obj.package.url, obj.uuid) - case 'Setting': - return '{}/setting/{}'.format(s.SERVER_URL, obj.uuid) - case _: - raise ValueError(f"Unknown model {obj.__class__.__name__}") - - -class Migration(migrations.Migration): - replaces = [('epdb', '0001_initial'), ('epdb', '0002_externaldatabase_alter_apitoken_options_and_more'), - ('epdb', '0003_applicabilitydomain_url_compound_url_and_more')] - - initial = True - - dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), - ('contenttypes', '0002_remove_content_type_name'), - ] - - operations = [ - migrations.CreateModel( - name='Compound', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', - django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, - verbose_name='Aliases')), - ], - ), - migrations.CreateModel( - name='EPModel', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('polymorphic_ctype', - models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, - related_name='polymorphic_%(app_label)s.%(class)s_set+', - to='contenttypes.contenttype')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - ), - migrations.CreateModel( - name='Permission', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('permission', - models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)), - ], - ), - migrations.CreateModel( - name='License', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('link', models.URLField(verbose_name='link')), - ('image_link', models.URLField(verbose_name='Image link')), - ], - ), - migrations.CreateModel( - name='Rule', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', - django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, - verbose_name='Aliases')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - ), - migrations.CreateModel( - name='User', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('password', models.CharField(max_length=128, verbose_name='password')), - ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), - ('is_superuser', models.BooleanField(default=False, - help_text='Designates that this user has all permissions without explicitly assigning them.', - verbose_name='superuser status')), - ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, - help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', - max_length=150, unique=True, - validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], - verbose_name='username')), - ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), - ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), - ('is_staff', models.BooleanField(default=False, - help_text='Designates whether the user can log into this admin site.', - verbose_name='staff status')), - ('is_active', models.BooleanField(default=True, - help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', - verbose_name='active')), - ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), - ('email', models.EmailField(max_length=254, unique=True)), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('groups', models.ManyToManyField(blank=True, - help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', - related_name='user_set', related_query_name='user', to='auth.group', - verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', - related_name='user_set', related_query_name='user', - to='auth.permission', verbose_name='user permissions')), - ], - options={ - 'verbose_name': 'user', - 'verbose_name_plural': 'users', - 'abstract': False, - }, - managers=[ - ('objects', django.contrib.auth.models.UserManager()), - ], - ), - migrations.CreateModel( - name='CompoundStructure', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', - django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, - verbose_name='Aliases')), - ('smiles', models.TextField(verbose_name='SMILES')), - ('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')), - ('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')), - ('normalized_structure', models.BooleanField(default=False)), - ('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='compound', - name='default_structure', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, - related_name='compound_default_structure', to='epdb.compoundstructure', - verbose_name='Default Structure'), - ), - migrations.CreateModel( - name='Edge', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', - django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, - verbose_name='Aliases')), - ('polymorphic_ctype', - models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, - related_name='polymorphic_%(app_label)s.%(class)s_set+', - to='contenttypes.contenttype')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - ), - migrations.CreateModel( - name='EnviFormer', - fields=[ - ('epmodel_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='epdb.epmodel')), - ('threshold', models.FloatField(default=0.5)), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.epmodel',), - ), - migrations.CreateModel( - name='PluginModel', - fields=[ - ('epmodel_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='epdb.epmodel')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.epmodel',), - ), - migrations.CreateModel( - name='RuleBaseRelativeReasoning', - fields=[ - ('epmodel_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='epdb.epmodel')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.epmodel',), - ), - migrations.CreateModel( - name='Group', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(verbose_name='Group name')), - ('public', models.BooleanField(default=False, verbose_name='Public Group')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('group_member', - models.ManyToManyField(related_name='groups_in_group', to='epdb.group', verbose_name='Group member')), - ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, - verbose_name='Group Owner')), - ('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, - verbose_name='User members')), - ('url', models.TextField(null=True, verbose_name='URL')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='user', - name='default_group', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, - related_name='default_group', to='epdb.group', verbose_name='Default Group'), - ), - migrations.CreateModel( - name='Node', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', - django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, - verbose_name='Aliases')), - ('depth', models.IntegerField(verbose_name='Node depth')), - ('default_node_label', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', - to='epdb.compoundstructure', verbose_name='Default Node Label')), - ('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', - verbose_name='All Node Labels')), - ('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='edge', - name='end_nodes', - field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'), - ), - migrations.AddField( - model_name='edge', - name='start_nodes', - field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'), - ), - migrations.CreateModel( - name='Package', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')), - ('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - to='epdb.license', verbose_name='License')), - ('url', models.TextField(null=True, verbose_name='URL')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='epmodel', - name='package', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', - verbose_name='Package'), - ), - migrations.AddField( - model_name='compound', - name='package', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', - verbose_name='Package'), - ), - migrations.AddField( - model_name='user', - name='default_package', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.package', - verbose_name='Default Package'), - ), - migrations.CreateModel( - name='SequentialRule', - fields=[ - ('rule_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='epdb.rule')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.rule',), - ), - migrations.CreateModel( - name='SimpleRule', - fields=[ - ('rule_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='epdb.rule')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.rule',), - ), - migrations.AddField( - model_name='rule', - name='package', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', - verbose_name='Package'), - ), - migrations.AddField( - model_name='rule', - name='polymorphic_ctype', - field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, - related_name='polymorphic_%(app_label)s.%(class)s_set+', - to='contenttypes.contenttype'), - ), - migrations.CreateModel( - name='Pathway', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', - django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, - verbose_name='Aliases')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', - verbose_name='Package')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='node', - name='pathway', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', - verbose_name='belongs to'), - ), - migrations.AddField( - model_name='edge', - name='pathway', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', - verbose_name='belongs to'), - ), - migrations.CreateModel( - name='Reaction', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('aliases', - django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, - verbose_name='Aliases')), - ('multi_step', models.BooleanField(verbose_name='Multistep Reaction')), - ('medline_references', - django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, - verbose_name='Medline References')), - ('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', - verbose_name='Educts')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', - verbose_name='Package')), - ('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', - verbose_name='Products')), - ('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='edge', - name='edge_label', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', - verbose_name='Edge label'), - ), - migrations.CreateModel( - name='Scenario', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('scenario_date', models.CharField(default='No date', max_length=256)), - ('scenario_type', models.CharField(default='Not specified', max_length=256)), - ('additional_information', models.JSONField(verbose_name='Additional Information')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', - verbose_name='Package')), - ('parent', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, - to='epdb.scenario')), - ('url', models.TextField(null=True, verbose_name='URL')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='rule', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='reaction', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='pathway', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='node', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='edge', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='compoundstructure', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.AddField( - model_name='compound', - name='scenarios', - field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), - ), - migrations.CreateModel( - name='Setting', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('public', models.BooleanField(default=False)), - ('global_default', models.BooleanField(default=False)), - ('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')), - ('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')), - ('model_threshold', - models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')), - ('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, - to='epdb.epmodel', verbose_name='Setting EPModel')), - ('rule_packages', - models.ManyToManyField(blank=True, related_name='setting_rule_packages', to='epdb.package', - verbose_name='Setting Rule Packages')), - ('url', models.TextField(null=True, verbose_name='URL')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='pathway', - name='setting', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, - to='epdb.setting', verbose_name='Setting'), - ), - migrations.AddField( - model_name='user', - name='default_setting', - field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', - verbose_name='The users default settings'), - ), - migrations.CreateModel( - name='MLRelativeReasoning', - fields=[ - ('epmodel_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='epdb.epmodel')), - ('threshold', models.FloatField(default=0.5)), - ('model_status', models.CharField( - choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), - ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', - 'Model is built and can be used for predictions, Model is not evaluated yet.'), - ('EVALUATING', 'Model is evaluating'), - ('FINISHED', 'Model has finished building and evaluation.'), - ('ERROR', 'Model has failed.')], default='INITIAL')), - ('eval_results', models.JSONField(blank=True, default=dict, null=True)), - ('data_packages', - models.ManyToManyField(related_name='data_packages', to='epdb.package', verbose_name='Data Packages')), - ('eval_packages', models.ManyToManyField(related_name='eval_packages', to='epdb.package', - verbose_name='Evaluation Packages')), - ('rule_packages', - models.ManyToManyField(related_name='rule_packages', to='epdb.package', verbose_name='Rule Packages')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.epmodel',), - ), - migrations.CreateModel( - name='SimpleAmbitRule', - fields=[ - ('simplerule_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='epdb.simplerule')), - ('smirks', models.TextField(verbose_name='SMIRKS')), - ('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')), - ('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.simplerule',), - ), - migrations.CreateModel( - name='SimpleRDKitRule', - fields=[ - ('simplerule_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='epdb.simplerule')), - ('reaction_smarts', models.TextField(verbose_name='SMIRKS')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.simplerule',), - ), - migrations.CreateModel( - name='SequentialRuleOrdering', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('order_index', models.IntegerField()), - ('sequential_rule', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')), - ('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')), - ], - ), - migrations.AddField( - model_name='sequentialrule', - name='simple_rules', - field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', - verbose_name='Simple rules'), - ), - migrations.CreateModel( - name='ParallelRule', - fields=[ - ('rule_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - primary_key=True, serialize=False, to='epdb.rule')), - ('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')), - ], - options={ - 'abstract': False, - 'base_manager_name': 'objects', - }, - bases=('epdb.rule',), - ), - migrations.AlterUniqueTogether( - name='compound', - unique_together={('uuid', 'package')}, - ), - migrations.CreateModel( - name='GroupPackagePermission', - fields=[ - ('permission_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - to='epdb.permission')), - ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, - verbose_name='UUID of this object')), - ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', - verbose_name='Permission to')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', - verbose_name='Permission on')), - ], - options={ - 'unique_together': {('package', 'group')}, - }, - bases=('epdb.permission',), - ), - migrations.CreateModel( - name='UserPackagePermission', - fields=[ - ('permission_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - to='epdb.permission')), - ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, - verbose_name='UUID of this object')), - ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', - verbose_name='Permission on')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, - verbose_name='Permission to')), - ], - options={ - 'unique_together': {('package', 'user')}, - }, - bases=('epdb.permission',), - ), - migrations.CreateModel( - name='UserSettingPermission', - fields=[ - ('permission_ptr', - models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, - to='epdb.permission')), - ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, - verbose_name='UUID of this object')), - ('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', - verbose_name='Permission on')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, - verbose_name='Permission to')), - ], - options={ - 'unique_together': {('setting', 'user')}, - }, - bases=('epdb.permission',), - ), - migrations.CreateModel( - name='ExternalDatabase', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')), - ('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')), - ('description', models.TextField(blank=True, verbose_name='Description')), - ('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')), - ('url_pattern', models.CharField(blank=True, - help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", - max_length=500, verbose_name='URL Pattern')), - ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), - ], - options={ - 'verbose_name': 'External Database', - 'verbose_name_plural': 'External Databases', - 'db_table': 'epdb_external_database', - 'ordering': ['name'], - }, - ), - migrations.AlterModelOptions( - name='edge', - options={}, - ), - migrations.RemoveField( - model_name='edge', - name='polymorphic_ctype', - ), - migrations.CreateModel( - name='ApplicabilityDomain', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), - ('name', models.TextField(default='no name', verbose_name='Name')), - ('description', models.TextField(default='no description', verbose_name='Descriptions')), - ('kv', models.JSONField(blank=True, default=dict, null=True)), - ('num_neighbours', models.IntegerField(default=5)), - ('reliability_threshold', models.FloatField(default=0.5)), - ('local_compatibilty_threshold', models.FloatField(default=0.5)), - ('model', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning')), - ('functional_groups', models.JSONField(blank=True, default=dict, null=True)), - ('url', models.TextField(null=True, verbose_name='URL')), - ], - options={ - 'abstract': False, - }, - ), - migrations.AddField( - model_name='mlrelativereasoning', - name='app_domain', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, - to='epdb.applicabilitydomain'), - ), - migrations.CreateModel( - name='APIToken', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('hashed_key', - models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True)), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('expires_at', - models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', - null=True)), - ('name', models.CharField(help_text='Descriptive name for this token', max_length=100)), - ('user', - models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, - related_name='api_tokens', to=settings.AUTH_USER_MODEL)), - ('is_active', models.BooleanField(default=True, help_text='Whether this token is active')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ], - options={ - 'ordering': ['-created'], - 'verbose_name': 'API Token', - 'verbose_name_plural': 'API Tokens', - 'db_table': 'epdb_api_token', - }, - ), - migrations.CreateModel( - name='ExternalIdentifier', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, - verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, - verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ('object_id', models.IntegerField()), - ('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')), - ('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')), - ('is_primary', - models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', - verbose_name='Is Primary')), - ('content_type', - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), - ('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', - verbose_name='External Database')), - ], - options={ - 'verbose_name': 'External Identifier', - 'verbose_name_plural': 'External Identifiers', - 'db_table': 'epdb_external_identifier', - 'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), - models.Index(fields=['database', 'identifier_value'], - name='epdb_extern_databas_486422_idx')], - 'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')}, - }, - ), - migrations.AddField( - model_name='compound', - name='url', - field=models.TextField(null=True, verbose_name='URL'), - ), - migrations.AddField( - model_name='compoundstructure', - name='url', - field=models.TextField(null=True, verbose_name='URL'), - ), - migrations.AddField( - model_name='edge', - name='url', - field=models.TextField(null=True, verbose_name='URL'), - ), - migrations.AddField( - model_name='epmodel', - name='url', - field=models.TextField(null=True, verbose_name='URL'), - ), - migrations.AddField( - model_name='node', - name='url', - field=models.TextField(null=True, verbose_name='URL'), - ), - migrations.AddField( - model_name='pathway', - name='url', - field=models.TextField(null=True, verbose_name='URL'), - ), - migrations.AddField( - model_name='reaction', - name='url', - field=models.TextField(null=True, verbose_name='URL'), - ), - migrations.AddField( - model_name='rule', - name='url', - field=models.TextField(null=True, verbose_name='URL'), - ), - migrations.AddField( - model_name='user', - name='url', - field=models.TextField(null=True, verbose_name='URL'), - ), - migrations.RunPython( - code=populate_url, - reverse_code=django.db.migrations.operations.special.RunPython.noop, - ), - migrations.AlterField( - model_name='applicabilitydomain', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='compound', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='compoundstructure', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='edge', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='epmodel', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='group', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='node', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='package', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='pathway', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='reaction', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='rule', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='scenario', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='setting', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='user', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - ] diff --git a/epdb/migrations/0002_externaldatabase_alter_apitoken_options_and_more.py b/epdb/migrations/0002_externaldatabase_alter_apitoken_options_and_more.py deleted file mode 100644 index 44215433..00000000 --- a/epdb/migrations/0002_externaldatabase_alter_apitoken_options_and_more.py +++ /dev/null @@ -1,128 +0,0 @@ -# Generated by Django 5.2.1 on 2025-08-25 18:07 - -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('contenttypes', '0002_remove_content_type_name'), - ('epdb', '0001_initial'), - ] - - operations = [ - migrations.CreateModel( - name='ExternalDatabase', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')), - ('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')), - ('description', models.TextField(blank=True, verbose_name='Description')), - ('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')), - ('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')), - ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), - ], - options={ - 'verbose_name': 'External Database', - 'verbose_name_plural': 'External Databases', - 'db_table': 'epdb_external_database', - 'ordering': ['name'], - }, - ), - migrations.AlterModelOptions( - name='apitoken', - options={'ordering': ['-created'], 'verbose_name': 'API Token', 'verbose_name_plural': 'API Tokens'}, - ), - migrations.AlterModelOptions( - name='edge', - options={}, - ), - migrations.RemoveField( - model_name='edge', - name='polymorphic_ctype', - ), - migrations.AddField( - model_name='apitoken', - name='is_active', - field=models.BooleanField(default=True, help_text='Whether this token is active'), - ), - migrations.AddField( - model_name='apitoken', - name='modified', - field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'), - ), - migrations.AddField( - model_name='applicabilitydomain', - name='functional_groups', - field=models.JSONField(blank=True, default=dict, null=True), - ), - migrations.AddField( - model_name='mlrelativereasoning', - name='app_domain', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'), - ), - migrations.AlterField( - model_name='apitoken', - name='created', - field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'), - ), - migrations.AlterField( - model_name='apitoken', - name='expires_at', - field=models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True), - ), - migrations.AlterField( - model_name='apitoken', - name='hashed_key', - field=models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True), - ), - migrations.AlterField( - model_name='apitoken', - name='name', - field=models.CharField(help_text='Descriptive name for this token', max_length=100), - ), - migrations.AlterField( - model_name='apitoken', - name='user', - field=models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL), - ), - migrations.AlterField( - model_name='applicabilitydomain', - name='num_neighbours', - field=models.IntegerField(default=5), - ), - migrations.AlterModelTable( - name='apitoken', - table='epdb_api_token', - ), - migrations.CreateModel( - name='ExternalIdentifier', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), - ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), - ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ('object_id', models.IntegerField()), - ('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')), - ('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')), - ('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')), - ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), - ('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')), - ], - options={ - 'verbose_name': 'External Identifier', - 'verbose_name_plural': 'External Identifiers', - 'db_table': 'epdb_external_identifier', - 'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')], - 'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')}, - }, - ), - ] diff --git a/epdb/migrations/0003_applicabilitydomain_url_compound_url_and_more.py b/epdb/migrations/0003_applicabilitydomain_url_compound_url_and_more.py deleted file mode 100644 index 5cb39127..00000000 --- a/epdb/migrations/0003_applicabilitydomain_url_compound_url_and_more.py +++ /dev/null @@ -1,228 +0,0 @@ -# Generated by Django 5.2.1 on 2025-08-26 17:05 - -from django.db import migrations, models - - -def populate_url(apps, schema_editor): - MODELS = [ - 'User', - 'Group', - 'Package', - 'Compound', - 'CompoundStructure', - 'Pathway', - 'Edge', - 'Node', - 'Reaction', - 'SimpleAmbitRule', - 'SimpleRDKitRule', - 'ParallelRule', - 'SequentialRule', - 'Scenario', - 'Setting', - 'MLRelativeReasoning', - 'EnviFormer', - 'ApplicabilityDomain', - ] - for model in MODELS: - obj_cls = apps.get_model("epdb", model) - for obj in obj_cls.objects.all(): - obj.url = assemble_url(obj) - if obj.url is None: - raise ValueError(f"Could not assemble url for {obj}") - obj.save() - - -def assemble_url(obj): - from django.conf import settings as s - match obj.__class__.__name__: - case 'User': - return '{}/user/{}'.format(s.SERVER_URL, obj.uuid) - case 'Group': - return '{}/group/{}'.format(s.SERVER_URL, obj.uuid) - case 'Package': - return '{}/package/{}'.format(s.SERVER_URL, obj.uuid) - case 'Compound': - return '{}/compound/{}'.format(obj.package.url, obj.uuid) - case 'CompoundStructure': - return '{}/structure/{}'.format(obj.compound.url, obj.uuid) - case 'SimpleAmbitRule': - return '{}/simple-ambit-rule/{}'.format(obj.package.url, obj.uuid) - case 'SimpleRDKitRule': - return '{}/simple-rdkit-rule/{}'.format(obj.package.url, obj.uuid) - case 'ParallelRule': - return '{}/parallel-rule/{}'.format(obj.package.url, obj.uuid) - case 'SequentialRule': - return '{}/sequential-rule/{}'.format(obj.compound.url, obj.uuid) - case 'Reaction': - return '{}/reaction/{}'.format(obj.package.url, obj.uuid) - case 'Pathway': - return '{}/pathway/{}'.format(obj.package.url, obj.uuid) - case 'Node': - return '{}/node/{}'.format(obj.pathway.url, obj.uuid) - case 'Edge': - return '{}/edge/{}'.format(obj.pathway.url, obj.uuid) - case 'MLRelativeReasoning': - return '{}/model/{}'.format(obj.package.url, obj.uuid) - case 'EnviFormer': - return '{}/model/{}'.format(obj.package.url, obj.uuid) - case 'ApplicabilityDomain': - return '{}/model/{}/applicability-domain/{}'.format(obj.model.package.url, obj.model.uuid, obj.uuid) - case 'Scenario': - return '{}/scenario/{}'.format(obj.package.url, obj.uuid) - case 'Setting': - return '{}/setting/{}'.format(s.SERVER_URL, obj.uuid) - case _: - raise ValueError(f"Unknown model {obj.__class__.__name__}") - - -class Migration(migrations.Migration): - dependencies = [ - ('epdb', '0002_externaldatabase_alter_apitoken_options_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='applicabilitydomain', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='compound', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='compoundstructure', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='edge', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='epmodel', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='group', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='node', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='package', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='pathway', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='reaction', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='rule', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='scenario', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='setting', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - migrations.AddField( - model_name='user', - name='url', - field=models.TextField(null=True, unique=False, verbose_name='URL'), - ), - - migrations.RunPython(populate_url, reverse_code=migrations.RunPython.noop), - - migrations.AlterField( - model_name='applicabilitydomain', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='compound', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='compoundstructure', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='edge', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='epmodel', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='group', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='node', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='package', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='pathway', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='reaction', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='rule', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='scenario', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='setting', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - migrations.AlterField( - model_name='user', - name='url', - field=models.TextField(null=True, unique=True, verbose_name='URL'), - ), - ] diff --git a/epdb/migrations/0004_alter_mlrelativereasoning_options_and_more.py b/epdb/migrations/0004_alter_mlrelativereasoning_options_and_more.py deleted file mode 100644 index 674a73dc..00000000 --- a/epdb/migrations/0004_alter_mlrelativereasoning_options_and_more.py +++ /dev/null @@ -1,55 +0,0 @@ -# Generated by Django 5.2.1 on 2025-09-09 09:21 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('epdb', '0001_squashed_0003_applicabilitydomain_url_compound_url_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='mlrelativereasoning', - options={}, - ), - migrations.AlterField( - model_name='mlrelativereasoning', - name='data_packages', - field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'), - ), - migrations.AlterField( - model_name='mlrelativereasoning', - name='eval_packages', - field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'), - ), - migrations.AlterField( - model_name='mlrelativereasoning', - name='rule_packages', - field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'), - ), - migrations.CreateModel( - name='RuleBasedRelativeReasoning', - fields=[ - ('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')), - ('threshold', models.FloatField(default=0.5)), - ('eval_results', models.JSONField(blank=True, default=dict, null=True)), - ('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')), - ('min_count', models.IntegerField(default=10)), - ('max_count', models.IntegerField(default=0)), - ('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')), - ('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages')), - ('eval_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages')), - ('rule_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages')), - ], - options={ - 'abstract': False, - }, - bases=('epdb.epmodel',), - ), - migrations.DeleteModel( - name='RuleBaseRelativeReasoning', - ), - ] diff --git a/epdb/migrations/0005_alter_group_group_member.py b/epdb/migrations/0005_alter_group_group_member.py deleted file mode 100644 index 62aa724a..00000000 --- a/epdb/migrations/0005_alter_group_group_member.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.1 on 2025-09-11 06:21 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('epdb', '0004_alter_mlrelativereasoning_options_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='group', - name='group_member', - field=models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member'), - ), - ] diff --git a/epdb/migrations/0006_mlrelativereasoning_multigen_eval_and_more.py b/epdb/migrations/0006_mlrelativereasoning_multigen_eval_and_more.py deleted file mode 100644 index 008214ae..00000000 --- a/epdb/migrations/0006_mlrelativereasoning_multigen_eval_and_more.py +++ /dev/null @@ -1,23 +0,0 @@ -# Generated by Django 5.2.1 on 2025-09-18 06:42 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('epdb', '0005_alter_group_group_member'), - ] - - operations = [ - migrations.AddField( - model_name='mlrelativereasoning', - name='multigen_eval', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='rulebasedrelativereasoning', - name='multigen_eval', - field=models.BooleanField(default=False), - ), - ] diff --git a/epdb/migrations/0007_alter_enviformer_options_enviformer_app_domain_and_more.py b/epdb/migrations/0007_alter_enviformer_options_enviformer_app_domain_and_more.py deleted file mode 100644 index 5ffbe7f6..00000000 --- a/epdb/migrations/0007_alter_enviformer_options_enviformer_app_domain_and_more.py +++ /dev/null @@ -1,53 +0,0 @@ -# Generated by Django 5.2.1 on 2025-10-07 08:19 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('epdb', '0006_mlrelativereasoning_multigen_eval_and_more'), - ] - - operations = [ - migrations.AlterModelOptions( - name='enviformer', - options={}, - ), - migrations.AddField( - model_name='enviformer', - name='app_domain', - field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'), - ), - migrations.AddField( - model_name='enviformer', - name='data_packages', - field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'), - ), - migrations.AddField( - model_name='enviformer', - name='eval_packages', - field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'), - ), - migrations.AddField( - model_name='enviformer', - name='eval_results', - field=models.JSONField(blank=True, default=dict, null=True), - ), - migrations.AddField( - model_name='enviformer', - name='model_status', - field=models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL'), - ), - migrations.AddField( - model_name='enviformer', - name='multigen_eval', - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name='enviformer', - name='rule_packages', - field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'), - ), - ] diff --git a/epdb/migrations/0008_enzymelink.py b/epdb/migrations/0008_enzymelink.py deleted file mode 100644 index 35d0a950..00000000 --- a/epdb/migrations/0008_enzymelink.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-10 06:58 - -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -import uuid -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0007_alter_enviformer_options_enviformer_app_domain_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="EnzymeLink", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ( - "uuid", - models.UUIDField( - default=uuid.uuid4, unique=True, verbose_name="UUID of this object" - ), - ), - ("name", models.TextField(default="no name", verbose_name="Name")), - ( - "description", - models.TextField(default="no description", verbose_name="Descriptions"), - ), - ("url", models.TextField(null=True, unique=True, verbose_name="URL")), - ("kv", models.JSONField(blank=True, default=dict, null=True)), - ("ec_number", models.TextField(verbose_name="EC Number")), - ("classification_level", models.IntegerField(verbose_name="Classification Level")), - ("linking_method", models.TextField(verbose_name="Linking Method")), - ("edge_evidence", models.ManyToManyField(to="epdb.edge")), - ("reaction_evidence", models.ManyToManyField(to="epdb.reaction")), - ( - "rule", - models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/epdb/migrations/0009_joblog.py b/epdb/migrations/0009_joblog.py deleted file mode 100644 index 5c731eb1..00000000 --- a/epdb/migrations/0009_joblog.py +++ /dev/null @@ -1,66 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-27 09:39 - -import django.db.models.deletion -import django.utils.timezone -import model_utils.fields -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0008_enzymelink"), - ] - - operations = [ - migrations.CreateModel( - name="JobLog", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ( - "created", - model_utils.fields.AutoCreatedField( - default=django.utils.timezone.now, editable=False, verbose_name="created" - ), - ), - ( - "modified", - model_utils.fields.AutoLastModifiedField( - default=django.utils.timezone.now, editable=False, verbose_name="modified" - ), - ), - ("task_id", models.UUIDField(unique=True)), - ("job_name", models.TextField()), - ( - "status", - models.CharField( - choices=[ - ("INITIAL", "Initial"), - ("SUCCESS", "Success"), - ("FAILURE", "Failure"), - ("REVOKED", "Revoked"), - ("IGNORED", "Ignored"), - ], - default="INITIAL", - max_length=20, - ), - ), - ("done_at", models.DateTimeField(blank=True, default=None, null=True)), - ("task_result", models.TextField(blank=True, default=None, null=True)), - ( - "user", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL - ), - ), - ], - options={ - "abstract": False, - }, - ), - ] diff --git a/epdb/migrations/0010_license_cc_string.py b/epdb/migrations/0010_license_cc_string.py deleted file mode 100644 index 5594c756..00000000 --- a/epdb/migrations/0010_license_cc_string.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-11 14:11 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0009_joblog"), - ] - - operations = [ - migrations.AddField( - model_name="license", - name="cc_string", - field=models.TextField(default="by-nc-sa", verbose_name="CC string"), - preserve_default=False, - ), - ] diff --git a/epdb/migrations/0011_auto_20251111_1413.py b/epdb/migrations/0011_auto_20251111_1413.py deleted file mode 100644 index d0a3463a..00000000 --- a/epdb/migrations/0011_auto_20251111_1413.py +++ /dev/null @@ -1,59 +0,0 @@ -# Generated by Django 5.2.7 on 2025-11-11 14:13 - -import re - -from django.contrib.postgres.aggregates import ArrayAgg -from django.db import migrations -from django.db.models import Min - - -def set_cc(apps, schema_editor): - License = apps.get_model("epdb", "License") - - # For all existing licenses extract cc_string from link - for license in License.objects.all(): - pattern = r"/licenses/([^/]+)/4\.0" - match = re.search(pattern, license.link) - if match: - license.cc_string = match.group(1) - license.save() - else: - raise ValueError(f"Could not find license for {license.link}") - - # Ensure we have all licenses - cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"] - for cc_string in cc_strings: - if not License.objects.filter(cc_string=cc_string).exists(): - new_license = License() - new_license.cc_string = cc_string - new_license.link = f"https://creativecommons.org/licenses/{cc_string}/4.0/" - new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png" - new_license.save() - - # As we might have existing Licenses representing the same License, - # get min pk and all pks as a list - license_lookup_qs = License.objects.values("cc_string").annotate( - lowest_pk=Min("id"), all_pks=ArrayAgg("id", order_by=("id",)) - ) - - license_lookup = { - row["cc_string"]: (row["lowest_pk"], row["all_pks"]) for row in license_lookup_qs - } - - Packages = apps.get_model("epdb", "Package") - - for k, v in license_lookup.items(): - # Set min pk to all packages pointing to any of the duplicates - Packages.objects.filter(pk__in=v[1]).update(license_id=v[0]) - # remove the min pk from "other" pks as we use them for deletion - v[1].remove(v[0]) - # Delete redundant License objects - License.objects.filter(pk__in=v[1]).delete() - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0010_license_cc_string"), - ] - - operations = [migrations.RunPython(set_cc)] diff --git a/epdb/migrations/0012_node_stereo_removed_pathway_predicted.py b/epdb/migrations/0012_node_stereo_removed_pathway_predicted.py deleted file mode 100644 index 648090d7..00000000 --- a/epdb/migrations/0012_node_stereo_removed_pathway_predicted.py +++ /dev/null @@ -1,22 +0,0 @@ -# Generated by Django 5.2.7 on 2025-12-02 13:09 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0011_auto_20251111_1413"), - ] - - operations = [ - migrations.AddField( - model_name="node", - name="stereo_removed", - field=models.BooleanField(default=False), - ), - migrations.AddField( - model_name="pathway", - name="predicted", - field=models.BooleanField(default=False), - ), - ] diff --git a/epdb/migrations/0013_setting_expansion_schema.py b/epdb/migrations/0013_setting_expansion_schema.py deleted file mode 100644 index 9a981795..00000000 --- a/epdb/migrations/0013_setting_expansion_schema.py +++ /dev/null @@ -1,25 +0,0 @@ -# Generated by Django 5.2.7 on 2025-12-14 11:30 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0012_node_stereo_removed_pathway_predicted"), - ] - - operations = [ - migrations.AddField( - model_name="setting", - name="expansion_schema", - field=models.CharField( - choices=[ - ("BFS", "Breadth First Search"), - ("DFS", "Depth First Search"), - ("GREEDY", "Greedy"), - ], - default="BFS", - max_length=20, - ), - ), - ] diff --git a/epdb/migrations/0014_rename_expansion_schema_setting_expansion_scheme.py b/epdb/migrations/0014_rename_expansion_schema_setting_expansion_scheme.py deleted file mode 100644 index b7332fee..00000000 --- a/epdb/migrations/0014_rename_expansion_schema_setting_expansion_scheme.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.7 on 2025-12-14 16:02 - -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0013_setting_expansion_schema"), - ] - - operations = [ - migrations.RenameField( - model_name="setting", - old_name="expansion_schema", - new_name="expansion_scheme", - ), - ] diff --git a/epdb/migrations/0015_user_is_reviewer.py b/epdb/migrations/0015_user_is_reviewer.py deleted file mode 100644 index b26db739..00000000 --- a/epdb/migrations/0015_user_is_reviewer.py +++ /dev/null @@ -1,17 +0,0 @@ -# Generated by Django 5.2.7 on 2026-01-19 19:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0014_rename_expansion_schema_setting_expansion_scheme"), - ] - - operations = [ - migrations.AddField( - model_name="user", - name="is_reviewer", - field=models.BooleanField(default=False), - ), - ] diff --git a/epdb/migrations/0016_remove_enviformer_model_status_and_more.py b/epdb/migrations/0016_remove_enviformer_model_status_and_more.py deleted file mode 100644 index 2f85b8aa..00000000 --- a/epdb/migrations/0016_remove_enviformer_model_status_and_more.py +++ /dev/null @@ -1,179 +0,0 @@ -# Generated by Django 5.2.7 on 2026-02-12 09:38 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0015_user_is_reviewer"), - ] - - operations = [ - migrations.RemoveField( - model_name="enviformer", - name="model_status", - ), - migrations.RemoveField( - model_name="mlrelativereasoning", - name="model_status", - ), - migrations.RemoveField( - model_name="rulebasedrelativereasoning", - name="model_status", - ), - migrations.AddField( - model_name="epmodel", - name="model_status", - field=models.CharField( - choices=[ - ("INITIAL", "Initial"), - ("INITIALIZING", "Model is initializing."), - ("BUILDING", "Model is building."), - ( - "BUILT_NOT_EVALUATED", - "Model is built and can be used for predictions, Model is not evaluated yet.", - ), - ("EVALUATING", "Model is evaluating"), - ("FINISHED", "Model has finished building and evaluation."), - ("ERROR", "Model has failed."), - ], - default="INITIAL", - ), - ), - migrations.AlterField( - model_name="enviformer", - name="eval_packages", - field=models.ManyToManyField( - blank=True, - related_name="%(app_label)s_%(class)s_eval_packages", - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Evaluation Packages", - ), - ), - migrations.AlterField( - model_name="enviformer", - name="rule_packages", - field=models.ManyToManyField( - blank=True, - related_name="%(app_label)s_%(class)s_rule_packages", - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Rule Packages", - ), - ), - migrations.AlterField( - model_name="mlrelativereasoning", - name="eval_packages", - field=models.ManyToManyField( - blank=True, - related_name="%(app_label)s_%(class)s_eval_packages", - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Evaluation Packages", - ), - ), - migrations.AlterField( - model_name="mlrelativereasoning", - name="rule_packages", - field=models.ManyToManyField( - blank=True, - related_name="%(app_label)s_%(class)s_rule_packages", - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Rule Packages", - ), - ), - migrations.AlterField( - model_name="rulebasedrelativereasoning", - name="eval_packages", - field=models.ManyToManyField( - blank=True, - related_name="%(app_label)s_%(class)s_eval_packages", - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Evaluation Packages", - ), - ), - migrations.AlterField( - model_name="rulebasedrelativereasoning", - name="rule_packages", - field=models.ManyToManyField( - blank=True, - related_name="%(app_label)s_%(class)s_rule_packages", - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Rule Packages", - ), - ), - migrations.CreateModel( - name="PropertyPluginModel", - fields=[ - ( - "epmodel_ptr", - models.OneToOneField( - auto_created=True, - on_delete=django.db.models.deletion.CASCADE, - parent_link=True, - primary_key=True, - serialize=False, - to="epdb.epmodel", - ), - ), - ("threshold", models.FloatField(default=0.5)), - ("eval_results", models.JSONField(blank=True, default=dict, null=True)), - ("multigen_eval", models.BooleanField(default=False)), - ("plugin_identifier", models.CharField(max_length=255)), - ( - "app_domain", - models.ForeignKey( - blank=True, - default=None, - null=True, - on_delete=django.db.models.deletion.SET_NULL, - to="epdb.applicabilitydomain", - ), - ), - ( - "data_packages", - models.ManyToManyField( - blank=True, - related_name="%(app_label)s_%(class)s_data_packages", - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Data Packages", - ), - ), - ( - "eval_packages", - models.ManyToManyField( - blank=True, - related_name="%(app_label)s_%(class)s_eval_packages", - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Evaluation Packages", - ), - ), - ( - "rule_packages", - models.ManyToManyField( - blank=True, - related_name="%(app_label)s_%(class)s_rule_packages", - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Rule Packages", - ), - ), - ], - options={ - "abstract": False, - }, - bases=("epdb.epmodel",), - ), - migrations.AddField( - model_name="setting", - name="property_models", - field=models.ManyToManyField( - blank=True, - related_name="settings", - to="epdb.propertypluginmodel", - verbose_name="Setting Property Models", - ), - ), - migrations.DeleteModel( - name="PluginModel", - ), - ] diff --git a/epdb/migrations/0017_additionalinformation.py b/epdb/migrations/0017_additionalinformation.py deleted file mode 100644 index a02af573..00000000 --- a/epdb/migrations/0017_additionalinformation.py +++ /dev/null @@ -1,93 +0,0 @@ -# Generated by Django 5.2.7 on 2026-02-20 12:02 - -import django.db.models.deletion -import uuid -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("contenttypes", "0002_remove_content_type_name"), - ("epdb", "0016_remove_enviformer_model_status_and_more"), - ] - - operations = [ - migrations.CreateModel( - name="AdditionalInformation", - fields=[ - ( - "id", - models.BigAutoField( - auto_created=True, primary_key=True, serialize=False, verbose_name="ID" - ), - ), - ("uuid", models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), - ("url", models.TextField(null=True, unique=True, verbose_name="URL")), - ("kv", models.JSONField(blank=True, default=dict, null=True)), - ("type", models.TextField(verbose_name="Additional Information Type")), - ("data", models.JSONField(blank=True, default=dict, null=True)), - ("object_id", models.PositiveBigIntegerField(blank=True, null=True)), - ( - "content_type", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - to="contenttypes.contenttype", - ), - ), - ( - "package", - models.ForeignKey( - on_delete=django.db.models.deletion.CASCADE, - to=settings.EPDB_PACKAGE_MODEL, - verbose_name="Package", - ), - ), - ( - "scenario", - models.ForeignKey( - blank=True, - null=True, - on_delete=django.db.models.deletion.CASCADE, - related_name="scenario_additional_information", - to="epdb.scenario", - ), - ), - ], - options={ - "indexes": [ - models.Index(fields=["type"], name="epdb_additi_type_394349_idx"), - models.Index( - fields=["scenario", "type"], name="epdb_additi_scenari_a59edf_idx" - ), - models.Index( - fields=["content_type", "object_id"], name="epdb_additi_content_44d4b4_idx" - ), - models.Index( - fields=["scenario", "content_type", "object_id"], - name="epdb_additi_scenari_ef2bf5_idx", - ), - ], - "constraints": [ - models.CheckConstraint( - condition=models.Q( - models.Q(("content_type__isnull", True), ("object_id__isnull", True)), - models.Q(("content_type__isnull", False), ("object_id__isnull", False)), - _connector="OR", - ), - name="ck_addinfo_gfk_pair", - ), - models.CheckConstraint( - condition=models.Q( - ("scenario__isnull", False), - ("content_type__isnull", False), - _connector="OR", - ), - name="ck_addinfo_not_both_null", - ), - ], - }, - ), - ] diff --git a/epdb/migrations/0018_auto_20260220_1203.py b/epdb/migrations/0018_auto_20260220_1203.py deleted file mode 100644 index d1f73e1a..00000000 --- a/epdb/migrations/0018_auto_20260220_1203.py +++ /dev/null @@ -1,132 +0,0 @@ -# Generated by Django 5.2.7 on 2026-02-20 12:03 - -from django.db import migrations - - -def get_additional_information(scenario): - from envipy_additional_information import registry - from envipy_additional_information.parsers import TypeOfAerationParser - - for k, vals in scenario.additional_information.items(): - if k == "enzyme": - continue - - if k == "SpikeConentration": - k = "SpikeConcentration" - - if k == "AerationType": - k = "TypeOfAeration" - - for v in vals: - # Per default additional fields are ignored - MAPPING = {c.__name__: c for c in registry.list_models().values()} - try: - inst = MAPPING[k](**v) - except Exception: - if k == "TypeOfAeration": - toa = TypeOfAerationParser() - inst = toa.from_string(v["type"]) - - # Add uuid to uniquely identify objects for manipulation - if "uuid" in v: - inst.__dict__["uuid"] = v["uuid"] - - yield inst - - -def forward_func(apps, schema_editor): - Scenario = apps.get_model("epdb", "Scenario") - ContentType = apps.get_model("contenttypes", "ContentType") - AdditionalInformation = apps.get_model("epdb", "AdditionalInformation") - - bulk = [] - related = [] - ctype = {o.model: o for o in ContentType.objects.all()} - parents = Scenario.objects.prefetch_related( - "compound_set", - "compoundstructure_set", - "reaction_set", - "rule_set", - "pathway_set", - "node_set", - "edge_set", - ).filter(parent__isnull=True) - - for i, scenario in enumerate(parents): - print(f"{i + 1}/{len(parents)}", end="\r") - if scenario.parent is not None: - related.append(scenario.parent) - continue - - for ai in get_additional_information(scenario): - bulk.append( - AdditionalInformation( - package=scenario.package, - scenario=scenario, - type=ai.__class__.__name__, - data=ai.model_dump(mode="json"), - ) - ) - - print("\n", len(bulk)) - - related = Scenario.objects.prefetch_related( - "compound_set", - "compoundstructure_set", - "reaction_set", - "rule_set", - "pathway_set", - "node_set", - "edge_set", - ).filter(parent__isnull=False) - - for i, scenario in enumerate(related): - print(f"{i + 1}/{len(related)}", end="\r") - parent = scenario.parent - # Check to which objects this scenario is attached to - for ai in get_additional_information(scenario): - rel_objs = [ - "compound", - "compoundstructure", - "reaction", - "rule", - "pathway", - "node", - "edge", - ] - for rel_obj in rel_objs: - for o in getattr(scenario, f"{rel_obj}_set").all(): - bulk.append( - AdditionalInformation( - package=scenario.package, - scenario=parent, - type=ai.__class__.__name__, - data=ai.model_dump(mode="json"), - content_type=ctype[rel_obj], - object_id=o.pk, - ) - ) - - print("Start creating additional information objects...") - AdditionalInformation.objects.bulk_create(bulk) - print("Done!") - print(len(bulk)) - - Scenario.objects.filter(parent__isnull=False).delete() - # Call ai save to fix urls - ais = AdditionalInformation.objects.all() - total = ais.count() - - for i, ai in enumerate(ais): - print(f"{i + 1}/{total}", end="\r") - ai.save() - - -class Migration(migrations.Migration): - dependencies = [ - ("epdb", "0017_additionalinformation"), - ] - - operations = [ - migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop), - ] diff --git a/epdb/migrations/0019_remove_scenario_additional_information_and_more.py b/epdb/migrations/0019_remove_scenario_additional_information_and_more.py index 0aff3833..5883f705 100644 --- a/epdb/migrations/0019_remove_scenario_additional_information_and_more.py +++ b/epdb/migrations/0019_remove_scenario_additional_information_and_more.py @@ -1,20 +1,741 @@ -# Generated by Django 5.2.7 on 2026-02-23 08:45 +# Generated by Django 5.2.7 on 2026-03-06 10:51 -from django.db import migrations +import django.contrib.auth.models +import django.contrib.auth.validators +import django.contrib.postgres.fields +import django.db.models.deletion +import django.utils.timezone +import model_utils.fields +import uuid +from django.conf import settings +from django.db import migrations, models class Migration(migrations.Migration): + + initial = True + dependencies = [ - ("epdb", "0018_auto_20260220_1203"), + ('auth', '0012_alter_user_first_name_max_length'), + ('contenttypes', '0002_remove_content_type_name'), + migrations.swappable_dependency(settings.EPDB_PACKAGE_MODEL), ] operations = [ - migrations.RemoveField( - model_name="scenario", - name="additional_information", + migrations.CreateModel( + name='ApplicabilityDomain', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('num_neighbours', models.IntegerField(default=5)), + ('reliability_threshold', models.FloatField(default=0.5)), + ('local_compatibilty_threshold', models.FloatField(default=0.5)), + ('functional_groups', models.JSONField(blank=True, default=dict, null=True)), + ], + options={ + 'abstract': False, + }, ), - migrations.RemoveField( - model_name="scenario", - name="parent", + migrations.CreateModel( + name='Edge', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EPModel', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + ), + migrations.CreateModel( + name='ExternalDatabase', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')), + ('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')), + ('description', models.TextField(blank=True, verbose_name='Description')), + ('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')), + ('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')), + ('is_active', models.BooleanField(default=True, verbose_name='Is Active')), + ], + options={ + 'verbose_name': 'External Database', + 'verbose_name_plural': 'External Databases', + 'db_table': 'epdb_external_database', + 'ordering': ['name'], + }, + ), + migrations.CreateModel( + name='Permission', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)), + ], + ), + migrations.CreateModel( + name='License', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('cc_string', models.TextField(verbose_name='CC string')), + ('link', models.URLField(verbose_name='link')), + ('image_link', models.URLField(verbose_name='Image link')), + ], + ), + migrations.CreateModel( + name='Rule', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')), + ('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + ), + migrations.CreateModel( + name='User', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('password', models.CharField(max_length=128, verbose_name='password')), + ('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')), + ('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')), + ('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')), + ('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')), + ('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')), + ('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')), + ('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')), + ('email', models.EmailField(max_length=254, unique=True)), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('is_reviewer', models.BooleanField(default=False)), + ('default_package', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Default Package')), + ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')), + ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')), + ], + options={ + 'verbose_name': 'user', + 'verbose_name_plural': 'users', + 'abstract': False, + }, + managers=[ + ('objects', django.contrib.auth.models.UserManager()), + ], + ), + migrations.CreateModel( + name='APIToken', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('hashed_key', models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True)), + ('expires_at', models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True)), + ('name', models.CharField(help_text='Descriptive name for this token', max_length=100)), + ('is_active', models.BooleanField(default=True, help_text='Whether this token is active')), + ('user', models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'API Token', + 'verbose_name_plural': 'API Tokens', + 'db_table': 'epdb_api_token', + 'ordering': ['-created'], + }, + ), + migrations.CreateModel( + name='Compound', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')), + ], + ), + migrations.CreateModel( + name='CompoundStructure', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), + ('smiles', models.TextField(verbose_name='SMILES')), + ('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')), + ('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')), + ('normalized_structure', models.BooleanField(default=False)), + ('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='compound', + name='default_structure', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'), + ), + migrations.CreateModel( + name='PropertyPluginModel', + fields=[ + ('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')), + ('threshold', models.FloatField(default=0.5)), + ('eval_results', models.JSONField(blank=True, default=dict, null=True)), + ('multigen_eval', models.BooleanField(default=False)), + ('plugin_identifier', models.CharField(max_length=255)), + ], + options={ + 'abstract': False, + }, + bases=('epdb.epmodel',), + ), + migrations.CreateModel( + name='Group', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('name', models.TextField(verbose_name='Group name')), + ('public', models.BooleanField(default=False, verbose_name='Public Group')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('group_member', models.ManyToManyField(blank=True, related_name='groups_in_group', to='epdb.group', verbose_name='Group member')), + ('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')), + ('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='user', + name='default_group', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'), + ), + migrations.CreateModel( + name='JobLog', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('task_id', models.UUIDField(unique=True)), + ('job_name', models.TextField()), + ('status', models.CharField(choices=[('INITIAL', 'Initial'), ('SUCCESS', 'Success'), ('FAILURE', 'Failure'), ('REVOKED', 'Revoked'), ('IGNORED', 'Ignored')], default='INITIAL', max_length=20)), + ('done_at', models.DateTimeField(blank=True, default=None, null=True)), + ('task_result', models.TextField(blank=True, default=None, null=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='Package', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')), + ('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')), + ], + options={ + 'swappable': 'EPDB_PACKAGE_MODEL', + }, + ), + migrations.CreateModel( + name='Node', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), + ('depth', models.IntegerField(verbose_name='Node depth')), + ('stereo_removed', models.BooleanField(default=False)), + ('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')), + ('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')), + ('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='edge', + name='end_nodes', + field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'), + ), + migrations.AddField( + model_name='edge', + name='start_nodes', + field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'), + ), + migrations.CreateModel( + name='SequentialRule', + fields=[ + ('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('epdb.rule',), + ), + migrations.CreateModel( + name='SimpleRule', + fields=[ + ('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('epdb.rule',), + ), + migrations.CreateModel( + name='Pathway', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), + ('predicted', models.BooleanField(default=False)), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='node', + name='pathway', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'), + ), + migrations.AddField( + model_name='edge', + name='pathway', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'), + ), + migrations.CreateModel( + name='Reaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')), + ('multi_step', models.BooleanField(verbose_name='Multistep Reaction')), + ('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')), + ('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')), + ('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')), + ('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')), + ], + options={ + 'abstract': False, + }, + ), + migrations.CreateModel( + name='EnzymeLink', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('ec_number', models.TextField(verbose_name='EC Number')), + ('classification_level', models.IntegerField(verbose_name='Classification Level')), + ('linking_method', models.TextField(verbose_name='Linking Method')), + ('edge_evidence', models.ManyToManyField(to='epdb.edge')), + ('rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.rule')), + ('reaction_evidence', models.ManyToManyField(to='epdb.reaction')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='edge', + name='edge_label', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'), + ), + migrations.CreateModel( + name='Scenario', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('scenario_date', models.CharField(default='No date', max_length=256)), + ('scenario_type', models.CharField(default='Not specified', max_length=256)), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='rule', + name='scenarios', + field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), + ), + migrations.AddField( + model_name='reaction', + name='scenarios', + field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), + ), + migrations.AddField( + model_name='pathway', + name='scenarios', + field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), + ), + migrations.AddField( + model_name='node', + name='scenarios', + field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), + ), + migrations.AddField( + model_name='edge', + name='scenarios', + field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), + ), + migrations.AddField( + model_name='compoundstructure', + name='scenarios', + field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), + ), + migrations.AddField( + model_name='compound', + name='scenarios', + field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'), + ), + migrations.CreateModel( + name='Setting', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')), + ('name', models.TextField(default='no name', verbose_name='Name')), + ('description', models.TextField(default='no description', verbose_name='Descriptions')), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('public', models.BooleanField(default=False)), + ('global_default', models.BooleanField(default=False)), + ('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')), + ('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')), + ('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')), + ('expansion_scheme', models.CharField(choices=[('BFS', 'Breadth First Search'), ('DFS', 'Depth First Search'), ('GREEDY', 'Greedy')], default='BFS', max_length=20)), + ('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')), + ('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Setting Rule Packages')), + ('property_models', models.ManyToManyField(blank=True, related_name='settings', to='epdb.propertypluginmodel', verbose_name='Setting Property Models')), + ], + options={ + 'abstract': False, + }, + ), + migrations.AddField( + model_name='pathway', + name='setting', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'), + ), + migrations.AddField( + model_name='user', + name='default_setting', + field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'), + ), + migrations.CreateModel( + name='EnviFormer', + fields=[ + ('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')), + ('threshold', models.FloatField(default=0.5)), + ('eval_results', models.JSONField(blank=True, default=dict, null=True)), + ('multigen_eval', models.BooleanField(default=False)), + ('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')), + ('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')), + ('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')), + ('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')), + ], + options={ + 'abstract': False, + }, + bases=('epdb.epmodel',), + ), + migrations.CreateModel( + name='MLRelativeReasoning', + fields=[ + ('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')), + ('threshold', models.FloatField(default=0.5)), + ('eval_results', models.JSONField(blank=True, default=dict, null=True)), + ('multigen_eval', models.BooleanField(default=False)), + ('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')), + ('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')), + ('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')), + ('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')), + ], + options={ + 'abstract': False, + }, + bases=('epdb.epmodel',), + ), + migrations.AddField( + model_name='applicabilitydomain', + name='model', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning'), + ), + migrations.AddField( + model_name='propertypluginmodel', + name='app_domain', + field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'), + ), + migrations.AddField( + model_name='propertypluginmodel', + name='data_packages', + field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages'), + ), + migrations.AddField( + model_name='propertypluginmodel', + name='eval_packages', + field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages'), + ), + migrations.AddField( + model_name='propertypluginmodel', + name='rule_packages', + field=models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages'), + ), + migrations.CreateModel( + name='RuleBasedRelativeReasoning', + fields=[ + ('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')), + ('threshold', models.FloatField(default=0.5)), + ('eval_results', models.JSONField(blank=True, default=dict, null=True)), + ('multigen_eval', models.BooleanField(default=False)), + ('min_count', models.IntegerField(default=10)), + ('max_count', models.IntegerField(default=0)), + ('app_domain', models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain')), + ('data_packages', models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Data Packages')), + ('eval_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_eval_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Evaluation Packages')), + ('rule_packages', models.ManyToManyField(blank=True, related_name='%(app_label)s_%(class)s_rule_packages', to=settings.EPDB_PACKAGE_MODEL, verbose_name='Rule Packages')), + ], + options={ + 'abstract': False, + }, + bases=('epdb.epmodel',), + ), + migrations.CreateModel( + name='ExternalIdentifier', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')), + ('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('object_id', models.IntegerField()), + ('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')), + ('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')), + ('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')), + ('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')), + ], + options={ + 'verbose_name': 'External Identifier', + 'verbose_name_plural': 'External Identifiers', + 'db_table': 'epdb_external_identifier', + 'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')], + 'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')}, + }, + ), + migrations.CreateModel( + name='SimpleAmbitRule', + fields=[ + ('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')), + ('smirks', models.TextField(verbose_name='SMIRKS')), + ('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')), + ('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('epdb.simplerule',), + ), + migrations.CreateModel( + name='SimpleRDKitRule', + fields=[ + ('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')), + ('reaction_smarts', models.TextField(verbose_name='SMIRKS')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('epdb.simplerule',), + ), + migrations.CreateModel( + name='SequentialRuleOrdering', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('order_index', models.IntegerField()), + ('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')), + ('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')), + ], + ), + migrations.AddField( + model_name='sequentialrule', + name='simple_rules', + field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'), + ), + migrations.CreateModel( + name='ParallelRule', + fields=[ + ('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')), + ('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')), + ], + options={ + 'abstract': False, + 'base_manager_name': 'objects', + }, + bases=('epdb.rule',), + ), + migrations.AlterUniqueTogether( + name='compound', + unique_together={('uuid', 'package')}, + ), + migrations.CreateModel( + name='AdditionalInformation', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), + ('url', models.TextField(null=True, unique=True, verbose_name='URL')), + ('kv', models.JSONField(blank=True, default=dict, null=True)), + ('type', models.TextField(verbose_name='Additional Information Type')), + ('data', models.JSONField(blank=True, default=dict, null=True)), + ('object_id', models.PositiveBigIntegerField(blank=True, null=True)), + ('content_type', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Package')), + ('scenario', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='scenario_additional_information', to='epdb.scenario')), + ], + options={ + 'indexes': [models.Index(fields=['type'], name='epdb_additi_type_394349_idx'), models.Index(fields=['scenario', 'type'], name='epdb_additi_scenari_a59edf_idx'), models.Index(fields=['content_type', 'object_id'], name='epdb_additi_content_44d4b4_idx'), models.Index(fields=['scenario', 'content_type', 'object_id'], name='epdb_additi_scenari_ef2bf5_idx')], + 'constraints': [models.CheckConstraint(condition=models.Q(models.Q(('content_type__isnull', True), ('object_id__isnull', True)), models.Q(('content_type__isnull', False), ('object_id__isnull', False)), _connector='OR'), name='ck_addinfo_gfk_pair'), models.CheckConstraint(condition=models.Q(('scenario__isnull', False), ('content_type__isnull', False), _connector='OR'), name='ck_addinfo_not_both_null')], + }, + ), + migrations.CreateModel( + name='GroupPackagePermission', + fields=[ + ('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')), + ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')), + ('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')), + ], + options={ + 'unique_together': {('package', 'group')}, + }, + bases=('epdb.permission',), + ), + migrations.CreateModel( + name='UserPackagePermission', + fields=[ + ('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')), + ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')), + ('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.EPDB_PACKAGE_MODEL, verbose_name='Permission on')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')), + ], + options={ + 'unique_together': {('package', 'user')}, + }, + bases=('epdb.permission',), + ), + migrations.CreateModel( + name='UserSettingPermission', + fields=[ + ('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')), + ('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')), + ('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')), + ], + options={ + 'unique_together': {('setting', 'user')}, + }, + bases=('epdb.permission',), ), ] diff --git a/epdb/migrations/0023_alter_compoundstructure_options_and_more.py b/epdb/migrations/0023_alter_compoundstructure_options_and_more.py index 6868f514..7da59c39 100644 --- a/epdb/migrations/0023_alter_compoundstructure_options_and_more.py +++ b/epdb/migrations/0023_alter_compoundstructure_options_and_more.py @@ -46,4 +46,9 @@ class Migration(migrations.Migration): name="molfile", field=models.TextField(blank=True, null=True, verbose_name="Molfile"), ), + migrations.AddField( + model_name="group", + name="secret", + field=models.BooleanField(default=False, verbose_name="Secret Group"), + ), ] diff --git a/epdb/models.py b/epdb/models.py index 9b8bd633..3f66cc5b 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -204,6 +204,7 @@ class Group(TimeStampedModel): name = models.TextField(blank=False, null=False, verbose_name="Group name") owner = models.ForeignKey("User", verbose_name="Group Owner", on_delete=models.CASCADE) public = models.BooleanField(verbose_name="Public Group", default=False) + secret = models.BooleanField(verbose_name="Secret Group", default=False) description = models.TextField( blank=False, null=False, verbose_name="Descriptions", default="no description" ) @@ -867,18 +868,25 @@ class Compound( standardized_smiles = FormatConverter.standardize(smiles, remove_stereo=True) + subclasses = CompoundStructure.__subclasses__() + + qs = CompoundStructure.objects.filter(smiles=smiles, compound__package=package) + if subclasses: + qs = qs.not_instance_of(*subclasses) + # Check if we find a direct match for a given SMILES - if CompoundStructure.objects.filter(smiles=smiles, compound__package=package).exists(): - return CompoundStructure.objects.get(smiles=smiles, compound__package=package).compound + if qs.exists(): + return qs.first().compound + + + qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package) + if subclasses: + qs = qs.not_instance_of(*subclasses) # Check if we can find the standardized one - if CompoundStructure.objects.filter( - smiles=standardized_smiles, compound__package=package - ).exists(): + if qs.exists(): # TODO should we add a structure? - return CompoundStructure.objects.get( - smiles=standardized_smiles, compound__package=package - ).compound + return qs.first().compound # Generate Compound c = Compound() diff --git a/epdb/views.py b/epdb/views.py index 50385576..95b3eb42 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -388,6 +388,9 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]: "debug": s.DEBUG, "external_databases": ExternalDatabase.get_databases(), "site_id": s.MATOMO_SITE_ID, + # EDIT START + "secret_groups": Group.objects.filter(secret=True), + # EDIT END }, } @@ -587,10 +590,38 @@ def packages(request): "package-description", s.DEFAULT_VALUES["description"] ) + # EDIT START + data_pool = None + package_classification = request.POST.get("package-classification") + classification = Package.Classification(int(package_classification)) + # For SECRET we'll need a data pool which will be an additional perm check later + if classification == Package.Classification.SECRET: + package_data_pool = request.POST.get("package-data-pool") + + if package_data_pool is None: + return error(request, "Invalid data pool.", "Data Pool is required!") + + data_pool = GroupManager.get_group_by_url(current_user, package_data_pool) + + if data_pool is None: + return error(request, "Invalid data pool.", "Data Pool does not exist or no access!") + + if not data_pool.secret: + return error(request, "Invalid data pool.", "Data Pool is not a secret group!") + created_package = PackageManager.create_package( current_user, package_name, package_description ) + created_package.classification_level = classification + + # Set previously determined data pool + if classification == Package.Classification.SECRET: + created_package.data_pool = data_pool + + created_package.save() + # EDIT END + return redirect(created_package.url) elif request.method == "OPTIONS": @@ -906,12 +937,14 @@ def package_models(request, package_uuid): "requires_rule_packages": True, "requires_data_packages": True, }, - "EnviFormer": { + } + + if s.ENVIFORMER_PRESENT: + context["model_types"]["EnviFormer"] = { "type": "enviformer", "requires_rule_packages": False, "requires_data_packages": True, }, - } if s.FLAGS.get("PLUGINS", False): for k, v in s.CLASSIFIER_PLUGINS.items(): diff --git a/static/images/Restricted.gif b/static/images/Restricted.gif new file mode 100644 index 00000000..12ea8cfd Binary files /dev/null and b/static/images/Restricted.gif differ diff --git a/static/images/bayer-logo.svg b/static/images/bayer-logo.svg new file mode 100644 index 00000000..d3b5d5df --- /dev/null +++ b/static/images/bayer-logo.svg @@ -0,0 +1,30 @@ + + + + + + + + + diff --git a/static/images/restricted_mid.png b/static/images/restricted_mid.png new file mode 100644 index 00000000..6a5c248c Binary files /dev/null and b/static/images/restricted_mid.png differ diff --git a/static/images/secret_mid.png b/static/images/secret_mid.png new file mode 100644 index 00000000..f9772030 Binary files /dev/null and b/static/images/secret_mid.png differ diff --git a/static/images/secret_small.png b/static/images/secret_small.png new file mode 100644 index 00000000..f8d6cc03 Binary files /dev/null and b/static/images/secret_small.png differ diff --git a/static/js/pw.js b/static/js/pw.js index 26f4d870..4f581fc2 100644 --- a/static/js/pw.js +++ b/static/js/pw.js @@ -605,7 +605,36 @@ function draw(pathway, elem) { // Check if target is pseudo and draw marker only if not pseudo .attr("class", d => d.target.pseudo ? "link_no_arrow" : "link") .attr("marker-end", d => d.target.pseudo ? '' : d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)') + .on("click", function(event, d) { + const wasHighlighted = d3.select(this).classed("highlighted"); + d3.selectAll("line").classed("highlighted", false); + + if (!wasHighlighted) { + const toHighlight = []; + toHighlight.push(d.el); + + if (d.source.pseudo || d.target.pseudo) { + if (d.target.pseudo) { + d3.selectAll("line").each(e => { + if (e !== undefined && e.source.id === d.target.id) { + toHighlight.push(e.el); + } + }); + } else { + d3.selectAll("line").each(e => { + if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) { + toHighlight.push(e.el); + } + }); + } + } + + for (const e of toHighlight) { + d3.select(e).classed("highlighted", true); + } + } + }) // add element to links array link.each(function (d) { @@ -624,7 +653,13 @@ function draw(pathway, elem) { .on("drag", dragged) .on("end", dragended)) .on("click", function (event, d) { - d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted")); + const wasHighlighted = d3.select(this).select("circle").classed("highlighted"); + + d3.selectAll('circle.highlighted').classed('highlighted', false); + + if (!wasHighlighted) { + d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted")); + } }) // Kreise für die Knoten hinzufügen diff --git a/templates/actions/objects/pathway.html b/templates/actions/objects/pathway.html index 85e4164b..91c861d6 100644 --- a/templates/actions/objects/pathway.html +++ b/templates/actions/objects/pathway.html @@ -22,7 +22,7 @@ {% for tpl in action_button_templates %} {% include tpl %} {% endfor %} - + {% endif %}
  • - +
  • Set Aliases
  • - +
  • Delete Compound @@ -111,7 +116,12 @@
  • Delete Reaction diff --git a/templates/collections/compounds_paginated.html b/templates/collections/compounds_paginated.html index f5868225..fd30d98e 100644 --- a/templates/collections/compounds_paginated.html +++ b/templates/collections/compounds_paginated.html @@ -5,7 +5,7 @@ {% block action_button %}
    - {% if meta.can_edit %} + {% if meta.can_edit or not meta.url_contains_package %} + {% if meta.can_edit or not meta.url_contains_package %} + {% if meta.enabled_features.MODEL_BUILDING %} + + {% endif %} {% endif %} {% endblock action_button %} diff --git a/templates/collections/packages_paginated.html b/templates/collections/packages_paginated.html index c9d9668b..cd59b6eb 100644 --- a/templates/collections/packages_paginated.html +++ b/templates/collections/packages_paginated.html @@ -3,7 +3,6 @@ {% block page_title %}Packages{% endblock %} {% block action_button %} - {% if meta.can_edit %}
    - {% endif %} {% endblock action_button %} {% block action_modals %} diff --git a/templates/collections/pathways_paginated.html b/templates/collections/pathways_paginated.html index 900f68d4..e87711dd 100644 --- a/templates/collections/pathways_paginated.html +++ b/templates/collections/pathways_paginated.html @@ -3,7 +3,7 @@ {% block page_title %}Pathways{% endblock %} {% block action_button %} - {% if meta.can_edit %} + {% if meta.can_edit or not meta.url_contains_package %}
    +
    {% if not public_mode %} @@ -88,12 +89,23 @@ >Scenario
  • +
    +
  • + Group +
  • {% endif %}