forked from enviPath/enviPy
[Feature] PEPPER in enviPath (#332)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#332
This commit is contained in:
@ -2,6 +2,7 @@ from django.conf import settings as s
|
||||
from django.contrib import admin
|
||||
|
||||
from .models import (
|
||||
AdditionalInformation,
|
||||
Compound,
|
||||
CompoundStructure,
|
||||
Edge,
|
||||
@ -16,6 +17,7 @@ from .models import (
|
||||
Node,
|
||||
ParallelRule,
|
||||
Pathway,
|
||||
PropertyPluginModel,
|
||||
Reaction,
|
||||
Scenario,
|
||||
Setting,
|
||||
@ -27,8 +29,20 @@ from .models import (
|
||||
Package = s.GET_PACKAGE_MODEL()
|
||||
|
||||
|
||||
class AdditionalInformationAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class UserAdmin(admin.ModelAdmin):
|
||||
list_display = ["username", "email", "is_active", "is_staff", "is_superuser"]
|
||||
list_display = [
|
||||
"username",
|
||||
"email",
|
||||
"is_active",
|
||||
"is_staff",
|
||||
"is_superuser",
|
||||
"last_login",
|
||||
"date_joined",
|
||||
]
|
||||
|
||||
|
||||
class UserPackagePermissionAdmin(admin.ModelAdmin):
|
||||
@ -65,6 +79,10 @@ class EnviFormerAdmin(EPAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class PropertyPluginModelAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
class LicenseAdmin(admin.ModelAdmin):
|
||||
list_display = ["cc_string", "link", "image_link"]
|
||||
|
||||
@ -117,6 +135,7 @@ class ExternalIdentifierAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
admin.site.register(AdditionalInformation, AdditionalInformationAdmin)
|
||||
admin.site.register(User, UserAdmin)
|
||||
admin.site.register(UserPackagePermission, UserPackagePermissionAdmin)
|
||||
admin.site.register(Group, GroupAdmin)
|
||||
@ -125,6 +144,7 @@ admin.site.register(JobLog, JobLogAdmin)
|
||||
admin.site.register(Package, PackageAdmin)
|
||||
admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin)
|
||||
admin.site.register(EnviFormer, EnviFormerAdmin)
|
||||
admin.site.register(PropertyPluginModel, PropertyPluginModelAdmin)
|
||||
admin.site.register(License, LicenseAdmin)
|
||||
admin.site.register(Compound, CompoundAdmin)
|
||||
admin.site.register(CompoundStructure, CompoundStructureAdmin)
|
||||
|
||||
@ -15,3 +15,9 @@ class EPDBConfig(AppConfig):
|
||||
|
||||
model_name = getattr(settings, "EPDB_PACKAGE_MODEL", "epdb.Package")
|
||||
logger.info(f"Using Package model: {model_name}")
|
||||
|
||||
if settings.PLUGINS_ENABLED:
|
||||
from bridge.contracts import Property
|
||||
from utilities.plugin import discover_plugins
|
||||
|
||||
settings.PROPERTY_PLUGINS.update(**discover_plugins(_cls=Property))
|
||||
|
||||
@ -22,6 +22,7 @@ from epdb.models import (
|
||||
Node,
|
||||
Pathway,
|
||||
Permission,
|
||||
PropertyPluginModel,
|
||||
Reaction,
|
||||
Rule,
|
||||
Setting,
|
||||
@ -1109,10 +1110,11 @@ class SettingManager(object):
|
||||
description: str = None,
|
||||
max_nodes: int = None,
|
||||
max_depth: int = None,
|
||||
rule_packages: List[Package] = None,
|
||||
rule_packages: List[Package] | None = None,
|
||||
model: EPModel = None,
|
||||
model_threshold: float = None,
|
||||
expansion_scheme: ExpansionSchemeChoice = ExpansionSchemeChoice.BFS,
|
||||
property_models: List["PropertyPluginModel"] | None = None,
|
||||
):
|
||||
new_s = Setting()
|
||||
|
||||
@ -1133,6 +1135,11 @@ class SettingManager(object):
|
||||
new_s.rule_packages.add(r)
|
||||
new_s.save()
|
||||
|
||||
if property_models is not None:
|
||||
for pm in property_models:
|
||||
new_s.property_models.add(pm)
|
||||
new_s.save()
|
||||
|
||||
usp = UserSettingPermission()
|
||||
usp.user = user
|
||||
usp.setting = new_s
|
||||
|
||||
@ -41,9 +41,7 @@ class Command(BaseCommand):
|
||||
"SequentialRule",
|
||||
"Scenario",
|
||||
"Setting",
|
||||
"MLRelativeReasoning",
|
||||
"RuleBasedRelativeReasoning",
|
||||
"EnviFormer",
|
||||
"EPModel",
|
||||
"ApplicabilityDomain",
|
||||
"EnzymeLink",
|
||||
]
|
||||
|
||||
76
epdb/management/commands/recreate_db.py
Normal file
76
epdb/management/commands/recreate_db.py
Normal file
@ -0,0 +1,76 @@
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
from django.core.management import call_command
|
||||
from django.core.management.base import BaseCommand
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
def add_arguments(self, parser):
|
||||
parser.add_argument(
|
||||
"-n",
|
||||
"--name",
|
||||
type=str,
|
||||
help="Name of the database to recreate. Default is 'appdb'",
|
||||
default="appdb",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--dump",
|
||||
type=str,
|
||||
help="Path to the dump file",
|
||||
default="./fixtures/db.dump",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-ou",
|
||||
"--oldurl",
|
||||
type=str,
|
||||
help="Old URL, e.g. https://envipath.org/",
|
||||
default="https://envipath.org/",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
"-nu",
|
||||
"--newurl",
|
||||
type=str,
|
||||
help="New URL, e.g. http://localhost:8000/",
|
||||
default="http://localhost:8000/",
|
||||
)
|
||||
|
||||
def handle(self, *args, **options):
|
||||
dump_file = options["dump"]
|
||||
|
||||
if not os.path.exists(dump_file):
|
||||
raise ValueError(f"Dump file {dump_file} does not exist")
|
||||
|
||||
print(f"Dropping database {options['name']} y/n: ", end="")
|
||||
|
||||
if input() in "yY":
|
||||
result = subprocess.run(
|
||||
["dropdb", "appdb"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
print(result.stdout)
|
||||
else:
|
||||
raise ValueError("Aborted")
|
||||
|
||||
print(f"Creating database {options['name']}")
|
||||
|
||||
result = subprocess.run(
|
||||
["createdb", "appdb"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
print(result.stdout)
|
||||
print(f"Restoring database {options['name']} from {dump_file}")
|
||||
|
||||
result = subprocess.run(
|
||||
["pg_restore", "-d", "appdb", dump_file, "--no-owner"],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
print(result.stdout)
|
||||
call_command("localize_urls", "--old", options["oldurl"], "--new", options["newurl"])
|
||||
179
epdb/migrations/0016_remove_enviformer_model_status_and_more.py
Normal file
179
epdb/migrations/0016_remove_enviformer_model_status_and_more.py
Normal file
@ -0,0 +1,179 @@
|
||||
# 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",
|
||||
),
|
||||
]
|
||||
93
epdb/migrations/0017_additionalinformation.py
Normal file
93
epdb/migrations/0017_additionalinformation.py
Normal file
@ -0,0 +1,93 @@
|
||||
# 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",
|
||||
),
|
||||
],
|
||||
},
|
||||
),
|
||||
]
|
||||
132
epdb/migrations/0018_auto_20260220_1203.py
Normal file
132
epdb/migrations/0018_auto_20260220_1203.py
Normal file
@ -0,0 +1,132 @@
|
||||
# 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),
|
||||
]
|
||||
@ -0,0 +1,20 @@
|
||||
# Generated by Django 5.2.7 on 2026-02-23 08:45
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("epdb", "0018_auto_20260220_1203"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name="scenario",
|
||||
name="additional_information",
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name="scenario",
|
||||
name="parent",
|
||||
),
|
||||
]
|
||||
605
epdb/models.py
605
epdb/models.py
@ -29,6 +29,8 @@ from polymorphic.models import PolymorphicModel
|
||||
from sklearn.metrics import jaccard_score, precision_score, recall_score
|
||||
from sklearn.model_selection import ShuffleSplit
|
||||
|
||||
from bridge.contracts import Property
|
||||
from bridge.dto import RunResult, PredictedProperty
|
||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||
from utilities.ml import (
|
||||
ApplicabilityDomainPCA,
|
||||
@ -667,6 +669,23 @@ class ScenarioMixin(models.Model):
|
||||
abstract = True
|
||||
|
||||
|
||||
class AdditionalInformationMixin(models.Model):
|
||||
"""
|
||||
Optional mixin: lets you do compound.additional_information.all()
|
||||
without an explicit M2M table.
|
||||
"""
|
||||
|
||||
additional_information = GenericRelation(
|
||||
"epdb.AdditionalInformation",
|
||||
content_type_field="content_type",
|
||||
object_id_field="object_id",
|
||||
related_query_name="target",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class License(models.Model):
|
||||
cc_string = models.TextField(blank=False, null=False, verbose_name="CC string")
|
||||
link = models.URLField(blank=False, null=False, verbose_name="link")
|
||||
@ -745,7 +764,9 @@ class Package(EnviPathModel):
|
||||
swappable = "EPDB_PACKAGE_MODEL"
|
||||
|
||||
|
||||
class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin):
|
||||
class Compound(
|
||||
EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin
|
||||
):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -1073,7 +1094,9 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
||||
unique_together = [("uuid", "package")]
|
||||
|
||||
|
||||
class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin):
|
||||
class CompoundStructure(
|
||||
EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin
|
||||
):
|
||||
compound = models.ForeignKey("epdb.Compound", on_delete=models.CASCADE, db_index=True)
|
||||
smiles = models.TextField(blank=False, null=False, verbose_name="SMILES")
|
||||
canonical_smiles = models.TextField(blank=False, null=False, verbose_name="Canonical SMILES")
|
||||
@ -1167,10 +1190,11 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
|
||||
hls: Dict[Scenario, List[HalfLife]] = defaultdict(list)
|
||||
|
||||
for n in self.related_nodes:
|
||||
for scen in n.scenarios.all().order_by("name"):
|
||||
for ai in scen.get_additional_information():
|
||||
if isinstance(ai, HalfLife):
|
||||
hls[scen].append(ai)
|
||||
for ai in n.additional_information.filter(scenario__isnull=False).order_by(
|
||||
"scenario__name"
|
||||
):
|
||||
if isinstance(ai.get(), HalfLife):
|
||||
hls[ai.scenario].append(ai.get())
|
||||
|
||||
return dict(hls)
|
||||
|
||||
@ -1195,7 +1219,7 @@ class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
|
||||
return ".".join(self.ec_number.split(".")[:3]) + ".-"
|
||||
|
||||
|
||||
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -1424,8 +1448,6 @@ class SimpleRDKitRule(SimpleRule):
|
||||
return "{}/simple-rdkit-rule/{}".format(self.package.url, self.uuid)
|
||||
|
||||
|
||||
#
|
||||
#
|
||||
class ParallelRule(Rule):
|
||||
simple_rules = models.ManyToManyField("epdb.SimpleRule", verbose_name="Simple rules")
|
||||
|
||||
@ -1561,7 +1583,9 @@ class SequentialRuleOrdering(models.Model):
|
||||
order_index = models.IntegerField(null=False, blank=False)
|
||||
|
||||
|
||||
class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin):
|
||||
class Reaction(
|
||||
EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin, AdditionalInformationMixin
|
||||
):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -1757,7 +1781,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
|
||||
return res
|
||||
|
||||
|
||||
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -2140,7 +2164,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
||||
|
||||
|
||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
pathway = models.ForeignKey(
|
||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -2175,6 +2199,11 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
def d3_json(self):
|
||||
app_domain_data = self.get_app_domain_assessment_data()
|
||||
|
||||
predicted_properties = defaultdict(list)
|
||||
for ai in self.additional_information.all():
|
||||
if isinstance(ai.get(), PredictedProperty):
|
||||
predicted_properties[ai.get().__class__.__name__].append(ai.data)
|
||||
|
||||
return {
|
||||
"depth": self.depth,
|
||||
"stereo_removed": self.stereo_removed,
|
||||
@ -2193,6 +2222,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
else None,
|
||||
"uncovered_functional_groups": False,
|
||||
},
|
||||
"predicted_properties": predicted_properties,
|
||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||
"timeseries": self.get_timeseries_data(),
|
||||
}
|
||||
@ -2210,6 +2240,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
||||
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||
stereo_removed = True
|
||||
|
||||
c = Compound.create(pathway.package, smiles, name=name, description=description)
|
||||
|
||||
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
|
||||
@ -2233,10 +2264,10 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
||||
|
||||
def get_timeseries_data(self):
|
||||
for scenario in self.scenarios.all():
|
||||
for ai in scenario.get_additional_information():
|
||||
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
||||
return ai.model_dump(mode="json")
|
||||
for ai in self.additional_information.all():
|
||||
if ai.__class__.__name__ == "OECD301FTimeSeries":
|
||||
return ai.model_dump(mode="json")
|
||||
|
||||
return None
|
||||
|
||||
def get_app_domain_assessment_data(self):
|
||||
@ -2267,7 +2298,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
return res
|
||||
|
||||
|
||||
class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
pathway = models.ForeignKey(
|
||||
"epdb.Pathway", verbose_name="belongs to", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
@ -2409,38 +2440,11 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
)
|
||||
|
||||
|
||||
class EPModel(PolymorphicModel, EnviPathModel):
|
||||
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
|
||||
def _url(self):
|
||||
return "{}/model/{}".format(self.package.url, self.uuid)
|
||||
|
||||
|
||||
class PackageBasedModel(EPModel):
|
||||
rule_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
)
|
||||
data_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Data Packages",
|
||||
related_name="%(app_label)s_%(class)s_data_packages",
|
||||
)
|
||||
eval_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
)
|
||||
threshold = models.FloatField(null=False, blank=False, default=0.5)
|
||||
eval_results = JSONField(null=True, blank=True, default=dict)
|
||||
app_domain = models.ForeignKey(
|
||||
"epdb.ApplicabilityDomain", on_delete=models.SET_NULL, null=True, blank=True, default=None
|
||||
)
|
||||
multigen_eval = models.BooleanField(null=False, blank=False, default=False)
|
||||
|
||||
INITIAL = "INITIAL"
|
||||
INITIALIZING = "INITIALIZING"
|
||||
BUILDING = "BUILDING"
|
||||
@ -2467,6 +2471,35 @@ class PackageBasedModel(EPModel):
|
||||
def ready_for_prediction(self) -> bool:
|
||||
return self.model_status in [self.BUILT_NOT_EVALUATED, self.EVALUATING, self.FINISHED]
|
||||
|
||||
def _url(self):
|
||||
return "{}/model/{}".format(self.package.url, self.uuid)
|
||||
|
||||
|
||||
class PackageBasedModel(EPModel):
|
||||
rule_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
blank=True,
|
||||
)
|
||||
data_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Data Packages",
|
||||
related_name="%(app_label)s_%(class)s_data_packages",
|
||||
)
|
||||
eval_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
blank=True,
|
||||
)
|
||||
threshold = models.FloatField(null=False, blank=False, default=0.5)
|
||||
eval_results = JSONField(null=True, blank=True, default=dict)
|
||||
app_domain = models.ForeignKey(
|
||||
"epdb.ApplicabilityDomain", on_delete=models.SET_NULL, null=True, blank=True, default=None
|
||||
)
|
||||
multigen_eval = models.BooleanField(null=False, blank=False, default=False)
|
||||
|
||||
@property
|
||||
def pr_curve(self):
|
||||
if self.model_status != self.FINISHED:
|
||||
@ -3011,7 +3044,7 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
|
||||
mod = joblib.load(os.path.join(s.MODEL_DIR, f"{self.uuid}_mod.pkl"))
|
||||
return mod
|
||||
|
||||
def predict(self, smiles) -> List["PredictionResult"]:
|
||||
def predict(self, smiles, *args, **kwargs) -> List["PredictionResult"]:
|
||||
start = datetime.now()
|
||||
ds = self.load_dataset()
|
||||
classify_ds, classify_prods = ds.classification_dataset([smiles], self.applicable_rules)
|
||||
@ -3111,7 +3144,7 @@ class MLRelativeReasoning(PackageBasedModel):
|
||||
mod.base_clf.n_jobs = -1
|
||||
return mod
|
||||
|
||||
def predict(self, smiles) -> List["PredictionResult"]:
|
||||
def predict(self, smiles, *args, **kwargs) -> List["PredictionResult"]:
|
||||
start = datetime.now()
|
||||
ds = self.load_dataset()
|
||||
classify_ds, classify_prods = ds.classification_dataset([smiles], self.applicable_rules)
|
||||
@ -3419,16 +3452,16 @@ class EnviFormer(PackageBasedModel):
|
||||
mod = load(device=s.ENVIFORMER_DEVICE, ckpt_path=ckpt)
|
||||
return mod
|
||||
|
||||
def predict(self, smiles) -> List["PredictionResult"]:
|
||||
def predict(self, smiles, *args, **kwargs) -> List["PredictionResult"]:
|
||||
return self.predict_batch([smiles])[0]
|
||||
|
||||
def predict_batch(self, smiles_list):
|
||||
def predict_batch(self, smiles: List[str], *args, **kwargs):
|
||||
# Standardizer removes all but one compound from a raw SMILES string, so they need to be processed separately
|
||||
canon_smiles = [
|
||||
".".join(
|
||||
[FormatConverter.standardize(s, remove_stereo=True) for s in smiles.split(".")]
|
||||
)
|
||||
for smiles in smiles_list
|
||||
for smi in smiles
|
||||
]
|
||||
logger.info(f"Submitting {canon_smiles} to {self.get_name()}")
|
||||
start = datetime.now()
|
||||
@ -3777,8 +3810,216 @@ class EnviFormer(PackageBasedModel):
|
||||
return []
|
||||
|
||||
|
||||
class PluginModel(EPModel):
|
||||
pass
|
||||
class PropertyPluginModel(PackageBasedModel):
|
||||
plugin_identifier = models.CharField(max_length=255)
|
||||
|
||||
rule_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Rule Packages",
|
||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||
blank=True,
|
||||
)
|
||||
data_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Data Packages",
|
||||
related_name="%(app_label)s_%(class)s_data_packages",
|
||||
blank=True,
|
||||
)
|
||||
eval_packages = models.ManyToManyField(
|
||||
s.EPDB_PACKAGE_MODEL,
|
||||
verbose_name="Evaluation Packages",
|
||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def create(
|
||||
package: "Package",
|
||||
plugin_identifier: str,
|
||||
rule_packages: List["Package"] | None,
|
||||
data_packages: List["Package"] | None,
|
||||
name: "str" = None,
|
||||
description: str = None,
|
||||
):
|
||||
mod = PropertyPluginModel()
|
||||
mod.package = package
|
||||
|
||||
# Clean for potential XSS
|
||||
if name is not None:
|
||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if name is None or name == "":
|
||||
name = f"PropertyPluginModel {PropertyPluginModel.objects.filter(package=package).count() + 1}"
|
||||
|
||||
mod.name = name
|
||||
|
||||
if description is not None and description.strip() != "":
|
||||
mod.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if plugin_identifier is None:
|
||||
raise ValueError("Plugin identifier must be set")
|
||||
|
||||
impl = s.PROPERTY_PLUGINS.get(plugin_identifier, None)
|
||||
|
||||
if impl is None:
|
||||
raise ValueError(f"Unknown plugin identifier: {plugin_identifier}")
|
||||
|
||||
inst = impl()
|
||||
|
||||
mod.plugin_identifier = plugin_identifier
|
||||
|
||||
if inst.requires_rule_packages() and (rule_packages is None or len(rule_packages) == 0):
|
||||
raise ValueError("Plugin requires rules but none were provided")
|
||||
elif not inst.requires_rule_packages() and (
|
||||
rule_packages is not None and len(rule_packages) > 0
|
||||
):
|
||||
raise ValueError("Plugin does not require rules but some were provided")
|
||||
|
||||
if rule_packages is None:
|
||||
rule_packages = []
|
||||
|
||||
if inst.requires_data_packages() and (data_packages is None or len(data_packages) == 0):
|
||||
raise ValueError("Plugin requires data but none were provided")
|
||||
elif not inst.requires_data_packages() and (
|
||||
data_packages is not None and len(data_packages) > 0
|
||||
):
|
||||
raise ValueError("Plugin does not require data but some were provided")
|
||||
|
||||
if data_packages is None:
|
||||
data_packages = []
|
||||
|
||||
mod.save()
|
||||
|
||||
for p in rule_packages:
|
||||
mod.rule_packages.add(p)
|
||||
|
||||
for p in data_packages:
|
||||
mod.data_packages.add(p)
|
||||
|
||||
mod.save()
|
||||
return mod
|
||||
|
||||
def instance(self) -> "Property":
|
||||
"""
|
||||
Returns an instance of the plugin implementation.
|
||||
|
||||
This method retrieves the implementation of the plugin identified by
|
||||
`self.plugin_identifier` from the `PROPERTY_PLUGINS` mapping, then
|
||||
instantiates and returns it.
|
||||
|
||||
Returns:
|
||||
object: An instance of the plugin implementation.
|
||||
"""
|
||||
impl = s.PROPERTY_PLUGINS[self.plugin_identifier]
|
||||
instance = impl()
|
||||
return instance
|
||||
|
||||
def build_dataset(self):
|
||||
"""
|
||||
Required by general model contract but actual implementation resides in plugin.
|
||||
"""
|
||||
return
|
||||
|
||||
def build_model(self):
|
||||
from bridge.dto import BaseDTO
|
||||
|
||||
self.model_status = self.BUILDING
|
||||
self.save()
|
||||
|
||||
compounds = CompoundStructure.objects.filter(compound__package__in=self.data_packages.all())
|
||||
reactions = Reaction.objects.filter(package__in=self.data_packages.all())
|
||||
rules = Rule.objects.filter(package__in=self.rule_packages.all())
|
||||
|
||||
eP = BaseDTO(str(self.uuid), self.url, s.MODEL_DIR, compounds, reactions, rules)
|
||||
|
||||
instance = self.instance()
|
||||
|
||||
_ = instance.build(eP)
|
||||
|
||||
self.model_status = self.BUILT_NOT_EVALUATED
|
||||
self.save()
|
||||
|
||||
def predict(self, smiles, *args, **kwargs) -> RunResult:
|
||||
return self.predict_batch([smiles], *args, **kwargs)
|
||||
|
||||
def predict_batch(self, smiles: List[str], *args, **kwargs) -> RunResult:
|
||||
from bridge.dto import BaseDTO, CompoundProto
|
||||
from dataclasses import dataclass
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class TempCompound(CompoundProto):
|
||||
url = None
|
||||
name = None
|
||||
smiles: str
|
||||
|
||||
batch = [TempCompound(smiles=smi) for smi in smiles]
|
||||
|
||||
reactions = Reaction.objects.filter(package__in=self.data_packages.all())
|
||||
rules = Rule.objects.filter(package__in=self.rule_packages.all())
|
||||
|
||||
eP = BaseDTO(str(self.uuid), self.url, s.MODEL_DIR, batch, reactions, rules)
|
||||
|
||||
instance = self.instance()
|
||||
|
||||
return instance.run(eP, *args, **kwargs)
|
||||
|
||||
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
|
||||
from bridge.dto import BaseDTO
|
||||
|
||||
if self.model_status != self.BUILT_NOT_EVALUATED:
|
||||
raise ValueError("Model must be built before evaluation")
|
||||
|
||||
self.model_status = self.EVALUATING
|
||||
self.save()
|
||||
|
||||
if eval_packages is not None:
|
||||
for p in eval_packages:
|
||||
self.eval_packages.add(p)
|
||||
|
||||
rules = Rule.objects.filter(package__in=self.rule_packages.all())
|
||||
|
||||
if self.eval_packages.count() > 0:
|
||||
reactions = Reaction.objects.filter(package__in=self.data_packages.all())
|
||||
compounds = CompoundStructure.objects.filter(
|
||||
compound__package__in=self.data_packages.all()
|
||||
)
|
||||
else:
|
||||
reactions = Reaction.objects.filter(package__in=self.eval_packages.all())
|
||||
compounds = CompoundStructure.objects.filter(
|
||||
compound__package__in=self.eval_packages.all()
|
||||
)
|
||||
|
||||
eP = BaseDTO(str(self.uuid), self.url, s.MODEL_DIR, compounds, reactions, rules)
|
||||
|
||||
instance = self.instance()
|
||||
|
||||
try:
|
||||
if self.eval_packages.count() > 0:
|
||||
res = instance.evaluate(eP, **kwargs)
|
||||
self.eval_results = res.data
|
||||
else:
|
||||
res = instance.build_and_evaluate(eP)
|
||||
self.eval_results = self.compute_averages(res.data)
|
||||
|
||||
self.model_status = self.FINISHED
|
||||
self.save()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error during evaluation: {type(e).__name__}, {e}")
|
||||
self.model_status = self.ERROR
|
||||
self.save()
|
||||
|
||||
return res
|
||||
|
||||
@staticmethod
|
||||
def compute_averages(data):
|
||||
sum_dict = {}
|
||||
for result in data:
|
||||
for key, value in result.items():
|
||||
sum_dict.setdefault(key, []).append(value)
|
||||
sum_dict = {k: sum(v) / len(data) for k, v in sum_dict.items()}
|
||||
return sum_dict
|
||||
|
||||
|
||||
class Scenario(EnviPathModel):
|
||||
@ -3790,11 +4031,6 @@ class Scenario(EnviPathModel):
|
||||
max_length=256, null=False, blank=False, default="Not specified"
|
||||
)
|
||||
|
||||
# for Referring Scenarios this property will be filled
|
||||
parent = models.ForeignKey("self", on_delete=models.CASCADE, default=None, null=True)
|
||||
|
||||
additional_information = models.JSONField(verbose_name="Additional Information")
|
||||
|
||||
def _url(self):
|
||||
return "{}/scenario/{}".format(self.package.url, self.uuid)
|
||||
|
||||
@ -3810,11 +4046,14 @@ class Scenario(EnviPathModel):
|
||||
):
|
||||
new_s = Scenario()
|
||||
new_s.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"Scenario {Scenario.objects.filter(package=package).count() + 1}"
|
||||
|
||||
new_s.name = name
|
||||
|
||||
if description is not None and description.strip() != "":
|
||||
@ -3826,19 +4065,14 @@ class Scenario(EnviPathModel):
|
||||
if scenario_type is not None and scenario_type.strip() != "":
|
||||
new_s.scenario_type = scenario_type
|
||||
|
||||
add_inf = defaultdict(list)
|
||||
|
||||
for info in additional_information:
|
||||
cls_name = info.__class__.__name__
|
||||
# Clean for potential XSS hidden in the additional information fields.
|
||||
ai_data = json.loads(nh3.clean(info.model_dump_json()).strip())
|
||||
ai_data["uuid"] = f"{uuid4()}"
|
||||
add_inf[cls_name].append(ai_data)
|
||||
|
||||
new_s.additional_information = add_inf
|
||||
# TODO Remove
|
||||
new_s.additional_information = {}
|
||||
|
||||
new_s.save()
|
||||
|
||||
for ai in additional_information:
|
||||
AdditionalInformation.create(package, ai, scenario=new_s)
|
||||
|
||||
return new_s
|
||||
|
||||
@transaction.atomic
|
||||
@ -3852,19 +4086,9 @@ class Scenario(EnviPathModel):
|
||||
Returns:
|
||||
str: UUID of the created item
|
||||
"""
|
||||
cls_name = data.__class__.__name__
|
||||
# Clean for potential XSS hidden in the additional information fields.
|
||||
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
|
||||
generated_uuid = str(uuid4())
|
||||
ai_data["uuid"] = generated_uuid
|
||||
ai = AdditionalInformation.create(self.package, ai=data, scenario=self)
|
||||
|
||||
if cls_name not in self.additional_information:
|
||||
self.additional_information[cls_name] = []
|
||||
|
||||
self.additional_information[cls_name].append(ai_data)
|
||||
self.save()
|
||||
|
||||
return generated_uuid
|
||||
return str(ai.uuid)
|
||||
|
||||
@transaction.atomic
|
||||
def update_additional_information(self, ai_uuid: str, data: "EnviPyModel") -> None:
|
||||
@ -3878,110 +4102,158 @@ class Scenario(EnviPathModel):
|
||||
Raises:
|
||||
ValueError: If item with given UUID not found or type mismatch
|
||||
"""
|
||||
found_type = None
|
||||
found_idx = -1
|
||||
ai = AdditionalInformation.objects.filter(uuid=ai_uuid, scenario=self)
|
||||
|
||||
# Find the item by UUID
|
||||
for type_name, items in self.additional_information.items():
|
||||
for idx, item_data in enumerate(items):
|
||||
if item_data.get("uuid") == ai_uuid:
|
||||
found_type = type_name
|
||||
found_idx = idx
|
||||
break
|
||||
if found_type:
|
||||
break
|
||||
if ai.exists() and ai.count() == 1:
|
||||
ai = ai.first()
|
||||
# Verify the model type matches (prevent type changes)
|
||||
new_type = data.__class__.__name__
|
||||
if new_type != ai.type:
|
||||
raise ValueError(
|
||||
f"Cannot change type from {ai.type} to {new_type}. "
|
||||
f"Delete and create a new item instead."
|
||||
)
|
||||
|
||||
if found_type is None:
|
||||
ai.data = data.__class__(
|
||||
**json.loads(nh3.clean(data.model_dump_json()).strip())
|
||||
).model_dump(mode="json")
|
||||
ai.save()
|
||||
else:
|
||||
raise ValueError(f"Additional information with UUID {ai_uuid} not found")
|
||||
|
||||
# Verify the model type matches (prevent type changes)
|
||||
new_type = data.__class__.__name__
|
||||
if new_type != found_type:
|
||||
raise ValueError(
|
||||
f"Cannot change type from {found_type} to {new_type}. "
|
||||
f"Delete and create a new item instead."
|
||||
)
|
||||
|
||||
# Update the item data, preserving UUID
|
||||
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
|
||||
ai_data["uuid"] = ai_uuid
|
||||
|
||||
self.additional_information[found_type][found_idx] = ai_data
|
||||
self.save()
|
||||
|
||||
@transaction.atomic
|
||||
def remove_additional_information(self, ai_uuid):
|
||||
found_type = None
|
||||
found_idx = -1
|
||||
ai = AdditionalInformation.objects.filter(uuid=ai_uuid, scenario=self)
|
||||
|
||||
for k, vals in self.additional_information.items():
|
||||
for i, v in enumerate(vals):
|
||||
if v["uuid"] == ai_uuid:
|
||||
found_type = k
|
||||
found_idx = i
|
||||
break
|
||||
|
||||
if found_type is not None and found_idx >= 0:
|
||||
if len(self.additional_information[found_type]) == 1:
|
||||
del self.additional_information[found_type]
|
||||
else:
|
||||
self.additional_information[found_type].pop(found_idx)
|
||||
self.save()
|
||||
if ai.exists() and ai.count() == 1:
|
||||
ai.delete()
|
||||
else:
|
||||
raise ValueError(f"Could not find additional information with uuid {ai_uuid}")
|
||||
|
||||
@transaction.atomic
|
||||
def set_additional_information(self, data: Dict[str, "EnviPyModel"]):
|
||||
new_ais = defaultdict(list)
|
||||
for k, vals in data.items():
|
||||
for v in vals:
|
||||
# Clean for potential XSS hidden in the additional information fields.
|
||||
ai_data = json.loads(nh3.clean(v.model_dump_json()).strip())
|
||||
if hasattr(v, "uuid"):
|
||||
ai_data["uuid"] = str(v.uuid)
|
||||
else:
|
||||
ai_data["uuid"] = str(uuid4())
|
||||
raise NotImplementedError("Not implemented yet")
|
||||
|
||||
new_ais[k].append(ai_data)
|
||||
def get_additional_information(self, direct_only=True):
|
||||
ais = AdditionalInformation.objects.filter(scenario=self)
|
||||
|
||||
self.additional_information = new_ais
|
||||
self.save()
|
||||
|
||||
def get_additional_information(self):
|
||||
from envipy_additional_information import registry
|
||||
|
||||
for k, vals in self.additional_information.items():
|
||||
if k == "enzyme":
|
||||
continue
|
||||
|
||||
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 as e:
|
||||
logger.error(f"Could not load additional information {k}: {e}")
|
||||
if s.SENTRY_ENABLED:
|
||||
from sentry_sdk import capture_exception
|
||||
|
||||
capture_exception(e)
|
||||
|
||||
# Add uuid to uniquely identify objects for manipulation
|
||||
if "uuid" in v:
|
||||
inst.__dict__["uuid"] = v["uuid"]
|
||||
|
||||
yield inst
|
||||
if direct_only:
|
||||
return ais.filter(content_object__isnull=True)
|
||||
else:
|
||||
return ais
|
||||
|
||||
def related_pathways(self):
|
||||
scens = [self]
|
||||
if self.parent is not None:
|
||||
scens.append(self.parent)
|
||||
|
||||
return Pathway.objects.filter(
|
||||
scenarios__in=scens, package__reviewed=True, package=self.package
|
||||
scenarios=self, package__reviewed=True, package=self.package
|
||||
).distinct()
|
||||
|
||||
|
||||
class AdditionalInformation(models.Model):
|
||||
package = models.ForeignKey(
|
||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||
)
|
||||
uuid = models.UUIDField(unique=True, default=uuid4, editable=False)
|
||||
url = models.TextField(blank=False, null=True, verbose_name="URL", unique=True)
|
||||
kv = JSONField(null=True, blank=True, default=dict)
|
||||
# class name of pydantic model
|
||||
type = models.TextField(blank=False, null=False, verbose_name="Additional Information Type")
|
||||
# serialized pydantic model
|
||||
data = models.JSONField(null=True, blank=True, default=dict)
|
||||
|
||||
# The link to scenario is optional - e.g. when setting predicted properties to objects
|
||||
scenario = models.ForeignKey(
|
||||
"epdb.Scenario",
|
||||
null=True,
|
||||
blank=True,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="scenario_additional_information",
|
||||
)
|
||||
|
||||
# Generic target (Compound/Reaction/Pathway/...)
|
||||
content_type = models.ForeignKey(ContentType, null=True, blank=True, on_delete=models.CASCADE)
|
||||
object_id = models.PositiveBigIntegerField(null=True, blank=True)
|
||||
content_object = GenericForeignKey("content_type", "object_id")
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
package: "Package",
|
||||
ai: "EnviPyModel",
|
||||
scenario=None,
|
||||
content_object=None,
|
||||
skip_cleaning=False,
|
||||
):
|
||||
add_inf = AdditionalInformation()
|
||||
add_inf.package = package
|
||||
add_inf.type = ai.__class__.__name__
|
||||
|
||||
# dump, sanitize, validate before saving
|
||||
_ai = ai.__class__(**json.loads(nh3.clean(ai.model_dump_json()).strip()))
|
||||
|
||||
add_inf.data = _ai.model_dump(mode="json")
|
||||
|
||||
if scenario is not None:
|
||||
add_inf.scenario = scenario
|
||||
|
||||
if content_object is not None:
|
||||
add_inf.content_object = content_object
|
||||
|
||||
add_inf.save()
|
||||
|
||||
return add_inf
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.url:
|
||||
self.url = self._url()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _url(self):
|
||||
if self.content_object is not None:
|
||||
return f"{self.content_object.url}/additional-information/{self.uuid}"
|
||||
|
||||
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
||||
|
||||
def get(self) -> "EnviPyModel":
|
||||
from envipy_additional_information import registry
|
||||
|
||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||
try:
|
||||
inst = MAPPING[self.type](**self.data)
|
||||
except Exception as e:
|
||||
print(f"Error loading {self.type}: {e}")
|
||||
raise e
|
||||
|
||||
inst.__dict__["uuid"] = str(self.uuid)
|
||||
|
||||
return inst
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.type} ({self.uuid})"
|
||||
|
||||
class Meta:
|
||||
indexes = [
|
||||
models.Index(fields=["type"]),
|
||||
models.Index(fields=["scenario", "type"]),
|
||||
models.Index(fields=["content_type", "object_id"]),
|
||||
models.Index(fields=["scenario", "content_type", "object_id"]),
|
||||
]
|
||||
constraints = [
|
||||
# Generic FK must be complete or empty
|
||||
models.CheckConstraint(
|
||||
name="ck_addinfo_gfk_pair",
|
||||
check=(
|
||||
(Q(content_type__isnull=True) & Q(object_id__isnull=True))
|
||||
| (Q(content_type__isnull=False) & Q(object_id__isnull=False))
|
||||
),
|
||||
),
|
||||
# Disallow "floating" info
|
||||
models.CheckConstraint(
|
||||
name="ck_addinfo_not_both_null",
|
||||
check=Q(scenario__isnull=False) | Q(content_type__isnull=False),
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class UserSettingPermission(Permission):
|
||||
uuid = models.UUIDField(
|
||||
null=False, blank=False, verbose_name="UUID of this object", primary_key=True, default=uuid4
|
||||
@ -4028,6 +4300,13 @@ class Setting(EnviPathModel):
|
||||
null=True, blank=True, verbose_name="Setting Model Threshold", default=0.25
|
||||
)
|
||||
|
||||
property_models = models.ManyToManyField(
|
||||
"PropertyPluginModel",
|
||||
verbose_name="Setting Property Models",
|
||||
related_name="settings",
|
||||
blank=True,
|
||||
)
|
||||
|
||||
expansion_scheme = models.CharField(
|
||||
max_length=20,
|
||||
choices=ExpansionSchemeChoice.choices,
|
||||
|
||||
@ -11,7 +11,17 @@ from django.core.mail import EmailMultiAlternatives
|
||||
from django.utils import timezone
|
||||
|
||||
from epdb.logic import SPathway
|
||||
from epdb.models import Edge, EPModel, JobLog, Node, Pathway, Rule, Setting, User
|
||||
from epdb.models import (
|
||||
AdditionalInformation,
|
||||
Edge,
|
||||
EPModel,
|
||||
JobLog,
|
||||
Node,
|
||||
Pathway,
|
||||
Rule,
|
||||
Setting,
|
||||
User,
|
||||
)
|
||||
from utilities.chem import FormatConverter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -66,9 +76,9 @@ def mul(a, b):
|
||||
|
||||
|
||||
@shared_task(queue="predict")
|
||||
def predict_simple(model_pk: int, smiles: str):
|
||||
def predict_simple(model_pk: int, smiles: str, *args, **kwargs):
|
||||
mod = get_ml_model(model_pk)
|
||||
res = mod.predict(smiles)
|
||||
res = mod.predict(smiles, *args, **kwargs)
|
||||
return res
|
||||
|
||||
|
||||
@ -229,9 +239,28 @@ def predict(
|
||||
if JobLog.objects.filter(task_id=self.request.id).exists():
|
||||
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=pw.url)
|
||||
|
||||
# dispatch property job
|
||||
compute_properties.delay(pw_pk, pred_setting_pk)
|
||||
|
||||
return pw.url
|
||||
|
||||
|
||||
@shared_task(bind=True, queue="background")
|
||||
def compute_properties(self, pathway_pk: int, setting_pk: int):
|
||||
pw = Pathway.objects.get(id=pathway_pk)
|
||||
setting = Setting.objects.get(id=setting_pk)
|
||||
|
||||
nodes = [n for n in pw.nodes]
|
||||
smiles = [n.default_node_label.smiles for n in nodes]
|
||||
|
||||
for prop_mod in setting.property_models.all():
|
||||
if prop_mod.instance().is_heavy():
|
||||
rr = prop_mod.predict_batch(smiles)
|
||||
for idx, pred in enumerate(rr.result):
|
||||
n = nodes[idx]
|
||||
_ = AdditionalInformation.create(pw.package, ai=pred, content_object=n)
|
||||
|
||||
|
||||
@shared_task(bind=True, queue="background")
|
||||
def identify_missing_rules(
|
||||
self,
|
||||
|
||||
158
epdb/views.py
158
epdb/views.py
@ -1,7 +1,7 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Any, Dict, List
|
||||
from typing import Any, Dict, List, Iterable
|
||||
|
||||
import nh3
|
||||
from django.conf import settings as s
|
||||
@ -28,6 +28,7 @@ from .logic import (
|
||||
UserManager,
|
||||
)
|
||||
from .models import (
|
||||
AdditionalInformation,
|
||||
APIToken,
|
||||
Compound,
|
||||
CompoundStructure,
|
||||
@ -46,6 +47,7 @@ from .models import (
|
||||
Node,
|
||||
Pathway,
|
||||
Permission,
|
||||
PropertyPluginModel,
|
||||
Reaction,
|
||||
Rule,
|
||||
RuleBasedRelativeReasoning,
|
||||
@ -401,7 +403,7 @@ def breadcrumbs(
|
||||
def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
|
||||
scens = []
|
||||
for scenario_url in scenario_urls:
|
||||
# As empty lists will be removed in POST request well send ['']
|
||||
# As empty lists will be removed in POST request we'll send ['']
|
||||
if scenario_url == "":
|
||||
continue
|
||||
|
||||
@ -413,6 +415,7 @@ def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
|
||||
|
||||
|
||||
def set_aliases(current_user, attach_object, aliases: List[str]):
|
||||
# As empty lists will be removed in POST request we'll send ['']
|
||||
if aliases == [""]:
|
||||
aliases = []
|
||||
|
||||
@ -421,7 +424,7 @@ def set_aliases(current_user, attach_object, aliases: List[str]):
|
||||
|
||||
|
||||
def copy_object(current_user, target_package: "Package", source_object_url: str):
|
||||
# Ensures that source is readable
|
||||
# Ensures that source object is readable
|
||||
source_package = PackageManager.get_package_by_url(current_user, source_object_url)
|
||||
|
||||
if source_package == target_package:
|
||||
@ -429,7 +432,7 @@ def copy_object(current_user, target_package: "Package", source_object_url: str)
|
||||
|
||||
parser = EPDBURLParser(source_object_url)
|
||||
|
||||
# if the url won't contain a package or is a plain package
|
||||
# if the url don't contain a package or is a plain package
|
||||
if not parser.contains_package_url():
|
||||
raise ValueError(f"Object {source_object_url} can't be copied!")
|
||||
|
||||
@ -714,12 +717,36 @@ def models(request):
|
||||
|
||||
# Keep model_types for potential modal/action use
|
||||
context["model_types"] = {
|
||||
"ML Relative Reasoning": "ml-relative-reasoning",
|
||||
"Rule Based Relative Reasoning": "rule-based-relative-reasoning",
|
||||
"EnviFormer": "enviformer",
|
||||
"ML Relative Reasoning": {
|
||||
"type": "ml-relative-reasoning",
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"Rule Based Relative Reasoning": {
|
||||
"type": "rule-based-relative-reasoning",
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"EnviFormer": {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
context["model_types"][v.display()] = k
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
context["model_types"][v().display()] = {
|
||||
"type": k,
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
}
|
||||
for k, v in s.PROPERTY_PLUGINS.items():
|
||||
context["model_types"][v().display()] = {
|
||||
"type": k,
|
||||
"requires_rule_packages": v().requires_rule_packages,
|
||||
"requires_data_packages": v().requires_data_packages,
|
||||
}
|
||||
|
||||
# Context for paginated template
|
||||
context["entity_type"] = "model"
|
||||
@ -830,16 +857,36 @@ def package_models(request, package_uuid):
|
||||
)
|
||||
|
||||
context["model_types"] = {
|
||||
"ML Relative Reasoning": "mlrr",
|
||||
"Rule Based Relative Reasoning": "rbrr",
|
||||
"ML Relative Reasoning": {
|
||||
"type": "ml-relative-reasoning",
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"Rule Based Relative Reasoning": {
|
||||
"type": "rule-based-relative-reasoning",
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
"EnviFormer": {
|
||||
"type": "enviformer",
|
||||
"requires_rule_packages": False,
|
||||
"requires_data_packages": True,
|
||||
},
|
||||
}
|
||||
|
||||
if s.FLAGS.get("ENVIFORMER", False):
|
||||
context["model_types"]["EnviFormer"] = "enviformer"
|
||||
|
||||
if s.FLAGS.get("PLUGINS", False):
|
||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||
context["model_types"][v.display()] = k
|
||||
context["model_types"][v().display()] = {
|
||||
"type": k,
|
||||
"requires_rule_packages": True,
|
||||
"requires_data_packages": True,
|
||||
}
|
||||
for k, v in s.PROPERTY_PLUGINS.items():
|
||||
context["model_types"][v().display()] = {
|
||||
"type": k,
|
||||
"requires_rule_packages": v().requires_rule_packages,
|
||||
"requires_data_packages": v().requires_data_packages,
|
||||
}
|
||||
|
||||
return render(request, "collections/models_paginated.html", context)
|
||||
|
||||
@ -900,8 +947,24 @@ def package_models(request, package_uuid):
|
||||
]
|
||||
|
||||
mod = RuleBasedRelativeReasoning.create(**params)
|
||||
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS.values():
|
||||
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS:
|
||||
pass
|
||||
elif s.FLAGS.get("PLUGINS", False) and model_type in s.PROPERTY_PLUGINS:
|
||||
params["plugin_identifier"] = model_type
|
||||
impl = s.PROPERTY_PLUGINS[model_type]
|
||||
inst = impl()
|
||||
|
||||
if inst.requires_rule_packages():
|
||||
params["rule_packages"] = [
|
||||
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
||||
]
|
||||
else:
|
||||
params["rule_packages"] = []
|
||||
|
||||
if not inst.requires_data_packages():
|
||||
del params["data_packages"]
|
||||
|
||||
mod = PropertyPluginModel.create(**params)
|
||||
else:
|
||||
return error(
|
||||
request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
|
||||
@ -925,14 +988,18 @@ def package_model(request, package_uuid, model_uuid):
|
||||
if request.method == "GET":
|
||||
classify = request.GET.get("classify", False)
|
||||
ad_assessment = request.GET.get("app-domain-assessment", False)
|
||||
# TODO this needs to be generic
|
||||
half_life = request.GET.get("half_life", False)
|
||||
|
||||
if classify or ad_assessment:
|
||||
if any([classify, ad_assessment, half_life]):
|
||||
smiles = request.GET.get("smiles", "").strip()
|
||||
|
||||
# Check if smiles is non empty and valid
|
||||
if smiles == "":
|
||||
return JsonResponse({"error": "Received empty SMILES"}, status=400)
|
||||
|
||||
stereo = FormatConverter.has_stereo(smiles)
|
||||
|
||||
try:
|
||||
stand_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||
except ValueError:
|
||||
@ -966,6 +1033,19 @@ def package_model(request, package_uuid, model_uuid):
|
||||
|
||||
return JsonResponse(res, safe=False)
|
||||
|
||||
elif half_life:
|
||||
from epdb.tasks import dispatch_eager, predict_simple
|
||||
|
||||
_, run_res = dispatch_eager(
|
||||
current_user, predict_simple, current_model.pk, stand_smiles, include_svg=True
|
||||
)
|
||||
|
||||
# Here we expect a single result
|
||||
if isinstance(run_res.result, Iterable):
|
||||
return JsonResponse(run_res.result[0].model_dump(mode="json"), safe=False)
|
||||
|
||||
return JsonResponse(run_res.result.model_dump(mode="json"), safe=False)
|
||||
|
||||
else:
|
||||
app_domain_assessment = current_model.app_domain.assess(stand_smiles)
|
||||
return JsonResponse(app_domain_assessment, safe=False)
|
||||
@ -980,7 +1060,11 @@ def package_model(request, package_uuid, model_uuid):
|
||||
context["model"] = current_model
|
||||
context["current_object"] = current_model
|
||||
|
||||
return render(request, "objects/model.html", context)
|
||||
if isinstance(current_model, PropertyPluginModel):
|
||||
context["plugin_identifier"] = current_model.plugin_identifier
|
||||
return render(request, "objects/model/property_model.html", context)
|
||||
else:
|
||||
return render(request, "objects/model/classification_model.html", context)
|
||||
|
||||
elif request.method == "POST":
|
||||
if hidden := request.POST.get("hidden", None):
|
||||
@ -1940,6 +2024,7 @@ def package_pathways(request, package_uuid):
|
||||
prediction_setting = SettingManager.get_setting_by_url(current_user, prediction_setting)
|
||||
else:
|
||||
prediction_setting = current_user.prediction_settings()
|
||||
|
||||
pw = Pathway.create(
|
||||
current_package,
|
||||
stand_smiles,
|
||||
@ -2504,8 +2589,10 @@ def package_scenario(request, package_uuid, scenario_uuid):
|
||||
context["breadcrumbs"] = breadcrumbs(current_package, "scenario", current_scenario)
|
||||
|
||||
context["scenario"] = current_scenario
|
||||
# Get scenarios that have current_scenario as a parent
|
||||
context["children"] = current_scenario.scenario_set.order_by("name")
|
||||
|
||||
context["associated_additional_information"] = AdditionalInformation.objects.filter(
|
||||
scenario=current_scenario
|
||||
)
|
||||
|
||||
# Note: Modals now fetch schemas and data from API endpoints
|
||||
# Keeping these for backwards compatibility if needed elsewhere
|
||||
@ -2612,11 +2699,22 @@ def user(request, user_uuid):
|
||||
|
||||
context["user"] = requested_user
|
||||
|
||||
model_qs = EPModel.objects.none()
|
||||
for p in PackageManager.get_all_readable_packages(requested_user, include_reviewed=True):
|
||||
model_qs |= p.models
|
||||
accessible_packages = PackageManager.get_all_readable_packages(
|
||||
requested_user, include_reviewed=True
|
||||
)
|
||||
|
||||
context["models"] = model_qs
|
||||
property_models = PropertyPluginModel.objects.filter(
|
||||
package__in=accessible_packages
|
||||
).order_by("name")
|
||||
|
||||
tp_prediction_models = (
|
||||
EPModel.objects.filter(package__in=accessible_packages)
|
||||
.exclude(id__in=[pm.id for pm in property_models])
|
||||
.order_by("name")
|
||||
)
|
||||
|
||||
context["models"] = tp_prediction_models
|
||||
context["property_models"] = property_models
|
||||
|
||||
context["tokens"] = APIToken.objects.filter(user=requested_user)
|
||||
|
||||
@ -2853,6 +2951,18 @@ def settings(request):
|
||||
else:
|
||||
raise BadRequest("Neither Model-Based nor Rule-Based as Method selected!")
|
||||
|
||||
property_model_urls = request.POST.getlist("prediction-setting-property-models")
|
||||
|
||||
if property_model_urls:
|
||||
mods = []
|
||||
for pm_url in property_model_urls:
|
||||
model = PropertyPluginModel.objects.get(url=pm_url)
|
||||
|
||||
if PackageManager.readable(current_user, model.package):
|
||||
mods.append(model)
|
||||
|
||||
params["property_models"] = mods
|
||||
|
||||
created_setting = SettingManager.create_setting(
|
||||
current_user,
|
||||
name=name,
|
||||
|
||||
Reference in New Issue
Block a user