From 6680668c892d6623594c5aede4b36a545c47dc6f Mon Sep 17 00:00:00 2001 From: Tim Lorsbach Date: Fri, 6 Mar 2026 15:15:08 +0100 Subject: [PATCH] adjusted migration Initial bayer app Show Pack Classification Adjusted docker compose to bayer specifics Adjusted Dockerfile for Bayer Adding secret flags to group, add secret pools to packages Adjusted View for Package creation Prep configs, added Package Create Modal wip More on PES wip wip Wip minor PW interactions API PES wip Make Select Widget reflect required make required generallay available Update UI if pathway mode is set to build Added ais circle adjustments Initial Zoom, fix AD Creation wip --- Dockerfile | 17 +- bayer/__init__.py | 0 bayer/additional_information/__init__.py | 178 +++ bayer/admin.py | 19 + bayer/apps.py | 6 + bayer/epdb_hooks.py | 40 + bayer/migrations/0001_initial.py | 35 + bayer/migrations/0002_initial.py | 22 + ...compound_pesstructure_package_data_pool.py | 41 + bayer/migrations/__init__.py | 0 bayer/models.py | 237 ++++ .../actions/collections/new_pes.html | 9 + .../actions/objects/pathway_add_pes.html | 8 + .../modals/collections/new_package_modal.html | 175 +++ .../modals/collections/new_pes_modal.html | 174 +++ .../objects/add_pathway_pes_node_modal.html | 174 +++ .../objects/compound_structure_viz.html | 19 + bayer/templates/objects/compound_viz.html | 19 + bayer/templates/objects/node_viz.html | 19 + bayer/templates/objects/package.html | 97 ++ bayer/templates/static/login.html | 154 +++ bayer/tests.py | 3 + bayer/tests/__init__.py | 0 bayer/tests/pes/__init__.py | 0 bayer/tests/pes/test_pes.py | 174 +++ bayer/urls.py | 19 + bayer/views.py | 167 +++ bb4g/__init__.py | 183 +++ bridge/contracts.py | 10 +- docker-compose.dev.yml | 44 +- docker-compose.yml | 18 +- envipath/settings.py | 55 +- epauth/urls.py | 1 + epauth/views.py | 42 +- epdb/legacy_api.py | 181 ++- epdb/logic.py | 80 +- epdb/migrations/0001_initial.py | 594 ---------- ...abilitydomain_url_compound_url_and_more.py | 1020 ----------------- ...atabase_alter_apitoken_options_and_more.py | 128 --- ...abilitydomain_url_compound_url_and_more.py | 228 ---- ...er_mlrelativereasoning_options_and_more.py | 55 - .../0005_alter_group_group_member.py | 18 - ...elativereasoning_multigen_eval_and_more.py | 23 - ..._options_enviformer_app_domain_and_more.py | 53 - epdb/migrations/0008_enzymelink.py | 64 -- epdb/migrations/0009_joblog.py | 66 -- epdb/migrations/0010_license_cc_string.py | 18 - epdb/migrations/0011_auto_20251111_1413.py | 59 - ...2_node_stereo_removed_pathway_predicted.py | 22 - .../0013_setting_expansion_schema.py | 25 - ...pansion_schema_setting_expansion_scheme.py | 17 - epdb/migrations/0015_user_is_reviewer.py | 17 - ...remove_enviformer_model_status_and_more.py | 179 --- epdb/migrations/0017_additionalinformation.py | 93 -- epdb/migrations/0018_auto_20260220_1203.py | 132 --- ...cenario_additional_information_and_more.py | 739 +++++++++++- ...lter_compoundstructure_options_and_more.py | 5 + epdb/models.py | 42 +- epdb/views.py | 37 +- package.json | 8 +- pnpm-workspace.yaml | 4 +- static/images/Restricted.gif | Bin 0 -> 1577 bytes static/images/bayer-logo.svg | 30 + static/images/restricted_mid.png | Bin 0 -> 2243 bytes static/images/secret_mid.png | Bin 0 -> 1490 bytes static/images/secret_small.png | Bin 0 -> 3226 bytes static/js/alpine/components/widgets.js | 3 + static/js/pw.js | 47 +- templates/actions/objects/pathway.html | 20 +- .../collections/compounds_paginated.html | 2 +- templates/collections/models_paginated.html | 18 +- templates/collections/packages_paginated.html | 2 - templates/collections/pathways_paginated.html | 2 +- .../collections/reactions_paginated.html | 2 +- templates/collections/rules_paginated.html | 2 +- .../collections/scenarios_paginated.html | 2 +- .../collections/structures_paginated.html | 2 +- templates/components/navbar.html | 12 + .../components/widgets/select_widget.html | 1 + .../modals/collections/new_model_modal.html | 4 +- .../new_prediction_setting_modal.html | 4 +- .../objects/delete_pathway_edge_modal.html | 19 + .../objects/delete_pathway_node_modal.html | 16 + templates/objects/group.html | 4 +- templates/objects/node.html | 7 + templates/objects/pathway.html | 6 +- templates/predict_pathway.html | 18 +- templates/static/login_base.html | 2 - 88 files changed, 3360 insertions(+), 2931 deletions(-) create mode 100644 bayer/__init__.py create mode 100644 bayer/additional_information/__init__.py create mode 100644 bayer/admin.py create mode 100644 bayer/apps.py create mode 100644 bayer/epdb_hooks.py create mode 100644 bayer/migrations/0001_initial.py create mode 100644 bayer/migrations/0002_initial.py create mode 100644 bayer/migrations/0003_pescompound_pesstructure_package_data_pool.py create mode 100644 bayer/migrations/__init__.py create mode 100644 bayer/models.py create mode 100644 bayer/templates/actions/collections/new_pes.html create mode 100644 bayer/templates/actions/objects/pathway_add_pes.html create mode 100644 bayer/templates/modals/collections/new_package_modal.html create mode 100644 bayer/templates/modals/collections/new_pes_modal.html create mode 100644 bayer/templates/modals/objects/add_pathway_pes_node_modal.html create mode 100644 bayer/templates/objects/compound_structure_viz.html create mode 100644 bayer/templates/objects/compound_viz.html create mode 100644 bayer/templates/objects/node_viz.html create mode 100644 bayer/templates/objects/package.html create mode 100644 bayer/templates/static/login.html create mode 100644 bayer/tests.py create mode 100644 bayer/tests/__init__.py create mode 100644 bayer/tests/pes/__init__.py create mode 100644 bayer/tests/pes/test_pes.py create mode 100644 bayer/urls.py create mode 100644 bayer/views.py create mode 100644 bb4g/__init__.py delete mode 100644 epdb/migrations/0001_initial.py delete mode 100644 epdb/migrations/0001_squashed_0003_applicabilitydomain_url_compound_url_and_more.py delete mode 100644 epdb/migrations/0002_externaldatabase_alter_apitoken_options_and_more.py delete mode 100644 epdb/migrations/0003_applicabilitydomain_url_compound_url_and_more.py delete mode 100644 epdb/migrations/0004_alter_mlrelativereasoning_options_and_more.py delete mode 100644 epdb/migrations/0005_alter_group_group_member.py delete mode 100644 epdb/migrations/0006_mlrelativereasoning_multigen_eval_and_more.py delete mode 100644 epdb/migrations/0007_alter_enviformer_options_enviformer_app_domain_and_more.py delete mode 100644 epdb/migrations/0008_enzymelink.py delete mode 100644 epdb/migrations/0009_joblog.py delete mode 100644 epdb/migrations/0010_license_cc_string.py delete mode 100644 epdb/migrations/0011_auto_20251111_1413.py delete mode 100644 epdb/migrations/0012_node_stereo_removed_pathway_predicted.py delete mode 100644 epdb/migrations/0013_setting_expansion_schema.py delete mode 100644 epdb/migrations/0014_rename_expansion_schema_setting_expansion_scheme.py delete mode 100644 epdb/migrations/0015_user_is_reviewer.py delete mode 100644 epdb/migrations/0016_remove_enviformer_model_status_and_more.py delete mode 100644 epdb/migrations/0017_additionalinformation.py delete mode 100644 epdb/migrations/0018_auto_20260220_1203.py create mode 100644 static/images/Restricted.gif create mode 100644 static/images/bayer-logo.svg create mode 100644 static/images/restricted_mid.png create mode 100644 static/images/secret_mid.png create mode 100644 static/images/secret_small.png diff --git a/Dockerfile b/Dockerfile index 08cf1600..39a5232a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,18 +6,23 @@ ENV PYTHONDONTWRITEBYTECODE=1 \ WORKDIR /app + RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ libpq-dev \ curl \ openssh-client \ git \ - nodejs \ - npm \ + ca-certificates \ && rm -rf /var/lib/apt/lists/* -# Install pnpm -RUN npm install -g pnpm +# Install Node 22 + pnpm +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get update \ + && apt-get install -y --no-install-recommends nodejs \ + && corepack enable \ + && corepack prepare pnpm@latest --activate \ + && rm -rf /var/lib/apt/lists/* RUN curl -LsSf https://astral.sh/uv/install.sh | sh ENV PATH="/root/.local/bin:${PATH}" @@ -30,13 +35,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/additional_information/__init__.py b/bayer/additional_information/__init__.py new file mode 100644 index 00000000..49b083b8 --- /dev/null +++ b/bayer/additional_information/__init__.py @@ -0,0 +1,178 @@ +import enum +from typing import Optional + +from envipy_additional_information import GroupEnum as G +from envipy_additional_information import SubcategoryEnum as S +from envipy_additional_information import ( + register, + register_parser_command, + EnviPyModel, + # EnviPyModelParser, + Interval, + UIConfig, + IntervalConfig, + WidgetType, + registry, +) + + +@register(keyname="compoundlabel", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL]) +class CompoundLabel(EnviPyModel): + label: str + + class UI: + title = "Compound Label" + label = UIConfig(widget=WidgetType.TEXT, label="Label", order=1) + + +# TODO Expose EnviPyModelParser in lib and subclass +@register_parser_command("compoundlabel") +class CompoundLabelParser: + @staticmethod + def from_string(data: str) -> CompoundLabel: + return CompoundLabel(label=data) + + +@register(keyname="studywaterstoragecapacity", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL]) +class StudyWaterStorageCapacity(EnviPyModel): + capacity: str + + class UI: + title = "Study Water Storage Capacity" + capacity = UIConfig(widget=WidgetType.TEXT, label="Study Water Storage Capacity", order=1) + + +# TODO Expose EnviPyModelParser in lib and subclass +@register_parser_command("studywst") +class StudyWaterStorageCapacityParser: + @staticmethod + def from_string(data: str) -> StudyWaterStorageCapacity: + return StudyWaterStorageCapacity(capacity=data) + + +class ObservationType(enum.Enum): + OBSERVED = "observed" + APPLIED = "applied" + NA = 'NA' + + +@register(keyname="observation", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL]) +class Observation(EnviPyModel): + type: ObservationType + min_value: Optional[float] = None + max_value: Optional[float] = None + + class UI: + title = "Observation" + type = UIConfig(widget=WidgetType.SELECT, label="Observed or Applied", order=1) + min_value = UIConfig(widget=WidgetType.NUMBER, label="Min Value", order=2) + max_value = UIConfig(widget=WidgetType.NUMBER, label="Max Value", order=3) + + +# TODO Expose EnviPyModelParser in lib and subclass +@register_parser_command("observation") +class ObservationParser: + @staticmethod + def from_string(data: str) -> Observation: + parts = data.split(";") + + observation_type = ObservationType(parts[0]) + min_value = None + if parts[1]: + try: + min_value = float(parts[1]) + except ValueError: + pass + + max_value = None + if parts[2]: + try: + max_value = float(parts[2]) + except ValueError: + pass + + return Observation(type=observation_type, min_value=min_value, max_value=max_value) + + +@register(keyname="kinetics", groups=[S.MISC, G.SLUDGE, G.SEDIMENT, G.SOIL]) +class Kinetics(EnviPyModel): + dt50: Interval[float] + normalized_dt50: bool + chi2err: Optional[float] = None + t_test: Optional[float] = None + swarc: Optional[float] = None + visual_fit: Optional[int] = None + comment: str + source: str + kinetic_model: str + k1: Optional[float] = None + k2: Optional[float] = None + g: Optional[float] = None + tb: Optional[float] = None + alpha: Optional[float] = None + beta: Optional[float] = None + + class UI: + title = "Kinetics" + + # Field config + dt50 = IntervalConfig(label="DT50 Range", order=1, unit="d") + normalized_dt50 = UIConfig(widget=WidgetType.CHECKBOX, label="Normalized DT50", order=2) + chi2err = UIConfig(widget=WidgetType.NUMBER, label="Chi2err", order=3) + t_test = UIConfig(widget=WidgetType.NUMBER, label="T-Test", order=4) + swarc = UIConfig(widget=WidgetType.NUMBER, label="SWARC", order=5) + visual_fit = UIConfig(widget=WidgetType.NUMBER, label="Visual Fit", order=6) + comment = UIConfig(widget=WidgetType.TEXTAREA, label="Comments", order=7) + source = UIConfig(widget=WidgetType.TEXT, label="Source", order=8) + kinetic_model = UIConfig(widget=WidgetType.SELECT, label="Kinetic Model", order=9) + k1 = UIConfig(widget=WidgetType.NUMBER, label="K1", order=10) + k2 = UIConfig(widget=WidgetType.NUMBER, label="K2", order=11) + g = UIConfig(widget=WidgetType.NUMBER, label="G", order=12) + tb = UIConfig(widget=WidgetType.NUMBER, label="TB", order=13) + alpha = UIConfig(widget=WidgetType.NUMBER, label="Alpha", order=14) + beta = UIConfig(widget=WidgetType.NUMBER, label="Beta", order=15) + + +# TODO Expose EnviPyModelParser in lib and subclass +@register_parser_command("kineticevaluation") +class KinecticsParser: + @staticmethod + def from_string(data: str) -> Kinetics: + parts = data.split(";") + dt50 = registry.get_parser("interval").from_string(parts[0]) + normalized_dt50 = parts[1] == "true" + chi2err = float(parts[2]) if parts[2] else None + t_test = float(parts[3]) if parts[3] else None + swarc = float(parts[4]) if parts[4] else None + visual_fit = int(parts[5]) if parts[5] else None + comment = parts[6] + source = parts[7] + kinetic_model = parts[8] + k1 = float(parts[9]) if parts[9] else None + k2 = float(parts[10]) if parts[10] else None + g = float(parts[11]) if parts[11] else None + tb = float(parts[12]) if parts[12] else None + alpha = float(parts[13]) if parts[13] else None + beta = float(parts[14]) if parts[14] else None + + return Kinetics( + dt50=dt50, + normalized_dt50=normalized_dt50, + chi2err=chi2err, + t_test=t_test, + swarc=swarc, + visual_fit=visual_fit, + comment=comment, + source=source, + kinetic_model=kinetic_model, + k1=k1, + k2=k2, + g=g, + tb=tb, + alpha=alpha, + beta=beta, + ) + + +if __name__ == '__main__': + print(KinecticsParser.from_string("187.0 - 187.0;false;;;;;;;AFO;;;;;;")) 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..92e71e7a --- /dev/null +++ b/bayer/epdb_hooks.py @@ -0,0 +1,40 @@ +import logging + +from bayer import additional_information # noqa: F401 +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", +) 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..0ee478c4 --- /dev/null +++ b/bayer/models.py @@ -0,0 +1,237 @@ +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)}", + "image_type": "png", + } 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..7a88d109 --- /dev/null +++ b/bayer/views.py @@ -0,0 +1,167 @@ +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(): + + if current_package.classification_level != Package.Classification.SECRET: + return BadRequest("Cannot create PESs for non-secret packages.") + + 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(): + + if current_package.classification_level != Package.Classification.SECRET: + return BadRequest("Cannot create PESs for non-secret packages.") + + 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) + + node_qs = Node.objects.filter(pathway=current_pathway, default_node_label=pes.default_structure) + if node_qs.exists(): + return redirect(current_pathway.url) + + 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..ce4e615e 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,48 @@ 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") + +os.environ["NO_PROXY"] = "localhost,127.0.0.1,epbiotransformer3" \ No newline at end of file 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 b9a9d491..6705bc25 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 # @@ -794,6 +847,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") @@ -805,9 +859,32 @@ 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(): + + if p.classification_level != Package.Classification.SECRET: + return 400, {"Cannot create PESs for non-secret packages."} + + 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)} @@ -1902,25 +1979,67 @@ class CreateNode(Schema): nodeName: str | None = None nodeReason: str | None = None nodeDepth: str | None = None + pesLink: str | None = None @router.post( "/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node", - response={200: str | Any, 403: Error}, + response={200: str | Any, 400: Error, 403: Error}, ) def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]): try: 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(float(n.nodeDepth)) + # TODO Code Dup from bayer.views + + if n.pesLink: + from bayer.views import fetch_pes + from bayer.models import PESCompound + + try: + pes_data = fetch_pes(request, n.pesLink) + except ValueError as e: + return 400, {"message": f"Could not fetch PES data for {n.pesLink}"} + + classification = pes_data.get("classificationLevel", "") + if "secret" == classification.lower(): + + if p.classification_level != Package.Classification.SECRET: + return 400, "Cannot create PESs for non-secret packages." + + 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, n.nodeName, n.nodeReason) + + node_qs = Node.objects.filter(pathway=pw, default_node_label=c.default_structure) + if node_qs.exists(): + return redirect(pw.url) + + 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!"} @@ -2030,13 +2149,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, ) @@ -2047,7 +2169,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 f94d89e9..0f64f019 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() @@ -2197,7 +2205,23 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix depth_map[0] = list() processed = set() - for n in self.root_nodes: + data_driven_root_nodes = self.node_set.all().annotate( + prod_cnt=Count('edge_products'), + educt_cnt=Count('edge_educts') + ).filter(prod_cnt=0, educt_cnt__gt=0).distinct() + + # Eval QuerySet + root_nodes_by_depth = list(self.root_nodes) + + data_driven_root_nodes.update(depth=0) + root_nodes = [n for n in data_driven_root_nodes] + + for n in root_nodes_by_depth: + if n not in root_nodes: + if len(n.edge_products.all()) == 0: + root_nodes.append(n) + + for n in root_nodes: depth_map[0].append(n) # At most depth len(nodes) is possible diff --git a/epdb/views.py b/epdb/views.py index 169fd86d..d34285ac 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/package.json b/package.json index bf4f1516..d2606698 100644 --- a/package.json +++ b/package.json @@ -21,5 +21,11 @@ "django", "tailwindcss", "daisyui" - ] + ], + "pnpm": { + "onlyBuiltDependencies": [ + "@parcel/watcher", + "@tailwindcss/oxide" + ] + } } diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index bff00c56..00d2a6d2 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,3 +1,5 @@ +allowBuilds: + '@parcel/watcher': true onlyBuiltDependencies: - '@parcel/watcher' - - '@tailwindcss/oxide' + - '@tailwindcss/oxide' \ No newline at end of file diff --git a/static/images/Restricted.gif b/static/images/Restricted.gif new file mode 100644 index 0000000000000000000000000000000000000000..12ea8cfd6a69e31a06bb32bc57bfcbc4aac5f35a GIT binary patch literal 1577 zcmd7N`#X~h0KoBgi@ChrE z{jOF?lpia^g{I=C2C6xha;8?^bxl%gp`2of@^z$tdCEHoa(0+vI73o-NcE{t(NBiN zpX6MsC{Jg5^TU6q|6^KeigkQKViGkuCG}xi`lF1@$61d(+)?hi`FXU0r-k(A&x(sm zUX)doR+hhHRMk}1y=o}HXfRn#%`L5M?H!%$u5M0G@85m>1A}k4LvM#iM#tWbPrT=S znEW_3J$3@&ikO=hppLg{3l`Sb_O~3zS_6Q!iG7R7ntXBbHB+&Uz`W+}b2Y0&A{Jl+ zgl@B1P;D7fpA<3IN<>Du9j%A&ND0)@8Qh~o!Jj&hGOo&DV9IP!uK4Vo-^R|S zzk2|8xWA_Cvih4j@s>4U4y*siKub=83RPKcJ3&?2HI1={pg@Btk5Qo z1K9XarN6vU17ekt#B^F0=#3JOdjBO$Mxv*ffu~P-E@}k zguaeX9)r%@SES)i#hOQjX7=eu*=k`7mj^7_7?%~`HZp6Y&3!lE^sezzVBe8NI4lMk zM&?Hm5NfY&7eUGy4`VyG^w4};S^x{Y;vi@YxOQhgujs^meq&*@(NyUkT^9Mr)1)RE zCfg8XX${?InbGRHrx9~ZU`vU;>lM%5DCX&niiNX@2aS&UCc~+Y*~H#T+MNG>2_r{s z=hU0Pxk=`eV1W?%MF4&swsXpdq|7YTipDdh=Ny}Ze6>BdAU!D*>E zp0L;5|ExZGU2aLEp7Fnu!wd}bjuS4?xrK3t0U*7pSDJMx5{IV|G*^8S4XUyx_C+Vc zfA!PX9X4J6GZs-6QqdZ0l3$SNTbZUw`o(0pX0JIpAiIaM6%XHg@+CK;HJtp}Ay-aL zdnEgKr(4r}iWKMEI^_%0bzrZ9HNsKna9gv9tK}zDV&R6S6KNFAN`O+qhrXQYOOpN8K^3i{$z z*>Ew~lL?!#%1(7&vx6Ntccs$4Y8fx=uc%dh8f-$(lyqNuvx|deirOdblqTp#wqAja zwN|ZuXnm&@4o?D+vESF|*2d`Ra4Tj#cibUJ<=1~8;mtEc&9F5L*CKO9>?Xj5**JC$ zCM=H|L^(tRZ}ZHMZ;yQsxivbu&|!B$&hr(Gg@@cr3&~hGnGJ7xobG6+Il(_Bh&%s` z+ldd$X|UC9%Dvt$J9M!n!~=ntjzJDofZ7cQhj50C^B0wyKPai)4$f#?g8gf5#Pjf; z2awHe^y)CEWv`0B!WfJ2giXp^ky64$!Y*vK?HPGXl`ZApg5;}=I!Lk6^c7MLGz&N_ zqH*>g+bgqm4b>BosleJQmYj~+UntAuJB1Zv~xl4 M@-9=j1{^s44_EQ;A^-pY literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6a5c248ccceac1089c9221bd4d762e1e3c397a33 GIT binary patch literal 2243 zcmV;!2t4Px#1ZP1_K>z@;j|==^1poj5W>8F2Mf8&Z{_)@Z(BevpZ)IF^p*(z`0x79kNLb{_p(U#r!e@nQ1zV{`qP*C z#(4YMr}m*B^_md*%!l@*DEPQo00021+(Qol001I%QchF<|NsC0|NsC0|NsC0|NsC0 z|NsC0|NsC0|NsC0|NsC001eQ?aR2}S32;bRa{vGr5dZ)e5dq33^FIIp2e?T@K~z{r z?OBU%A~_Hol2J&2kZhy_@tV#5f8|wmb^9@x2`eIPPDf=Kcl*(9m*3v*3iMy1Tl8Ju z4FjZp-|ZpmZ@wB3edq?r3mBp?+HY0nd30 z*n4V#!nJ)(p_ly9Ur=D8<>pWTJ}rG=Hqf^$#%@P|UbfK6GM+!z_xNGd_$M;ShUJqD zKq>LDitz799luw88a7lBpx`+r<8xvp9SQ#Zm*-GJ{fL9Ebc z3_nd@d#r*P17&>h1kMaV8o%@Y&>b{;4p42N>@l-25FjlrkNt-3qaTLub6EHI*qQ^C zqmY#@s?IhcJ}Pb^t!fCxSme*a@nV6+M5>Y*L=AvUc!G;qy&&d1&!ogn^jHj-3)6(E zeHvbH`P-$2obba6m`w2Jo@fq@OuTl_GOLWhgX~Jj?;sA8v5A_W;@8*mNq7V^etHH6iP9WAI3TisFlG?uU5mo88Anfx8zOK1u=GM#gKO-HE&36m1C=u$QOpNBa#H zes2DNO4x=~PQXfo!doYC!cC*fXzQ5EK&3&x;Ds_KFFbBk+*FqUDB*>g$PmGrO+ZL} zFF?V{&!%F3$4+^`E#`=w8v*=>7xbFPAa0q+Xd|;KTQk&wMzdqt1n{{XjO;k^p#NeTm}`V)3cURasG~odpsQ;rkD~Fn?Zj zt^pU^IZ@iTMk9>K1}oF>LSqL+>>4i!8h~{2UE0LpU ziYKil3nnjcio%I6!y;YwW1e*|5C^Y{3x~N2E;u}(QY(?8EH}Xt(Ie~;c9%Nr1VJ)%Os9Agx(MMwRA9%8HZaKdX*5q^FI zX>`b`$o}d!V;5#%!l)XNruuHc{HwiGOKK|g3^rX|P$Ac%R|v&a%S+3{tEm8d;6Xa0 zLX$JRk(C|8j$T=07+8BF{Wi!o9V2TRhpji?q zO9FiCXitz5$5?KFiV7*szAeyNE8>x(K$sB~P*HIOJr2lX$F={MGKdyXnK7^3Ui;YuacBL}dWs74+Sa3p)04+0T(MTz!!_U>o>77ER=TjLxh5`; ziW(QFSnX2D|G*`^YGo6TSLFE`_xFl~I~56cDiVD4xTBkO5A&D%AO2qYe*q9<-C&IH R{8#`0002ovPDHLkV1hWzIc@*| literal 0 HcmV?d00001 diff --git a/static/images/secret_mid.png b/static/images/secret_mid.png new file mode 100644 index 0000000000000000000000000000000000000000..f97720300df7d0b429342306019c100843c51762 GIT binary patch literal 1490 zcmV;@1ugoCP)Px#1ZP1_K>z@;j|==^1poj5W>8F2Mfc?6!zv&D|Ns5?`0&cd^3cxWo0j2}kLIYP z^w-top`H2b>fVTf$2Tp>J~YckJJMiR`||PJfqKO;Cg`uL>b0`&!@%skx!{h7&{+Ts4Ajzjo`G>1++cq%F2O}5;lr(*qqiGj_ z&d`3b2wvdVv;ob~9!ROh&KrvBU_z+6UXlxF+THGBw>zth-^RVjv83H-u(s_v4jb)j z+nngc7b*Ed&mcF1VQ4>1uIt!G@d}N*l6$l0^#Ypq9bR%kRUaxv#p=Wo&k-<%=(3Vr z)@^&l+IWF6=QY_O*(TT>DoDBf9|#1+9er*msL^+pskE3)pCGvlvXGre&-4 zM!Z|aB?THx#+8pHCG=w9H9@R2pK9U}{_tihm&9n)2T+Ilfj6s;F4kFX0xSa}+O27E z)NEjC&#nQ1M&EuppVhTP%&A#D>8BACJF8D5#WrE9*c8rHbQfRl#m8Z5z-@!9?YkEd zbKJV_hc~xCt6(7MO(V|U4yS^JKq&-mHw(<5XK?#*{8K_66~HBg9qXOd&O$G%To5=| zZ^NJfUZt3ZWZa@spTqRw2zPuEL@pqbHR6T230tZd5?$UBq)ces72l4o12L785Zn;q z66UbS_nI{}3?#j2M53IKKE^Q0fz$>RPx>f4*xJUdx(yFChUD--JohSi{TR^Nk72M~ zI>(;Jz;-k-?G3tFj@PVQ4!G^hXyQL z;js;952b?X?~qoVGLQh=03yUWT&rAYZq3jalz5qMK#!wC3w?+@88l`-z+6 z%+oXmHcATqLP;IAbaDQ@+|eUrxK!HA=n*d zvc`IDZCcZZ;eTY!C-Wg_n~&-yY?tKpt+@BUc=vU?PlpFD!b9=fweR5BUF$EFEOB!f zvLnjxGa3A)Mdm$y5P;V?0QDb zd3RlVQ$w?LwiiJ=jqkY@GtS$2cZxF}Cr4UW9H02S`HhB$YvL8S>zeBLTsDLD24;q$ zpE=AkSrz1fUZsIbOS~w$YSB=#-L=m=S|U~rDP;1vJK{!q zFw+W$b&L7bFSthRV!#qbWaMzIPK34Rm}woR`TdS-j+5znXq1u@>|W@rD-0vx+c4L< zX@XOzty*+cu8EABJI0ZZEt5lLiz*f z9oO3UL=L@b7%JED2wAe2=c}1(#BN~H9KT-vHd?s(AA>SD$8b~hkC&>Ka@l~=bNXK_ sma63reJcER?K^m8*OvZy`V0u|KQ%O6QSeBH>Hq)$07*qoM6N<$g1M&XWdHyG literal 0 HcmV?d00001 diff --git a/static/images/secret_small.png b/static/images/secret_small.png new file mode 100644 index 0000000000000000000000000000000000000000..f8d6cc03c54a9659ade9e6bddc748fc7fca43da1 GIT binary patch literal 3226 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 M07*qoM6N<$g4@kImjD0& literal 0 HcmV?d00001 diff --git a/static/js/alpine/components/widgets.js b/static/js/alpine/components/widgets.js index a0577b2e..d426e725 100644 --- a/static/js/alpine/components/widgets.js +++ b/static/js/alpine/components/widgets.js @@ -59,6 +59,9 @@ document.addEventListener("alpine:init", () => { get isEditMode() { return this.mode === "edit"; }, + get isRequired() { + return (this.schema.required || []).indexOf(this.fieldName) > -1 + } }); // Text widget diff --git a/static/js/pw.js b/static/js/pw.js index 682b3b59..80c964a6 100644 --- a/static/js/pw.js +++ b/static/js/pw.js @@ -22,7 +22,7 @@ function predictFromNode(url) { // data = {{ pathway.d3_json | safe }}; // elem = 'vizdiv' function draw(pathway, elem) { - + const initialzoom = 2.5 const nodeRadius = 20; const linkDistance = 100; const chargeStrength = -200; @@ -63,7 +63,7 @@ function draw(pathway, elem) { node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2; } - node.fy = node.depth * levelSpacing + 50; + node.fy = (node.depth + initialzoom + 0.5) * levelSpacing + 50; depthMap.set(node.depth, depthMap.get(node.depth) + 1); }); } @@ -572,11 +572,12 @@ function draw(pathway, elem) { .scaleExtent([0.5, 5]) .on("zoom", (event) => { zoomable.attr("transform", event.transform); - }); + }) + // Apply zoom to the SVG element - this enables wheel zoom svg.call(zoom); - + svg.call(zoom.scaleBy, initialzoom); // Also apply zoom to container to catch events that might not reach SVG // This ensures drag-to-pan works even when clicking on empty space container.call(zoom); @@ -624,6 +625,36 @@ function draw(pathway, elem) { return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)'; }) .attr("fill", "none") + .on("click", function(event, d) { + const wasHighlighted = d3.select(this).classed("highlighted"); + + d3.selectAll("path").classed("highlighted", false); + + if (!wasHighlighted) { + const toHighlight = []; + toHighlight.push(d.el); + + if (d.source.pseudo || d.target.pseudo) { + if (d.target.pseudo) { + d3.selectAll("path").each(e => { + if (e !== undefined && e.source.id === d.target.id) { + toHighlight.push(e.el); + } + }); + } else { + d3.selectAll("path").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) { @@ -642,7 +673,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 %}