1 Commits

Author SHA1 Message Date
f9cc71d375 adjusted migration
Some checks failed
API CI / api-tests (pull_request) Failing after 17s
CI / test (pull_request) Failing after 21s
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

auth log, bb4g fix

missing import

Added viz hint if PES is part of reaction

Add Edge check for pes

flip boolean

...

pes

Added extra

...

In / Out Edges Viz, Submitting Button Text

...
2026-07-02 10:20:04 +02:00
17 changed files with 503 additions and 491 deletions

View File

@ -1,2 +1,2 @@
class InvalidSMILESException(Exception): class InvalidSMILESException(Exception):
pass pass

View File

@ -1687,13 +1687,13 @@ class PathwayNode(Schema):
dt50s: List[Dict[str, str]] = Field([], alias="dt50s") dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate") engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
id: str = Field(None, alias="url") id: str = Field(None, alias="url")
idcomp: str = Field(None, alias="node_label_id") idcomp: str = Field(None, alias="default_node_label.url")
idreact: str = Field(None, alias="node_label_id") idreact: str = Field(None, alias="default_node_label.url")
image: str = Field(None, alias="image") image: str = Field(None, alias="image")
imageSize: int = Field(None, alias="image_size") imageSize: int = Field(None, alias="image_size")
name: str = Field(None, alias="name") name: str = Field(None, alias="name")
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate") proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
smiles: str = Field(None, alias="smiles") smiles: str = Field(None, alias="default_node_label.smiles")
pseudo: bool = Field(False, alias="pseudo") pseudo: bool = Field(False, alias="pseudo")
@staticmethod @staticmethod

View File

@ -99,15 +99,11 @@ class Command(BaseCommand):
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png" new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
new_license.save() new_license.save()
def import_package(self, data, owner, all_envipath_user_group): def import_package(self, data, owner):
p = PackageManager.import_legacy_package( return PackageManager.import_legacy_package(
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
) )
PackageManager.grant_read(owner, p, all_envipath_user_group)
return p
def create_default_setting(self, owner, packages): def create_default_setting(self, owner, packages):
s = SettingManager.create_setting( s = SettingManager.create_setting(
owner, owner,
@ -202,7 +198,7 @@ class Command(BaseCommand):
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8" s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
).read() ).read()
) )
imported_package = self.import_package(package_data, admin, g) imported_package = self.import_package(package_data, admin)
mapping[p.replace(".json", "")] = imported_package mapping[p.replace(".json", "")] = imported_package
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]]) setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])

View File

@ -1,150 +0,0 @@
# Generated by Django 6.0.3 on 2026-07-01 20:59
import django.contrib.postgres.fields
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0026_auto_20260602_1718"),
]
operations = [
migrations.AlterField(
model_name="compound",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="compound",
name="default_structure",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name="compound_default_structure",
to="epdb.compoundstructure",
verbose_name="Default Structure",
),
),
migrations.AlterField(
model_name="compound",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="compoundstructure",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="compoundstructure",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="edge",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="edge",
name="edge_label",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.CASCADE,
to="epdb.reaction",
verbose_name="Edge label",
),
),
migrations.AlterField(
model_name="edge",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="node",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="node",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="pathway",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="pathway",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="reaction",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="reaction",
name="medline_references",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(),
blank=True,
null=True,
verbose_name="Medline References",
),
),
migrations.AlterField(
model_name="reaction",
name="rules",
field=models.ManyToManyField(
blank=True, related_name="reaction_rule", to="epdb.rule", verbose_name="Rule"
),
),
migrations.AlterField(
model_name="reaction",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
migrations.AlterField(
model_name="rule",
name="aliases",
field=django.contrib.postgres.fields.ArrayField(
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
),
),
migrations.AlterField(
model_name="rule",
name="scenarios",
field=models.ManyToManyField(
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
),
),
]

View File

@ -637,7 +637,7 @@ class EnviPathModel(TimeStampedModel):
class AliasMixin(models.Model): class AliasMixin(models.Model):
aliases = ArrayField( aliases = ArrayField(
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list, blank=True models.TextField(blank=False, null=False), verbose_name="Aliases", default=list
) )
@transaction.atomic @transaction.atomic
@ -660,9 +660,7 @@ class AliasMixin(models.Model):
class ScenarioMixin(models.Model): class ScenarioMixin(models.Model):
scenarios = models.ManyToManyField( scenarios = models.ManyToManyField("epdb.Scenario", verbose_name="Attached Scenarios")
"epdb.Scenario", verbose_name="Attached Scenarios", blank=True
)
@transaction.atomic @transaction.atomic
def set_scenarios(self, scenarios: List["Scenario"]): def set_scenarios(self, scenarios: List["Scenario"]):
@ -862,7 +860,7 @@ class Compound(
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def create( def create(
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs package: "Package", smiles: str, name: str = None, description: str = None, molfile: str = None, *args, **kwargs
) -> "Compound": ) -> "Compound":
if smiles is None or smiles.strip() == "": if smiles is None or smiles.strip() == "":
raise InvalidSMILESException("SMILES is required") raise InvalidSMILESException("SMILES is required")
@ -1660,15 +1658,10 @@ class Reaction(
products = models.ManyToManyField( products = models.ManyToManyField(
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products" "epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
) )
rules = models.ManyToManyField( rules = models.ManyToManyField("epdb.Rule", verbose_name="Rule", related_name="reaction_rule")
"epdb.Rule", verbose_name="Rule", related_name="reaction_rule", blank=True
)
multi_step = models.BooleanField(verbose_name="Multistep Reaction") multi_step = models.BooleanField(verbose_name="Multistep Reaction")
medline_references = ArrayField( medline_references = ArrayField(
models.TextField(blank=False, null=False), models.TextField(blank=False, null=False), null=True, verbose_name="Medline References"
null=True,
verbose_name="Medline References",
blank=True,
) )
external_identifiers = GenericRelation("ExternalIdentifier") external_identifiers = GenericRelation("ExternalIdentifier")
@ -2262,12 +2255,10 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
depth_map[0] = list() depth_map[0] = list()
processed = set() processed = set()
data_driven_root_nodes = ( data_driven_root_nodes = self.node_set.all().annotate(
self.node_set.all() prod_cnt=Count('edge_products'),
.annotate(prod_cnt=Count("edge_products"), educt_cnt=Count("edge_educts")) educt_cnt=Count('edge_educts')
.filter(prod_cnt=0, educt_cnt__gt=0) ).filter(prod_cnt=0, educt_cnt__gt=0).distinct()
.distinct()
)
# Eval QuerySet # Eval QuerySet
root_nodes_by_depth = list(self.root_nodes) root_nodes_by_depth = list(self.root_nodes)
@ -2360,12 +2351,8 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
"node_label_id": self.default_node_label.url, "node_label_id": self.default_node_label.url,
"image": f"{self.url}?image=svg", "image": f"{self.url}?image=svg",
"image_svg": IndigoUtils.mol_to_svg( "image_svg": IndigoUtils.mol_to_svg(
self.default_node_label.molfile self.default_node_label.molfile if self.default_node_label.molfile is not None and self.default_node_label.molfile.strip() else self.default_node_label.smiles,
if self.default_node_label.molfile is not None width=40, height=40
and self.default_node_label.molfile.strip()
else self.default_node_label.smiles,
width=40,
height=40,
), ),
"image_type": "svg", "image_type": "svg",
"name": self.get_name(), "name": self.get_name(),
@ -2419,10 +2406,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
@property @property
def as_svg(self): def as_svg(self):
if ( if self.default_node_label.molfile is not None and self.default_node_label.molfile.strip() != "":
self.default_node_label.molfile is not None
and self.default_node_label.molfile.strip() != ""
):
return IndigoUtils.mol_to_svg(self.default_node_label.molfile) return IndigoUtils.mol_to_svg(self.default_node_label.molfile)
return IndigoUtils.mol_to_svg(self.default_node_label.smiles) return IndigoUtils.mol_to_svg(self.default_node_label.smiles)

View File

@ -785,7 +785,6 @@ def models(request):
{"Home": s.SERVER_URL}, {"Home": s.SERVER_URL},
{"Model": s.SERVER_URL + "/model"}, {"Model": s.SERVER_URL + "/model"},
] ]
context["entity_type"] = "model" context["entity_type"] = "model"
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/" context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
@ -810,7 +809,7 @@ def models(request):
"type": "enviformer", "type": "enviformer",
"requires_rule_packages": False, "requires_rule_packages": False,
"requires_data_packages": True, "requires_data_packages": True,
} },
if s.FLAGS.get("PLUGINS", False): if s.FLAGS.get("PLUGINS", False):
for k, v in s.CLASSIFIER_PLUGINS.items(): for k, v in s.CLASSIFIER_PLUGINS.items():
@ -943,12 +942,13 @@ def package_models(request, package_uuid):
"requires_data_packages": True, "requires_data_packages": True,
}, },
} }
if s.ENVIFORMER_PRESENT: if s.ENVIFORMER_PRESENT:
context["model_types"]["EnviFormer"] = { context["model_types"]["EnviFormer"] = {
"type": "enviformer", "type": "enviformer",
"requires_rule_packages": False, "requires_rule_packages": False,
"requires_data_packages": True, "requires_data_packages": True,
} },
if s.FLAGS.get("PLUGINS", False): if s.FLAGS.get("PLUGINS", False):
for k, v in s.CLASSIFIER_PLUGINS.items(): for k, v in s.CLASSIFIER_PLUGINS.items():
@ -1124,10 +1124,8 @@ def package_model(request, package_uuid, model_uuid):
} }
) )
# Sort data by prob desc # Sort data by prob desc
res["pred"] = sorted( res["pred"] = sorted(res["pred"], key=lambda x: x["probability"], reverse=True)
res["pred"], key=lambda x: x["probability"], reverse=True
)
return JsonResponse(res, safe=False) return JsonResponse(res, safe=False)
@ -1232,7 +1230,9 @@ def package(request, package_uuid):
if request.method == "GET": if request.method == "GET":
if request.GET.get("export", False) == "true": if request.GET.get("export", False) == "true":
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json" filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
pack_json = PackageManager.export_package(current_package) pack_json = PackageManager.export_package(
current_package, include_models=False, include_external_identifiers=False
)
response = JsonResponse(pack_json, content_type="application/json") response = JsonResponse(pack_json, content_type="application/json")
response["Content-Disposition"] = f'attachment; filename="{filename}"' response["Content-Disposition"] = f'attachment; filename="{filename}"'
@ -2368,13 +2368,10 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
node_description = request.POST.get("node-description") node_description = request.POST.get("node-description")
node_smiles = request.POST.get("node-smiles").strip() node_smiles = request.POST.get("node-smiles").strip()
try: try:
current_pathway.add_node(node_smiles, name=node_name, description=node_description) current_pathway.add_node(node_smiles, name=node_name, description=node_description)
except InvalidSMILESException: except InvalidSMILESException as e:
return error( return error(request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid")
request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
)
return redirect(current_pathway.url) return redirect(current_pathway.url)

View File

@ -81,7 +81,6 @@ def migration(request):
"bt0416-4269", "bt0416-4269",
"bt0432-4254", "bt0432-4254",
"bt0337-4117", "bt0337-4117",
"bt0322-3393",
] ]
if request.method == "GET": if request.method == "GET":

View File

@ -1,14 +1,18 @@
console.log("loaded pw.js") console.log("loaded pw.js")
function findPaths(source_idx, target_idx, links) { function findPaths(source_idx, target_idx, links) {
const resultLinks = new Set(); const resultLinks = new Set();
const visited = new Set(); const visited = new Set();
// Helper function for depth-first search // Helper function for depth-first search
function dfs(current) { function dfs(current) {
if (current === target_idx) { if (current === target_idx) {
return; return;
} }
visited.add(current); visited.add(current);
links.forEach(link => { links.forEach(link => {
if ( if (
link.target.id === current && link.target.id === current &&
@ -19,13 +23,18 @@ function findPaths(source_idx, target_idx, links) {
dfs(link.source.id); dfs(link.source.id);
} }
}); });
visited.delete(current); visited.delete(current);
} }
// Start DFS // Start DFS
dfs(source_idx); dfs(source_idx);
return Array.from(resultLinks); // Convert Set to Array for result return Array.from(resultLinks); // Convert Set to Array for result
} }
function predictFromNode(url) { function predictFromNode(url) {
fetch("", { fetch("", {
method: "POST", method: "POST",
@ -653,34 +662,36 @@ function draw(pathway, elem) {
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)'; return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
}) })
.attr("fill", "none") .attr("fill", "none")
.on("click", function(event, d) { .on("click", function(event, d) {
const wasHighlighted = d3.select(this).classed("highlighted"); const wasHighlighted = d3.select(this).classed("highlighted");
d3.selectAll("path").classed("highlighted", false);
d3.selectAll("path").classed("highlighted", false);
if (!wasHighlighted) { if (!wasHighlighted) {
const toHighlight = []; const toHighlight = [];
toHighlight.push(d.el); toHighlight.push(d.el);
if (d.source.pseudo || d.target.pseudo) { if (d.source.pseudo || d.target.pseudo) {
if (d.target.pseudo) { if (d.target.pseudo) {
d3.selectAll("path").each(e => { d3.selectAll("path").each(e => {
if (e !== undefined && e.source.id === d.target.id) { if (e !== undefined && e.source.id === d.target.id) {
toHighlight.push(e.el); 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);
}
});
} }
});
} 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) { for (const e of toHighlight) {
d3.select(e).classed("highlighted", true); d3.select(e).classed("highlighted", true);
} }
} }
}); })
// add element to links array // add element to links array
link.each(function (d) { link.each(function (d) {
@ -697,41 +708,44 @@ function draw(pathway, elem) {
.call(d3.drag() .call(d3.drag()
.on("start", dragstarted) .on("start", dragstarted)
.on("drag", dragged) .on("drag", dragged)
.on("end", dragended) .on("end", dragended))
)
.on("click", function (event, d) { .on("click", function (event, d) {
const wasHighlighted = d3.select(this).select("circle").classed("highlighted"); const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
d3.selectAll('circle.highlighted').classed('highlighted', false);
d3.selectAll("path").classed("inedge", false);
d3.selectAll("path").classed("outedge", false);
if (!wasHighlighted) { d3.selectAll('circle.highlighted').classed('highlighted', false);
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted")); d3.selectAll("path").classed("inedge", false);
const inEdges = findPaths(d.id, 3, links); d3.selectAll("path").classed("outedge", false);
const outEdges = []
// Colorize out edges green if (!wasHighlighted) {
for (const l of links) { d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
if (l.source.id === d.id) {
outEdges.push(l); const inEdges = findPaths(d.id, 3, links);
if (l.target.pseudo) { const outEdges = []
for (const l2 of links) {
if (l.target.id === l2.source.id) { // Colorize out edges green
outEdges.push(l2); for (const l of links) {
if (l.source.id === d.id) {
outEdges.push(l);
if (l.target.pseudo) {
for (const l2 of links) {
if (l.target.id === l2.source.id) {
outEdges.push(l2);
}
}
}
} }
}
} }
}
for (const e of inEdges) {
d3.select(e.el).classed("inedge", true);
}
for (const e of outEdges) {
d3.select(e.el).classed("outedge", true);
}
} }
for (const e of inEdges) {
d3.select(e.el).classed("inedge", true);
}
for (const e of outEdges) { })
d3.select(e.el).classed("outedge", true);
}
}
});
// Kreise für die Knoten hinzufügen // Kreise für die Knoten hinzufügen
node.append("circle") node.append("circle")

View File

@ -6,6 +6,7 @@
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })" x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
@modal-opened.window=" @modal-opened.window="
const links = d3.selectAll('path.highlighted'); const links = d3.selectAll('path.highlighted');
if (!links.empty()) { if (!links.empty()) {
const el = links.node(); const el = links.node();
const selectElement = document.getElementById('delete_pathway_edge_edges'); const selectElement = document.getElementById('delete_pathway_edge_edges');
@ -17,7 +18,9 @@
break; break;
} }
} }
selectElement.dispatchEvent(new Event('change')); selectElement.dispatchEvent(new Event('change'));
} }
" "
@close="reset()" @close="reset()"

View File

@ -6,14 +6,17 @@
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })" x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
@modal-opened.window=" @modal-opened.window="
const el = d3.select('circle.highlighted').node(); const el = d3.select('circle.highlighted').node();
if (el !== null) { if (el !== null) {
const selectElement = document.getElementById('delete_pathway_node_nodes'); const selectElement = document.getElementById('delete_pathway_node_nodes');
for (let option of selectElement.options) { for (let option of selectElement.options) {
if (option.value === el.__data__.url) { if (option.value === el.__data__.url) {
option.selected = true; option.selected = true;
break; break;
} }
} }
selectElement.dispatchEvent(new Event('change')); selectElement.dispatchEvent(new Event('change'));
} }
" "

View File

@ -56,10 +56,8 @@
<ul class="menu bg-base-200 rounded-box"> <ul class="menu bg-base-200 rounded-box">
{% for um in group.user_member.all %} {% for um in group.user_member.all %}
<li> <li>
<a <a href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}" class="hover:bg-base-300"
href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}" >{{ um.username }}
class="hover:bg-base-300"
>{{ um.username }}
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a {% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
> >
</li> </li>

View File

@ -77,10 +77,12 @@
stroke: red; stroke: red;
stroke-width: 3px; stroke-width: 3px;
} }
.outedge { .outedge {
stroke: green; stroke: green;
stroke-width: 3px; stroke-width: 3px;
} }
</style> </style>
<script src="{% static 'js/pw.js' %}"></script> <script src="{% static 'js/pw.js' %}"></script>
@ -120,7 +122,7 @@
<div class="collapse-title text-xl font-medium"> <div class="collapse-title text-xl font-medium">
Graphical Representation Graphical Representation
</div> </div>
<div class="collapse-content"> <div class="collapse-content ">
<div class="bg-base-100 mb-2 rounded-lg p-2"> <div class="bg-base-100 mb-2 rounded-lg p-2">
<div class="navbar bg-base-100 rounded-lg"> <div class="navbar bg-base-100 rounded-lg">
<div class="flex-1"> <div class="flex-1">

View File

@ -212,8 +212,6 @@
e.preventDefault(); e.preventDefault();
const button = this; const button = this;
button.disabled = true; button.disabled = true;
// Set text depending on mode
if (document.getElementById("predict-submit-button").innerText === "Build") { if (document.getElementById("predict-submit-button").innerText === "Build") {
button.textContent = "Building..."; button.textContent = "Building...";
} else { } else {

View File

@ -1,7 +1,5 @@
from django.conf import settings as s from django.conf import settings as s
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from epdb.exceptions import InvalidSMILESException
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Compound, User, CompoundStructure from epdb.models import Compound, User, CompoundStructure
@ -35,13 +33,13 @@ class CompoundTest(TestCase):
self.assertEqual(c.description, "No Desc") self.assertEqual(c.description, "No Desc")
def test_missing_smiles(self): def test_missing_smiles(self):
with self.assertRaises(InvalidSMILESException): with self.assertRaises(ValueError):
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc") _ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
with self.assertRaises(InvalidSMILESException): with self.assertRaises(ValueError):
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc") _ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
with self.assertRaises(InvalidSMILESException): with self.assertRaises(ValueError):
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc") _ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
def test_smiles_are_trimmed(self): def test_smiles_are_trimmed(self):
@ -98,7 +96,7 @@ class CompoundTest(TestCase):
self.assertEqual(len(self.package.compounds), 1) self.assertEqual(len(self.package.compounds), 1)
def test_wrong_smiles(self): def test_wrong_smiles(self):
with self.assertRaises(InvalidSMILESException): with self.assertRaises(ValueError):
_ = Compound.create( _ = Compound.create(
self.package, self.package,
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F", smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",

View File

@ -1,7 +1,6 @@
from django.conf import settings as s from django.conf import settings as s
from django.test import TestCase, override_settings from django.test import TestCase, override_settings
from epdb.exceptions import InvalidSMILESException
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Compound, User, Reaction, Rule from epdb.models import Compound, User, Reaction, Rule
@ -164,7 +163,7 @@ class ReactionTest(TestCase):
self.assertEqual(len(self.package.reactions), 1) self.assertEqual(len(self.package.reactions), 1)
def test_wrong_smiles(self): def test_wrong_smiles(self):
with self.assertRaises(InvalidSMILESException): with self.assertRaises(ValueError):
_ = Reaction.create( _ = Reaction.create(
package=self.package, package=self.package,
name="Eawag BBD reaction r0001", name="Eawag BBD reaction r0001",

View File

@ -46,14 +46,6 @@ class ModelViewTest(TestCase):
) )
expected = [ expected = [
{
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
"probability": 0.75,
"btrule": {
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
"name": "bt0001-3568",
},
},
{ {
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]], "products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
"probability": 0.25, "probability": 0.25,
@ -70,6 +62,14 @@ class ModelViewTest(TestCase):
"name": "bt0243-4301", "name": "bt0243-4301",
}, },
}, },
{
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
"probability": 0.75,
"btrule": {
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
"name": "bt0001-3568",
},
},
] ]
actual = response.json()["pred"] actual = response.json()["pred"]

View File

@ -6,15 +6,12 @@ import logging
import uuid import uuid
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
from typing import Any, Dict, List, Optional, TYPE_CHECKING from typing import Any, Dict, List, TYPE_CHECKING
from django.conf import settings as s from django.conf import settings as s
from django.db import transaction from django.db import transaction
from ninja import Schema
from pydantic import HttpUrl
from epdb.models import ( from epdb.models import (
AdditionalInformation,
Compound, Compound,
CompoundStructure, CompoundStructure,
Edge, Edge,
@ -31,6 +28,7 @@ from epdb.models import (
Rule, Rule,
RuleBasedRelativeReasoning, RuleBasedRelativeReasoning,
Scenario, Scenario,
SequentialRule,
Setting, Setting,
SimpleAmbitRule, SimpleAmbitRule,
SimpleRDKitRule, SimpleRDKitRule,
@ -45,235 +43,408 @@ if TYPE_CHECKING:
from epdb.logic import SPathway from epdb.logic import SPathway
class LicenseExportSchema(Schema):
cc_string: str
link: HttpUrl
image_link: HttpUrl
##############
# RefSchemas #
##############
class RefExportSchema(Schema):
uuid: str
url: str
@staticmethod
def resolve_uuid(obj):
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
return str(value)
class RefCompoundExportSchema(RefExportSchema): ...
class RefCompoundStructureExportSchema(RefExportSchema): ...
class RefReactionExportSchema(RefExportSchema): ...
class RefRuleExportSchema(RefExportSchema): ...
class RefNodeExportSchema(RefExportSchema): ...
class RefEdgeExportSchema(RefExportSchema): ...
class RefPathwayExportSchema(RefExportSchema): ...
class RefScenarioExportSchema(RefExportSchema): ...
############
# Compound #
############
class CompoundExportSchema(RefCompoundExportSchema):
name: str
description: str
aliases: List[str]
default_structure: RefCompoundStructureExportSchema
structures: List["CompoundStructureExportSchema"]
scenarios: List[RefScenarioExportSchema]
class CompoundStructureExportSchema(RefCompoundStructureExportSchema):
name: str
description: str
aliases: List[str]
smiles: str
molfile: Optional[str]
normalized_structure: bool
scenarios: List[RefScenarioExportSchema]
############
# Reaction #
############
class ReactionExportSchema(RefReactionExportSchema):
name: str
description: str
aliases: List[str]
educts: List[RefCompoundStructureExportSchema]
products: List[RefCompoundStructureExportSchema]
rules: List[RefRuleExportSchema]
multi_step: bool
medline_references: List[str] | None
scenarios: List[RefScenarioExportSchema]
#########
# Rules #
#########
class RuleExportSchema(RefRuleExportSchema):
name: str
description: str
aliases: List[str]
smirks: str
reactant_filter_smarts: Optional[str]
product_filter_smarts: Optional[str]
scenarios: List[RefScenarioExportSchema]
class ParallelRuleExportSchema(RefRuleExportSchema):
name: str
description: str
aliases: List[str]
simple_rules: List[RefRuleExportSchema]
scenarios: List[RefScenarioExportSchema]
###########################
# Pathway / Nodes / Edges #
###########################
class NodeExportSchema(RefNodeExportSchema):
name: str
description: str
aliases: List[str]
default_node_label: RefCompoundStructureExportSchema
node_labels: List[RefCompoundStructureExportSchema]
depth: int
stereo_removed: bool
scenarios: List[RefScenarioExportSchema]
class EdgeExportSchema(RefEdgeExportSchema):
name: str
description: str
aliases: List[str]
edge_label: RefReactionExportSchema
start_nodes: List[RefNodeExportSchema]
end_nodes: List[RefNodeExportSchema]
scenarios: List[RefScenarioExportSchema]
class PathwayExportSchema(RefPathwayExportSchema):
name: str
description: str
aliases: List[str]
predicted: bool
nodes: List[NodeExportSchema]
edges: List[EdgeExportSchema]
scenarios: List[RefScenarioExportSchema]
class ScenarioExportSchema(RefScenarioExportSchema):
name: str
description: str
scenario_date: str
scenario_type: str
class AdditionalInformationExportSchema(RefExportSchema):
type: str
data: dict
scenario: RefScenarioExportSchema | None = None
attach_object: RefExportSchema | None = None
@staticmethod
def resolve_attach_object(obj):
if isinstance(obj, dict):
if obj.get("attach_object") is None:
return None
return RefExportSchema.model_validate(obj["attach_object"])
return obj.content_object
###########
# Package #
###########
class PackageExportSchema(Schema):
name: str
description: str
uuid: str
reviewed: bool
license: LicenseExportSchema | None
compounds: List[CompoundExportSchema]
reactions: List[ReactionExportSchema]
simple_rules: List[RuleExportSchema]
composite_rules: List[ParallelRuleExportSchema]
pathways: List[PathwayExportSchema]
scenarios: List[ScenarioExportSchema]
additional_information: List[AdditionalInformationExportSchema]
@staticmethod
def resolve_uuid(obj):
value = obj.get("uuid") if isinstance(obj, dict) else obj.uuid
return str(value)
@staticmethod
def resolve_simple_rules(obj):
if isinstance(obj, dict):
result = []
for r in obj.get("simple_rules", []):
result.append(RuleExportSchema.model_validate(r))
return result
return SimpleAmbitRule.objects.filter(package=obj)
@staticmethod
def resolve_composite_rules(obj):
if isinstance(obj, dict):
result = []
for r in obj.get("composite_rules", []):
result.append(ParallelRuleExportSchema.model_validate(r))
return result
return ParallelRule.objects.filter(package=obj)
@staticmethod
def resolve_additional_information(obj):
if isinstance(obj, dict):
result = []
for ai in obj.get("additional_information", []):
result.append(AdditionalInformationExportSchema.model_validate(ai))
return result
return AdditionalInformation.objects.filter(package=obj)
class PackageExporter: class PackageExporter:
def __init__(self, package: Package): def __init__(
self,
package: Package,
include_models: bool = False,
include_external_identifiers: bool = True,
):
self._raw_package = package self._raw_package = package
self.include_modes = include_models
self.include_external_identifiers = include_external_identifiers
def do_export(self): def do_export(self):
return PackageExporter._export_package_as_json(self._raw_package) return PackageExporter._export_package_as_json(
self._raw_package, self.include_modes, self.include_external_identifiers
)
@staticmethod @staticmethod
def _export_package_as_json(package: Package) -> Dict[str, Any]: def _export_package_as_json(
package: Package, include_models: bool = False, include_external_identifiers: bool = True
) -> Dict[str, Any]:
""" """
Dumps a Package and all its related objects as JSON. Dumps a Package and all its related objects as JSON.
Args: Args:
package: The Package instance to dump package: The Package instance to dump
include_models: Whether to include EPModel objects
include_external_identifiers: Whether to include external identifiers
Returns: Returns:
Dict containing the complete package data as JSON-serializable structure Dict containing the complete package data as JSON-serializable structure
""" """
data = PackageExportSchema.from_orm(package) def serialize_base_object(
obj, include_aliases: bool = True, include_scenarios: bool = True
) -> Dict[str, Any]:
"""Serialize common EnviPathModel fields"""
base_dict = {
"uuid": str(obj.uuid),
"name": obj.name,
"description": obj.description,
"url": obj.url,
"kv": obj.kv,
}
return data.model_dump(mode="json") # Add aliases if the object has them
if include_aliases and hasattr(obj, "aliases"):
base_dict["aliases"] = obj.aliases
# Add scenarios if the object has them
if include_scenarios and hasattr(obj, "scenarios"):
base_dict["scenarios"] = [
{"uuid": str(s.uuid), "url": s.url} for s in obj.scenarios.all()
]
return base_dict
def serialize_external_identifiers(obj) -> List[Dict[str, Any]]:
"""Serialize external identifiers for an object"""
if not include_external_identifiers or not hasattr(obj, "external_identifiers"):
return []
identifiers = []
for ext_id in obj.external_identifiers.all():
identifier_dict = {
"uuid": str(ext_id.uuid),
"database": {
"uuid": str(ext_id.database.uuid),
"name": ext_id.database.name,
"base_url": ext_id.database.base_url,
},
"identifier_value": ext_id.identifier_value,
"url": ext_id.url,
"is_primary": ext_id.is_primary,
}
identifiers.append(identifier_dict)
return identifiers
# Start with the package itself
result = serialize_base_object(package, include_aliases=True, include_scenarios=True)
result["reviewed"] = package.reviewed
# # Add license information
# if package.license:
# result['license'] = {
# 'uuid': str(package.license.uuid),
# 'name': package.license.name,
# 'link': package.license.link,
# 'image_link': package.license.image_link
# }
# else:
# result['license'] = None
# Initialize collections
result.update(
{
"compounds": [],
"structures": [],
"rules": {"simple_rules": [], "parallel_rules": [], "sequential_rules": []},
"reactions": [],
"pathways": [],
"nodes": [],
"edges": [],
"scenarios": [],
"models": [],
}
)
print(f"Exporting package: {package.name}")
# Export compounds
print("Exporting compounds...")
for compound in package.compounds.prefetch_related("default_structure").order_by("url"):
compound_dict = serialize_base_object(
compound, include_aliases=True, include_scenarios=True
)
if compound.default_structure:
compound_dict["default_structure"] = {
"uuid": str(compound.default_structure.uuid),
"url": compound.default_structure.url,
}
else:
compound_dict["default_structure"] = None
compound_dict["external_identifiers"] = serialize_external_identifiers(compound)
result["compounds"].append(compound_dict)
# Export compound structures
print("Exporting compound structures...")
compound_structures = (
CompoundStructure.objects.filter(compound__package=package)
.select_related("compound")
.order_by("url")
)
for structure in compound_structures:
structure_dict = serialize_base_object(
structure, include_aliases=True, include_scenarios=True
)
structure_dict.update(
{
"compound": {
"uuid": str(structure.compound.uuid),
"url": structure.compound.url,
},
"smiles": structure.smiles,
"canonical_smiles": structure.canonical_smiles,
"inchikey": structure.inchikey,
"normalized_structure": structure.normalized_structure,
"external_identifiers": serialize_external_identifiers(structure),
}
)
result["structures"].append(structure_dict)
# Export rules
print("Exporting rules...")
# Simple rules (including SimpleAmbitRule and SimpleRDKitRule)
for rule in SimpleRule.objects.filter(package=package).order_by("url"):
rule_dict = serialize_base_object(rule, include_aliases=True, include_scenarios=True)
# Add specific fields for SimpleAmbitRule
if isinstance(rule, SimpleAmbitRule):
rule_dict.update(
{
"rule_type": "SimpleAmbitRule",
"smirks": rule.smirks,
"reactant_filter_smarts": rule.reactant_filter_smarts or "",
"product_filter_smarts": rule.product_filter_smarts or "",
}
)
elif isinstance(rule, SimpleRDKitRule):
rule_dict.update(
{"rule_type": "SimpleRDKitRule", "reaction_smarts": rule.reaction_smarts}
)
else:
rule_dict["rule_type"] = "SimpleRule"
result["rules"]["simple_rules"].append(rule_dict)
# Parallel rules
for rule in (
ParallelRule.objects.filter(package=package)
.prefetch_related("simple_rules")
.order_by("url")
):
rule_dict = serialize_base_object(rule, include_aliases=True, include_scenarios=True)
rule_dict["rule_type"] = "ParallelRule"
rule_dict["simple_rules"] = [
{"uuid": str(sr.uuid), "url": sr.url} for sr in rule.simple_rules.all()
]
result["rules"]["parallel_rules"].append(rule_dict)
# Sequential rules
for rule in (
SequentialRule.objects.filter(package=package)
.prefetch_related("simple_rules")
.order_by("url")
):
rule_dict = serialize_base_object(rule, include_aliases=True, include_scenarios=True)
rule_dict["rule_type"] = "SequentialRule"
rule_dict["simple_rules"] = [
{
"uuid": str(sr.uuid),
"url": sr.url,
"order_index": sr.sequentialruleordering_set.get(
sequential_rule=rule
).order_index,
}
for sr in rule.simple_rules.all()
]
result["rules"]["sequential_rules"].append(rule_dict)
# Export reactions
print("Exporting reactions...")
for reaction in package.reactions.prefetch_related("educts", "products", "rules").order_by(
"url"
):
reaction_dict = serialize_base_object(
reaction, include_aliases=True, include_scenarios=True
)
reaction_dict.update(
{
"educts": [{"uuid": str(e.uuid), "url": e.url} for e in reaction.educts.all()],
"products": [
{"uuid": str(p.uuid), "url": p.url} for p in reaction.products.all()
],
"rules": [{"uuid": str(r.uuid), "url": r.url} for r in reaction.rules.all()],
"multi_step": reaction.multi_step,
"medline_references": reaction.medline_references,
"external_identifiers": serialize_external_identifiers(reaction),
}
)
result["reactions"].append(reaction_dict)
# Export pathways
print("Exporting pathways...")
for pathway in package.pathways.order_by("url"):
pathway_dict = serialize_base_object(
pathway, include_aliases=True, include_scenarios=True
)
# Add setting reference if exists
if hasattr(pathway, "setting") and pathway.setting:
pathway_dict["setting"] = {
"uuid": str(pathway.setting.uuid),
"url": pathway.setting.url,
}
else:
pathway_dict["setting"] = None
result["pathways"].append(pathway_dict)
# Export nodes
print("Exporting nodes...")
pathway_nodes = (
Node.objects.filter(pathway__package=package)
.select_related("pathway", "default_node_label")
.prefetch_related("node_labels", "out_edges")
.order_by("url")
)
for node in pathway_nodes:
node_dict = serialize_base_object(node, include_aliases=True, include_scenarios=True)
node_dict.update(
{
"pathway": {"uuid": str(node.pathway.uuid), "url": node.pathway.url},
"default_node_label": {
"uuid": str(node.default_node_label.uuid),
"url": node.default_node_label.url,
},
"node_labels": [
{"uuid": str(label.uuid), "url": label.url}
for label in node.node_labels.all()
],
"out_edges": [
{"uuid": str(edge.uuid), "url": edge.url} for edge in node.out_edges.all()
],
"depth": node.depth,
}
)
result["nodes"].append(node_dict)
# Export edges
print("Exporting edges...")
pathway_edges = (
Edge.objects.filter(pathway__package=package)
.select_related("pathway", "edge_label")
.prefetch_related("start_nodes", "end_nodes")
.order_by("url")
)
for edge in pathway_edges:
edge_dict = serialize_base_object(edge, include_aliases=True, include_scenarios=True)
edge_dict.update(
{
"pathway": {"uuid": str(edge.pathway.uuid), "url": edge.pathway.url},
"edge_label": {"uuid": str(edge.edge_label.uuid), "url": edge.edge_label.url},
"start_nodes": [
{"uuid": str(node.uuid), "url": node.url} for node in edge.start_nodes.all()
],
"end_nodes": [
{"uuid": str(node.uuid), "url": node.url} for node in edge.end_nodes.all()
],
}
)
result["edges"].append(edge_dict)
# Export scenarios
print("Exporting scenarios...")
for scenario in package.scenarios.order_by("url"):
scenario_dict = serialize_base_object(
scenario, include_aliases=False, include_scenarios=False
)
scenario_dict.update(
{
"scenario_date": scenario.scenario_date,
"scenario_type": scenario.scenario_type,
"parent": {"uuid": str(scenario.parent.uuid), "url": scenario.parent.url}
if scenario.parent
else None,
"additional_information": scenario.additional_information,
}
)
result["scenarios"].append(scenario_dict)
# Export models
if include_models:
print("Exporting models...")
package_models = (
package.models.select_related("app_domain")
.prefetch_related("rule_packages", "data_packages", "eval_packages")
.order_by("url")
)
for model in package_models:
model_dict = serialize_base_object(
model, include_aliases=True, include_scenarios=False
)
# Common fields for PackageBasedModel
if hasattr(model, "rule_packages"):
model_dict.update(
{
"rule_packages": [
{"uuid": str(p.uuid), "url": p.url}
for p in model.rule_packages.all()
],
"data_packages": [
{"uuid": str(p.uuid), "url": p.url}
for p in model.data_packages.all()
],
"eval_packages": [
{"uuid": str(p.uuid), "url": p.url}
for p in model.eval_packages.all()
],
"threshold": model.threshold,
"eval_results": model.eval_results,
"model_status": model.model_status,
}
)
if model.app_domain:
model_dict["app_domain"] = {
"uuid": str(model.app_domain.uuid),
"url": model.app_domain.url,
}
else:
model_dict["app_domain"] = None
# Specific fields for different model types
if isinstance(model, RuleBasedRelativeReasoning):
model_dict.update(
{
"model_type": "RuleBasedRelativeReasoning",
"min_count": model.min_count,
"max_count": model.max_count,
}
)
elif isinstance(model, MLRelativeReasoning):
model_dict["model_type"] = "MLRelativeReasoning"
elif isinstance(model, EnviFormer):
model_dict["model_type"] = "EnviFormer"
else:
model_dict["model_type"] = "EPModel"
result["models"].append(model_dict)
print(f"Export completed for package: {package.name}")
print(f"- Compounds: {len(result['compounds'])}")
print(f"- Structures: {len(result['structures'])}")
print(f"- Simple rules: {len(result['rules']['simple_rules'])}")
print(f"- Parallel rules: {len(result['rules']['parallel_rules'])}")
print(f"- Sequential rules: {len(result['rules']['sequential_rules'])}")
print(f"- Reactions: {len(result['reactions'])}")
print(f"- Pathways: {len(result['pathways'])}")
print(f"- Nodes: {len(result['nodes'])}")
print(f"- Edges: {len(result['edges'])}")
print(f"- Scenarios: {len(result['scenarios'])}")
print(f"- Models: {len(result['models'])}")
return result
class PackageImporter: class PackageImporter: