forked from enviPath/enviPy
[Feature] Integrate DOI Links, Handle Cycles in Pathway Viz (#407)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#407
This commit is contained in:
48
epdb/migrations/0026_auto_20260602_1718.py
Normal file
48
epdb/migrations/0026_auto_20260602_1718.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-06-02 17:18
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
from envipy_additional_information import DOI
|
||||||
|
|
||||||
|
|
||||||
|
def forward_func(apps, schema_editor):
|
||||||
|
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||||
|
|
||||||
|
refs = AdditionalInformation.objects.filter(type="Reference")
|
||||||
|
|
||||||
|
remaining = []
|
||||||
|
|
||||||
|
for ref in refs:
|
||||||
|
r = ref.data["reference"]
|
||||||
|
try:
|
||||||
|
# PubMed IDs are plain ints, try parsing
|
||||||
|
_ = int(r)
|
||||||
|
# Nothing to do
|
||||||
|
except ValueError:
|
||||||
|
DOMAINS = [
|
||||||
|
"http://dx.doi.org/",
|
||||||
|
"https://dx.doi.org/",
|
||||||
|
"http://doi.org/",
|
||||||
|
"https://doi.org/",
|
||||||
|
]
|
||||||
|
for d in DOMAINS:
|
||||||
|
r = r.replace(d, "")
|
||||||
|
|
||||||
|
if r.startswith("10."):
|
||||||
|
ref.type = DOI.__name__
|
||||||
|
ref.data = {"doi": r}
|
||||||
|
ref.save()
|
||||||
|
else:
|
||||||
|
remaining.append(ref)
|
||||||
|
|
||||||
|
if len(remaining) > 0:
|
||||||
|
raise ValueError(f"Could not parse {len(remaining)} references")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0025_auto_20260511_2025"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||||
|
]
|
||||||
@ -1773,7 +1773,7 @@ class Reaction(
|
|||||||
return new_reaction
|
return new_reaction
|
||||||
|
|
||||||
def smirks(self):
|
def smirks(self):
|
||||||
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
|
return f"{'.'.join([cs.smiles for cs in self.educts.all().order_by('-pk')])}>>{'.'.join([cs.smiles for cs in self.products.all().order_by('-pk')])}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self):
|
def as_svg(self):
|
||||||
@ -1886,6 +1886,9 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
if n not in queue:
|
if n not in queue:
|
||||||
queue.append(n)
|
queue.append(n)
|
||||||
|
|
||||||
|
for i in queue:
|
||||||
|
processed.add(i)
|
||||||
|
|
||||||
while len(queue):
|
while len(queue):
|
||||||
current = queue.pop()
|
current = queue.pop()
|
||||||
processed.add(current)
|
processed.add(current)
|
||||||
@ -2194,17 +2197,8 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
depth_map[0] = list()
|
depth_map[0] = list()
|
||||||
processed = set()
|
processed = set()
|
||||||
|
|
||||||
for n in self.nodes:
|
for n in self.root_nodes:
|
||||||
num_parents = in_count[str(n.uuid)]
|
depth_map[0].append(n)
|
||||||
if num_parents == 0:
|
|
||||||
# must be a root node or unconnected node
|
|
||||||
if n.depth != 0:
|
|
||||||
n.depth = 0
|
|
||||||
n.save()
|
|
||||||
|
|
||||||
# Only root node may have children
|
|
||||||
if out_count[str(n.uuid)] > 0:
|
|
||||||
depth_map[0].append(n)
|
|
||||||
|
|
||||||
# At most depth len(nodes) is possible
|
# At most depth len(nodes) is possible
|
||||||
for i in range(self.nodes.count()):
|
for i in range(self.nodes.count()):
|
||||||
@ -2227,7 +2221,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
|
|
||||||
for depth, nodes in depth_map.items():
|
for depth, nodes in depth_map.items():
|
||||||
for n in nodes:
|
for n in nodes:
|
||||||
if n.depth != depth:
|
if n.depth != depth and depth != 0:
|
||||||
n.depth = depth
|
n.depth = depth
|
||||||
n.save()
|
n.save()
|
||||||
|
|
||||||
|
|||||||
@ -293,6 +293,34 @@ document.addEventListener("alpine:init", () => {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// PubMed link widget
|
||||||
|
Alpine.data(
|
||||||
|
"doiWidget",
|
||||||
|
(fieldName, data, schema, uiSchema, mode, debugErrors, context = null) => ({
|
||||||
|
...baseWidget(
|
||||||
|
fieldName,
|
||||||
|
data,
|
||||||
|
schema,
|
||||||
|
uiSchema,
|
||||||
|
mode,
|
||||||
|
debugErrors,
|
||||||
|
context,
|
||||||
|
),
|
||||||
|
|
||||||
|
get value() {
|
||||||
|
return this.data[this.fieldName] || "";
|
||||||
|
},
|
||||||
|
set value(v) {
|
||||||
|
this.data[this.fieldName] = v;
|
||||||
|
},
|
||||||
|
get doiUrl() {
|
||||||
|
return this.value
|
||||||
|
? `https://dx.doi.org/${this.value}`
|
||||||
|
: null;
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
// Compound link widget
|
// Compound link widget
|
||||||
Alpine.data(
|
Alpine.data(
|
||||||
"compoundWidget",
|
"compoundWidget",
|
||||||
|
|||||||
@ -581,11 +581,11 @@ function draw(pathway, elem) {
|
|||||||
for (idx in parents) {
|
for (idx in parents) {
|
||||||
p = nodes[parents[idx]]
|
p = nodes[parents[idx]]
|
||||||
// console.log(p.depth)
|
// console.log(p.depth)
|
||||||
if (p.depth >= n.depth) {
|
// if (p.depth >= n.depth) {
|
||||||
// keep the .5 steps for pseudo nodes
|
// // keep the .5 steps for pseudo nodes
|
||||||
n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
// n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
||||||
// console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
// // console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -123,6 +123,17 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<!-- DOI link widget -->
|
||||||
|
<template
|
||||||
|
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'doi-link'"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
x-data="doiWidget(fieldName, data, schema, uiSchema, mode, debugErrors, context)"
|
||||||
|
>
|
||||||
|
{% include "components/widgets/doi_link_widget.html" %}
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Compound link widget -->
|
<!-- Compound link widget -->
|
||||||
<template
|
<template
|
||||||
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"
|
x-if="getWidget(fieldName, schema.properties[fieldName]) === 'compound-link'"
|
||||||
|
|||||||
69
templates/components/widgets/doi_link_widget.html
Normal file
69
templates/components/widgets/doi_link_widget.html
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
{# DOI link widget - pure HTML template #}
|
||||||
|
<div class="form-control">
|
||||||
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-baseline">
|
||||||
|
<!-- Label -->
|
||||||
|
<label class="label sm:w-48 sm:shrink-0">
|
||||||
|
<span
|
||||||
|
class="label-text"
|
||||||
|
:class="{
|
||||||
|
'text-error': $store.validationErrors.hasError(fieldName, context),
|
||||||
|
'text-sm text-base-content/60': isViewMode
|
||||||
|
}"
|
||||||
|
x-text="label"
|
||||||
|
></span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- Input column -->
|
||||||
|
<div class="flex-1">
|
||||||
|
<!-- Help text -->
|
||||||
|
<template x-if="helpText">
|
||||||
|
<div class="label">
|
||||||
|
<span
|
||||||
|
class="label-text-alt text-base-content/60"
|
||||||
|
x-text="helpText"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- View mode: display as link -->
|
||||||
|
<template x-if="isViewMode">
|
||||||
|
<div class="mt-1">
|
||||||
|
<template x-if="value && doiUrl">
|
||||||
|
<a
|
||||||
|
:href="doiUrl"
|
||||||
|
class="link link-primary"
|
||||||
|
target="_blank"
|
||||||
|
x-text="value"
|
||||||
|
></a>
|
||||||
|
</template>
|
||||||
|
<template x-if="!value">
|
||||||
|
<span class="text-base-content/50">—</span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Edit mode -->
|
||||||
|
<template x-if="isEditMode">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
class="input input-bordered w-full"
|
||||||
|
:class="{ 'input-error': $store.validationErrors.hasError(fieldName, context) }"
|
||||||
|
placeholder="DOI e.g. 10.1016/j.jhazmat.2016.08.036"
|
||||||
|
x-model="value"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Errors -->
|
||||||
|
<template x-if="$store.validationErrors.hasError(fieldName, context)">
|
||||||
|
<div class="label">
|
||||||
|
<template
|
||||||
|
x-for="errMsg in $store.validationErrors.getErrors(fieldName, context)"
|
||||||
|
:key="errMsg"
|
||||||
|
>
|
||||||
|
<span class="label-text-alt text-error" x-text="errMsg"></span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
2
uv.lock
generated
2
uv.lock
generated
@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "envipy-additional-information"
|
name = "envipy-additional-information"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#79285f522e0a6ed3f2e805dfeeb6b9fa5cea4323" }
|
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#f2f251e0214f016760348730c45e56183d961201" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user