forked from enviPath/enviPy
Compare commits
16 Commits
689f7998eb
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| 14cfc1e4d7 | |||
| 868bbf5c05 | |||
| be5ee1d1d7 | |||
| 20fd949dfd | |||
| c9b643fe6e | |||
| 1a9f1cf9af | |||
| c7c7e17e43 | |||
| 674e10c7fa | |||
| 8079b80d57 | |||
| 76e63fda2c | |||
| 1e43c298d2 | |||
| b39fc7eaf8 | |||
| a2fc9f72cb | |||
| 734b02767e | |||
| 9d70db2ca2 | |||
| fec26d0233 |
26
epapi/v1/endpoints/joblogs.py
Normal file
26
epapi/v1/endpoints/joblogs.py
Normal file
@ -0,0 +1,26 @@
|
||||
from django.conf import settings as s
|
||||
from ninja import Router
|
||||
from ninja_extra.pagination import paginate
|
||||
|
||||
from epdb.models import JobLog
|
||||
from ..pagination import EnhancedPageNumberPagination
|
||||
from ..schemas import JobLogOutSchema
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.get("/joblog/", response=EnhancedPageNumberPagination.Output[JobLogOutSchema])
|
||||
@paginate(
|
||||
EnhancedPageNumberPagination,
|
||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||
)
|
||||
def list_all_joblogs(request):
|
||||
"""
|
||||
List all JobLogs from reviewed packages.
|
||||
"""
|
||||
current_user = request.user
|
||||
|
||||
if current_user.is_superuser:
|
||||
return JobLog.objects.all().order_by("-created")
|
||||
else:
|
||||
return JobLog.objects.filter(user=current_user).order_by("-created")
|
||||
@ -15,6 +15,7 @@ from .endpoints import (
|
||||
additional_information,
|
||||
settings,
|
||||
groups,
|
||||
joblogs,
|
||||
)
|
||||
|
||||
# Main router with authentication
|
||||
@ -37,6 +38,7 @@ router.add_router("", structure.router)
|
||||
router.add_router("", additional_information.router)
|
||||
router.add_router("", settings.router)
|
||||
router.add_router("", groups.router)
|
||||
router.add_router("", joblogs.router)
|
||||
|
||||
if s.IUCLID_EXPORT_ENABLED:
|
||||
from epiuclid.api import router as iuclid_router
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
from ninja import FilterSchema, FilterLookup, Schema
|
||||
from datetime import datetime
|
||||
from typing import Annotated, Optional, List, Dict, Any
|
||||
from uuid import UUID
|
||||
|
||||
from django.urls import reverse
|
||||
from ninja import Field, FilterSchema, FilterLookup, Schema
|
||||
|
||||
|
||||
# Filter schema for query parameters
|
||||
class ReviewStatusFilter(FilterSchema):
|
||||
@ -133,3 +136,23 @@ class GroupOutSchema(Schema):
|
||||
url: str = ""
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class SimpleUserOutSchema(Schema):
|
||||
uuid: UUID
|
||||
url: str
|
||||
name: str = Field(alias="username")
|
||||
|
||||
|
||||
class JobLogOutSchema(Schema):
|
||||
user: SimpleUserOutSchema
|
||||
id: UUID = Field(alias="task_id")
|
||||
url: str
|
||||
name: str = Field(alias="job_name")
|
||||
created: datetime = Field(alias="created")
|
||||
status: str = Field(alias="status")
|
||||
done: Optional[datetime] = Field(None, alias="done_at")
|
||||
|
||||
@staticmethod
|
||||
def resolve_url(obj):
|
||||
return reverse("job detail", kwargs={"job_uuid": obj.task_id})
|
||||
|
||||
112
epdb/admin.py
112
epdb/admin.py
@ -1,5 +1,8 @@
|
||||
import logging
|
||||
|
||||
from django.conf import settings as s
|
||||
from django.contrib import admin
|
||||
from django.contrib import messages
|
||||
|
||||
from .models import (
|
||||
AdditionalInformation,
|
||||
@ -29,6 +32,8 @@ from .models import (
|
||||
|
||||
Package = s.GET_PACKAGE_MODEL()
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AdditionalInformationAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
@ -45,6 +50,113 @@ class UserAdmin(admin.ModelAdmin):
|
||||
"date_joined",
|
||||
]
|
||||
|
||||
actions = ["send_welcome_mail", "send_affiliation_mail"]
|
||||
|
||||
@admin.action(description="Send welcome mail")
|
||||
def send_welcome_mail(self, request, queryset):
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
|
||||
tpl = """Hello {username},
|
||||
|
||||
Your account has been successfully activated.
|
||||
|
||||
To log in, please visit
|
||||
https://envipath.org/password_reset/
|
||||
and request a new password.
|
||||
|
||||
If you have any questions or feedback, feel free to visit our community forum at
|
||||
https://community.envipath.org/.
|
||||
You do not need to register again for the forum - you can log in using your enviPath account by clicking "Log In" and then "Log in with enviPath."
|
||||
|
||||
Best regards,
|
||||
|
||||
The enviPath Team"""
|
||||
|
||||
users = []
|
||||
|
||||
for user in queryset:
|
||||
if user.is_active:
|
||||
logger.info(f"{user.username} already active - not sending mail again")
|
||||
continue
|
||||
try:
|
||||
msg = EmailMultiAlternatives(
|
||||
"Your enviPath Account Is Now Active",
|
||||
tpl.format(username=user.username),
|
||||
"admin@envipath.org",
|
||||
[user.email],
|
||||
bcc=["admin@envipath.org"],
|
||||
)
|
||||
|
||||
msg.send(fail_silently=False)
|
||||
|
||||
user.is_active = True
|
||||
user.password = "ASDF"
|
||||
user.save()
|
||||
|
||||
users.append(user)
|
||||
logger.info(f"{user.username} -> {user.email} mail sent")
|
||||
except Exception as e:
|
||||
logger.info(f"Error sending mail to {user.username}: {e}")
|
||||
|
||||
self.message_user(
|
||||
request, f"Sent welcome mail to {[u.email for u in users]}", messages.SUCCESS
|
||||
)
|
||||
|
||||
@admin.action(description="Send affiliation mail")
|
||||
def send_affiliation_mail(self, request, queryset):
|
||||
from django.core.mail import EmailMultiAlternatives
|
||||
|
||||
tpl = """Dear {username},
|
||||
|
||||
Thank you for your interest in enviPath!
|
||||
|
||||
Please note that the public enviPath system is intended for non-commercial use only.
|
||||
We see that you registered using the email address {email}.
|
||||
If possible, we kindly ask you to register using an official email address that reflects your affiliation (e.g., a university, NGO, or research organization).
|
||||
|
||||
If you would like us to update your account, simply reply to this email and let us know which address we should use.
|
||||
We will then change it in our system, and you will receive a password reset email at the new address.
|
||||
|
||||
If you are registering with a company email address and are interested in commercial use, you are very welcome to book a meeting with us so we can discuss how we can best support you.
|
||||
To book a meeting, please visit https://envipath.com/book
|
||||
|
||||
If changing to an affiliation email address is not possible, please contact us at registration@envipath.org
|
||||
|
||||
Best regards,
|
||||
|
||||
enviPath team"""
|
||||
|
||||
users = []
|
||||
|
||||
for user in queryset:
|
||||
if user.is_active or user.contacted:
|
||||
logger.info(
|
||||
f"{user.username} already active or already contacted - not sending mail again"
|
||||
)
|
||||
continue
|
||||
try:
|
||||
msg = EmailMultiAlternatives(
|
||||
"Regarding your enviPath registration",
|
||||
tpl.format(username=user.username, email=user.email),
|
||||
"admin@envipath.org",
|
||||
[user.email],
|
||||
bcc=["admin@envipath.org"],
|
||||
)
|
||||
|
||||
msg.send(fail_silently=False)
|
||||
|
||||
user.contacted = True
|
||||
user.save()
|
||||
|
||||
users.append(user)
|
||||
logger.info(f"{user.username} -> {user.email} affiliation mail sent")
|
||||
except Exception as e:
|
||||
logger.info(f"Error sending mail to {user.username}: {e}")
|
||||
|
||||
self.message_user(
|
||||
request, f"Sent affiliation mail to {[u.email for u in users]}", messages.SUCCESS
|
||||
)
|
||||
|
||||
|
||||
class UserPackagePermissionAdmin(admin.ModelAdmin):
|
||||
pass
|
||||
|
||||
@ -386,23 +386,50 @@ class PackageSchema(Schema):
|
||||
|
||||
@staticmethod
|
||||
def resolve_readers(obj: Package):
|
||||
users = User.objects.filter(
|
||||
id__in=UserPackagePermission.objects.filter(
|
||||
package=obj, permission=UserPackagePermission.READ[0]
|
||||
).values_list("user", flat=True)
|
||||
).distinct()
|
||||
readers = []
|
||||
|
||||
return [{u.id: u.get_name()} for u in users]
|
||||
user_ids = UserPackagePermission.objects.filter(package=obj).values_list("user", flat=True)
|
||||
|
||||
users = User.objects.filter(id__in=user_ids).distinct()
|
||||
|
||||
for u in users:
|
||||
readers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
|
||||
|
||||
group_ids = GroupPackagePermission.objects.filter(package=obj).values_list(
|
||||
"group", flat=True
|
||||
)
|
||||
|
||||
groups = Group.objects.filter(id__in=group_ids).distinct()
|
||||
|
||||
for g in groups:
|
||||
readers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
|
||||
|
||||
return readers
|
||||
|
||||
@staticmethod
|
||||
def resolve_writers(obj: Package):
|
||||
users = User.objects.filter(
|
||||
id__in=UserPackagePermission.objects.filter(
|
||||
package=obj, permission=UserPackagePermission.WRITE[0]
|
||||
).values_list("user", flat=True)
|
||||
).distinct()
|
||||
writers = []
|
||||
|
||||
return [{u.id: u.get_name()} for u in users]
|
||||
user_ids = UserPackagePermission.objects.filter(
|
||||
package=obj,
|
||||
permission__in=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]],
|
||||
).values_list("user", flat=True)
|
||||
|
||||
users = User.objects.filter(id__in=user_ids).distinct()
|
||||
|
||||
for u in users:
|
||||
writers.append({"id": str(u.url), "identifier": "user", "name": u.get_name()})
|
||||
|
||||
group_ids = GroupPackagePermission.objects.filter(
|
||||
package=obj, permission=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]]
|
||||
).values_list("group", flat=True)
|
||||
|
||||
groups = Group.objects.filter(id__in=group_ids).distinct()
|
||||
|
||||
for g in groups:
|
||||
writers.append({"id": str(g.url), "identifier": "group", "name": g.get_name()})
|
||||
|
||||
return writers
|
||||
|
||||
@staticmethod
|
||||
def resolve_review_comment(obj):
|
||||
@ -799,6 +826,27 @@ def delete_compound(request, package_uuid, compound_uuid):
|
||||
}
|
||||
|
||||
|
||||
class CreateCompoundStructure(Schema):
|
||||
smiles: str
|
||||
name: str | None = None
|
||||
description: str | None = None
|
||||
inchi: str | None = None
|
||||
molfile: str | None = None
|
||||
|
||||
|
||||
@router.post("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure")
|
||||
def create_package_compound_structure(
|
||||
request, package_uuid, compound_uuid, structure: Form[CreateCompoundStructure]
|
||||
):
|
||||
try:
|
||||
p = get_package_for_write(request.user, package_uuid)
|
||||
c = Compound.objects.get(package=p, uuid=compound_uuid)
|
||||
cs = CompoundStructure.create(c, structure.smiles, structure.name, structure.description)
|
||||
return redirect(cs.url)
|
||||
except ValueError as e:
|
||||
return 400, {"message": str(e)}
|
||||
|
||||
|
||||
@router.delete(
|
||||
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
||||
)
|
||||
@ -1325,6 +1373,7 @@ class ScenarioSchema(Schema):
|
||||
aliases: List[str] = Field([], alias="aliases")
|
||||
collection: Dict["str", List[Dict[str, Any]]] = Field([], alias="collection")
|
||||
collectionID: Optional[str] = None
|
||||
date: str = Field(None, alias="scenario_date")
|
||||
description: str = Field(None, alias="description")
|
||||
id: str = Field(None, alias="url")
|
||||
identifier: str = "scenario"
|
||||
@ -1468,28 +1517,56 @@ def create_package_additional_information(request, package_uuid):
|
||||
scen = request.POST.get("scenario")
|
||||
scenario = Scenario.objects.get(package=p, url=scen)
|
||||
|
||||
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||
attach_obj = url_parser.get_object()
|
||||
if request.POST.get("adInfoTypes[]"):
|
||||
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||
attach_obj = url_parser.get_object()
|
||||
|
||||
if not hasattr(attach_obj, "additional_information"):
|
||||
raise ValueError("Can't attach additional information to this object!")
|
||||
if not hasattr(attach_obj, "additional_information"):
|
||||
raise ValueError("Can't attach additional information to this object!")
|
||||
|
||||
if not attach_obj.url.startswith(p.url):
|
||||
raise ValueError(
|
||||
"Additional Information can only be set to objects stored in the same package!"
|
||||
)
|
||||
if not attach_obj.url.startswith(p.url):
|
||||
raise ValueError(
|
||||
"Additional Information can only be set to objects stored in the same package!"
|
||||
)
|
||||
|
||||
types = request.POST.get("adInfoTypes[]", "").split(",")
|
||||
types = request.POST.get("adInfoTypes[]", "").split(",")
|
||||
|
||||
for t in types:
|
||||
ai = build_additional_information_from_request(request, t)
|
||||
for t in types:
|
||||
ai = build_additional_information_from_request(request, t)
|
||||
|
||||
AdditionalInformation.create(
|
||||
p,
|
||||
ai,
|
||||
scenario=scenario,
|
||||
content_object=attach_obj,
|
||||
)
|
||||
AdditionalInformation.create(
|
||||
p,
|
||||
ai,
|
||||
scenario=scenario,
|
||||
content_object=attach_obj,
|
||||
)
|
||||
|
||||
elif request.POST.get("ais"):
|
||||
import json
|
||||
|
||||
parsed_ais = json.loads(request.POST.get("ais"))
|
||||
|
||||
for ai_type, ais in parsed_ais.items():
|
||||
for ai in ais:
|
||||
attach_obj = None
|
||||
if ai.get("related"):
|
||||
url_parser = EPDBURLParser(ai.get("related").get("url"))
|
||||
attach_obj = url_parser.get_object()
|
||||
|
||||
if not hasattr(attach_obj, "additional_information"):
|
||||
raise ValueError("Can't attach additional information to this object!")
|
||||
|
||||
if not attach_obj.url.startswith(p.url):
|
||||
raise ValueError(
|
||||
"Additional Information can only be set to objects stored in the same package!"
|
||||
)
|
||||
|
||||
AdditionalInformation.create(
|
||||
p,
|
||||
AdditionalInformation.from_dict(ai_type, ai),
|
||||
scenario=scenario,
|
||||
content_object=attach_obj,
|
||||
)
|
||||
|
||||
# TODO implement additional information endpoint ?
|
||||
return redirect(f"{scenario.url}")
|
||||
@ -1540,6 +1617,7 @@ class PathwayNode(Schema):
|
||||
name: str = Field(None, alias="name")
|
||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
||||
pseudo: bool = Field(False, alias="pseudo")
|
||||
|
||||
@staticmethod
|
||||
def resolve_atom_count(obj: Node):
|
||||
@ -1693,6 +1771,29 @@ def create_package_pathway(
|
||||
return 403, {"message": str(e)}
|
||||
|
||||
|
||||
@router.post("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
||||
def update_pathway(request, package_uuid, pathway_uuid):
|
||||
try:
|
||||
p = get_package_for_write(request.user, package_uuid)
|
||||
|
||||
if request.POST.get("scenario"):
|
||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||
scen = Scenario.objects.get(package=p, url=request.POST.get("scenario"))
|
||||
|
||||
pw.scenarios.add(scen)
|
||||
pw.save()
|
||||
|
||||
return redirect(f"{pw.url}")
|
||||
|
||||
else:
|
||||
return 400, {"message": "No scenario specified!"}
|
||||
|
||||
except ValueError:
|
||||
return 403, {
|
||||
"message": f"Deleting Pathway with id {pathway_uuid} failed due to insufficient rights!"
|
||||
}
|
||||
|
||||
|
||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
||||
def delete_pathway(request, package_uuid, pathway_uuid):
|
||||
try:
|
||||
@ -1813,7 +1914,7 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
||||
|
||||
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
||||
node_depth = int(n.nodeDepth)
|
||||
node_depth = int(float(n.nodeDepth))
|
||||
else:
|
||||
node_depth = -1
|
||||
|
||||
@ -1958,6 +2059,10 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
||||
for pr in e.products.split(","):
|
||||
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
|
||||
|
||||
multi_step = False
|
||||
if e.multistep and e.multistep.strip() == "true":
|
||||
multi_step = True
|
||||
|
||||
new_e = Edge.create(
|
||||
pathway=pw,
|
||||
start_nodes=educts,
|
||||
@ -1965,8 +2070,12 @@ def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
||||
rule=None,
|
||||
name=None,
|
||||
description=e.edgeReason,
|
||||
multi_step=multi_step,
|
||||
)
|
||||
|
||||
# Update depths as sideeffect of above operation
|
||||
pw.update_depths()
|
||||
|
||||
return redirect(new_e.url)
|
||||
except ValueError:
|
||||
return 403, {"message": "Adding Edge failed!"}
|
||||
|
||||
@ -995,52 +995,9 @@ class PackageManager(object):
|
||||
|
||||
print("Fixing Node depths...")
|
||||
total_pws = Pathway.objects.filter(package=pack).count()
|
||||
|
||||
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
||||
in_count = defaultdict(lambda: 0)
|
||||
out_count = defaultdict(lambda: 0)
|
||||
|
||||
for e in pw.edges:
|
||||
# TODO check if this will remain
|
||||
for react in e.start_nodes.all():
|
||||
out_count[str(react.uuid)] += 1
|
||||
|
||||
for prod in e.end_nodes.all():
|
||||
in_count[str(prod.uuid)] += 1
|
||||
|
||||
root_nodes = []
|
||||
for n in pw.nodes:
|
||||
num_parents = in_count[str(n.uuid)]
|
||||
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:
|
||||
root_nodes.append(n)
|
||||
|
||||
levels = [root_nodes]
|
||||
seen = set()
|
||||
# Do a bfs to determine depths starting with level 0 a.k.a. root nodes
|
||||
for i, level_nodes in enumerate(levels):
|
||||
new_level = []
|
||||
for n in level_nodes:
|
||||
for e in n.out_edges.all():
|
||||
for prod in e.end_nodes.all():
|
||||
if str(prod.uuid) not in seen:
|
||||
old_depth = prod.depth
|
||||
if old_depth != i + 1:
|
||||
prod.depth = i + 1
|
||||
prod.save()
|
||||
|
||||
new_level.append(prod)
|
||||
|
||||
seen.add(str(n.uuid))
|
||||
|
||||
if new_level:
|
||||
levels.append(new_level)
|
||||
|
||||
pw.update_depths()
|
||||
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
||||
|
||||
return pack
|
||||
|
||||
17
epdb/migrations/0024_user_contacted.py
Normal file
17
epdb/migrations/0024_user_contacted.py
Normal file
@ -0,0 +1,17 @@
|
||||
# Generated by Django 6.0.3 on 2026-04-21 19:56
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("epdb", "0023_alter_compoundstructure_options_and_more"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="user",
|
||||
name="contacted",
|
||||
field=models.BooleanField(blank=True, null=True),
|
||||
),
|
||||
]
|
||||
56
epdb/migrations/0025_auto_20260511_2025.py
Normal file
56
epdb/migrations/0025_auto_20260511_2025.py
Normal file
@ -0,0 +1,56 @@
|
||||
# Generated by Django 6.0.3 on 2026-05-11 20:25
|
||||
|
||||
from django.db import migrations
|
||||
from envipy_additional_information import HalfLife, HalfLifeModel, HalfLifeWS
|
||||
|
||||
MAPPING = {
|
||||
"": HalfLifeModel.OTHER,
|
||||
"HS-SFO": HalfLifeModel.HS_SFO,
|
||||
"FOMC": HalfLifeModel.FOMC,
|
||||
"FOTC": HalfLifeModel.DFOP,
|
||||
"FMOC": HalfLifeModel.FOMC,
|
||||
"DFOP": HalfLifeModel.DFOP,
|
||||
"SFO + SFO": HalfLifeModel.SFO_SFO,
|
||||
"FOMC-SFO": HalfLifeModel.FOMC_SFO,
|
||||
"first order kinetics": HalfLifeModel.SFO,
|
||||
"SFO²": HalfLifeModel.SFO,
|
||||
"HS": HalfLifeModel.HS,
|
||||
"top down": HalfLifeModel.OTHER,
|
||||
"SFO": HalfLifeModel.SFO,
|
||||
"First Order": HalfLifeModel.SFO,
|
||||
"SFO/SFO": HalfLifeModel.SFO_SFO,
|
||||
"FOMC + SFO": HalfLifeModel.FOMC_SFO,
|
||||
"true": HalfLifeModel.SFO,
|
||||
"SFO-SFO": HalfLifeModel.SFO_SFO,
|
||||
"DFOP-SFO": HalfLifeModel.DFOP_SFO,
|
||||
}
|
||||
|
||||
|
||||
def forward_func(apps, schema_editor):
|
||||
AdditionalInformation = apps.get_model("epdb", "AdditionalInformation")
|
||||
|
||||
hls = AdditionalInformation.objects.filter(type="HalfLife")
|
||||
|
||||
for hl in hls:
|
||||
data = hl.data
|
||||
data["model"] = MAPPING[data["model"]].value
|
||||
hl.data = HalfLife(**data).model_dump(mode="json")
|
||||
hl.save()
|
||||
|
||||
hlws = AdditionalInformation.objects.filter(type="HalfLifeWS")
|
||||
|
||||
for hl in hlws:
|
||||
data = hl.data
|
||||
data["model"] = MAPPING[data["model"]].value
|
||||
hl.data = HalfLifeWS(**data).model_dump(mode="json")
|
||||
hl.save()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("epdb", "0024_user_contacted"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(forward_func, reverse_code=migrations.RunPython.noop),
|
||||
]
|
||||
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),
|
||||
]
|
||||
@ -75,6 +75,7 @@ class User(AbstractUser):
|
||||
blank=False,
|
||||
)
|
||||
is_reviewer = models.BooleanField(default=False)
|
||||
contacted = models.BooleanField(null=True, blank=True)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
REQUIRED_FIELDS = ["username"]
|
||||
@ -1694,7 +1695,7 @@ class Reaction(
|
||||
if name is not None and name.strip() != "":
|
||||
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
if description is not None and name.strip() != "":
|
||||
if description is not None and description.strip() != "":
|
||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||
|
||||
r.multi_step = multi_step
|
||||
@ -1772,7 +1773,7 @@ class Reaction(
|
||||
return new_reaction
|
||||
|
||||
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
|
||||
def as_svg(self):
|
||||
@ -1885,6 +1886,9 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
if n not in queue:
|
||||
queue.append(n)
|
||||
|
||||
for i in queue:
|
||||
processed.add(i)
|
||||
|
||||
while len(queue):
|
||||
current = queue.pop()
|
||||
processed.add(current)
|
||||
@ -2177,6 +2181,50 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
||||
):
|
||||
return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description)
|
||||
|
||||
def update_depths(self):
|
||||
# Collect number of in and out links per node
|
||||
in_count = defaultdict(lambda: 0)
|
||||
out_count = defaultdict(lambda: 0)
|
||||
|
||||
for e in self.edges:
|
||||
for react in e.start_nodes.all():
|
||||
out_count[str(react.uuid)] += 1
|
||||
|
||||
for prod in e.end_nodes.all():
|
||||
in_count[str(prod.uuid)] += 1
|
||||
|
||||
depth_map = {}
|
||||
depth_map[0] = list()
|
||||
processed = set()
|
||||
|
||||
for n in self.root_nodes:
|
||||
depth_map[0].append(n)
|
||||
|
||||
# At most depth len(nodes) is possible
|
||||
for i in range(self.nodes.count()):
|
||||
level_nodes = depth_map.get(i, [])
|
||||
|
||||
if len(level_nodes) == 0:
|
||||
break
|
||||
|
||||
unique_next_level = set()
|
||||
for n in level_nodes:
|
||||
processed.add(n)
|
||||
for e in self.edges:
|
||||
if n in e.start_nodes.all():
|
||||
for p in e.end_nodes.all():
|
||||
if p not in processed:
|
||||
unique_next_level.add(p)
|
||||
|
||||
if len(unique_next_level) > 0:
|
||||
depth_map[i + 1] = list(unique_next_level)
|
||||
|
||||
for depth, nodes in depth_map.items():
|
||||
for n in nodes:
|
||||
if n.depth != depth and depth != 0:
|
||||
n.depth = depth
|
||||
n.save()
|
||||
|
||||
|
||||
class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin):
|
||||
pathway = models.ForeignKey(
|
||||
@ -2404,6 +2452,8 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
rule: Optional[Rule] = None,
|
||||
name: Optional[str] = None,
|
||||
description: Optional[str] = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
e = Edge()
|
||||
e.pathway = pathway
|
||||
@ -2433,7 +2483,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
||||
educts=[n.default_node_label for n in e.start_nodes.all()],
|
||||
products=[n.default_node_label for n in e.end_nodes.all()],
|
||||
rules=rule,
|
||||
multi_step=False,
|
||||
multi_step=kwargs.get("multi_step", False),
|
||||
)
|
||||
|
||||
e.edge_label = r
|
||||
@ -4441,18 +4491,22 @@ class AdditionalInformation(models.Model):
|
||||
|
||||
return f"{self.scenario.url}/additional-information/{self.uuid}"
|
||||
|
||||
def get(self) -> "EnviPyModel":
|
||||
@staticmethod
|
||||
def from_dict(ai_type: str, ai_data: Dict[str, Any]):
|
||||
from envipy_additional_information import registry
|
||||
|
||||
MAPPING = {c.__name__: c for c in registry.list_models().values()}
|
||||
try:
|
||||
inst = MAPPING[self.type](**self.data)
|
||||
inst = MAPPING[ai_type](**ai_data)
|
||||
except Exception as e:
|
||||
print(f"Error loading {self.type}: {e}")
|
||||
print(f"Error loading {ai_type}: {e}")
|
||||
raise e
|
||||
|
||||
inst.__dict__["uuid"] = str(self.uuid)
|
||||
return inst
|
||||
|
||||
def get(self) -> "EnviPyModel":
|
||||
inst = AdditionalInformation.from_dict(self.type, self.data)
|
||||
inst.__dict__["uuid"] = str(self.uuid)
|
||||
return inst
|
||||
|
||||
def __str__(self) -> str:
|
||||
|
||||
@ -2506,6 +2506,9 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
|
||||
substrate_nodes, product_nodes, name=edge_name, description=edge_description
|
||||
)
|
||||
|
||||
# Update depths as sideeffect of above operation
|
||||
current_pathway.update_depths()
|
||||
|
||||
return redirect(current_pathway.url)
|
||||
|
||||
else:
|
||||
@ -3077,12 +3080,21 @@ def jobs(request):
|
||||
{"Home": s.SERVER_URL},
|
||||
{"Jobs": s.SERVER_URL + "/jobs"},
|
||||
]
|
||||
if current_user.is_superuser:
|
||||
context["jobs"] = JobLog.objects.all().order_by("-created")
|
||||
else:
|
||||
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
||||
# if current_user.is_superuser:
|
||||
# context["jobs"] = JobLog.objects.all().order_by("-created")
|
||||
# else:
|
||||
# context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
|
||||
|
||||
return render(request, "collections/joblog.html", context)
|
||||
# Context for paginated template
|
||||
context["entity_type"] = "joblog"
|
||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/joblog/"
|
||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||
context["list_title"] = "joblog"
|
||||
context["list_mode"] = "combined"
|
||||
|
||||
return render(request, "collections/joblog_paginated.html", context)
|
||||
|
||||
# return render(request, "collections/joblog.html", context)
|
||||
|
||||
elif request.method == "POST":
|
||||
job_name = request.POST.get("job-name")
|
||||
|
||||
@ -46,7 +46,7 @@ class PepperPrediction(PropertyPrediction):
|
||||
|
||||
import matplotlib.patches as mpatches
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
from matplotlib.figure import Figure
|
||||
from scipy import stats
|
||||
|
||||
"""
|
||||
@ -101,7 +101,8 @@ class PepperPrediction(PropertyPrediction):
|
||||
mask_red = x > vp
|
||||
|
||||
# Plot
|
||||
fig, ax = plt.subplots(figsize=(9, 5.5))
|
||||
fig = Figure(figsize=(9, 5.5))
|
||||
ax = fig.subplots()
|
||||
ax.plot(x, y, color="#1f4e79", lw=2, label="Lognormal PDF")
|
||||
|
||||
if np.any(mask_green):
|
||||
@ -146,13 +147,12 @@ class PepperPrediction(PropertyPrediction):
|
||||
]
|
||||
ax.legend(handles=patches, frameon=True)
|
||||
|
||||
plt.tight_layout()
|
||||
fig.tight_layout()
|
||||
|
||||
# --- Export to SVG string ---
|
||||
buf = io.StringIO()
|
||||
fig.savefig(buf, format="svg", bbox_inches="tight")
|
||||
svg = buf.getvalue()
|
||||
plt.close(fig)
|
||||
buf.close()
|
||||
|
||||
return svg
|
||||
|
||||
@ -187,8 +187,9 @@ class Pepper:
|
||||
groups = [group for group in dataset.group_by("structure_id")]
|
||||
|
||||
# Unless explicitly set compute everything serial
|
||||
if os.environ.get("N_PEPPER_THREADS", 1) > 1:
|
||||
results = Parallel(n_jobs=os.environ["N_PEPPER_THREADS"])(
|
||||
n_threads = int(os.environ.get("N_PEPPER_THREADS", 1))
|
||||
if n_threads > 1:
|
||||
results = Parallel(n_jobs=n_threads)(
|
||||
delayed(compute_bayes_per_group)(group[1])
|
||||
for group in dataset.group_by("structure_id")
|
||||
)
|
||||
|
||||
@ -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
|
||||
Alpine.data(
|
||||
"compoundWidget",
|
||||
|
||||
@ -463,7 +463,7 @@ function draw(pathway, elem) {
|
||||
// TODO needs to be generic once we store it as AddInf
|
||||
for (var s of n.predicted_properties["PepperPrediction"]) {
|
||||
if (s["mean"] != null) {
|
||||
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + "<br>"
|
||||
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + " days<br>"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -581,11 +581,11 @@ function draw(pathway, elem) {
|
||||
for (idx in parents) {
|
||||
p = nodes[parents[idx]]
|
||||
// console.log(p.depth)
|
||||
if (p.depth >= n.depth) {
|
||||
// keep the .5 steps for pseudo nodes
|
||||
n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
||||
// console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
||||
}
|
||||
// if (p.depth >= n.depth) {
|
||||
// // keep the .5 steps for pseudo nodes
|
||||
// n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1);
|
||||
// // console.log("Adjusting", orig_depth, Math.floor(p.depth + 1));
|
||||
// }
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
102
templates/collections/_joblog_paginated_list_partial.html
Normal file
102
templates/collections/_joblog_paginated_list_partial.html
Normal file
@ -0,0 +1,102 @@
|
||||
{# Partial for paginated list content - expects to be inside a remotePaginatedList Alpine.js context #}
|
||||
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
|
||||
{% load envipytags %}
|
||||
{# Loading state #}
|
||||
<div
|
||||
x-show="isLoading"
|
||||
class="mx-auto flex h-32 w-32 items-center justify-center"
|
||||
>
|
||||
{% include "components/loading-spinner.html" %}
|
||||
</div>
|
||||
|
||||
{# Error state #}
|
||||
<div
|
||||
x-show="!isLoading && error"
|
||||
class="alert alert-error/50 text-sm"
|
||||
x-text="error"
|
||||
></div>
|
||||
|
||||
{# Content #}
|
||||
<template x-if="!isLoading && !error">
|
||||
<div>
|
||||
{# Empty state #}
|
||||
<div
|
||||
x-show="totalItems === 0"
|
||||
class="text-base-content/70 py-8 text-center"
|
||||
>
|
||||
<p>No {{ empty_text|default:"items" }} found.</p>
|
||||
</div>
|
||||
|
||||
{# Items list #}
|
||||
<ul class="menu bg-base-100 rounded-box w-full" x-show="totalItems > 0">
|
||||
<table class="table-zebra table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>User</th>
|
||||
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Status</th>
|
||||
<th>Queued At</th>
|
||||
<th>Done At</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template x-for="obj in paginatedItems" :key="obj.url">
|
||||
<tr>
|
||||
<td>
|
||||
<a :href="obj.user.url"><span x-text="obj.user.name"></span></a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a :href="obj.url"><span x-text="obj.id"></span></a>
|
||||
</td>
|
||||
<td><span x-text="obj.name"></span></td>
|
||||
<td><span x-text="obj.status"></span></td>
|
||||
<td><span x-text="obj.created"></span></td>
|
||||
<td><span x-text="obj.done"></span></td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</ul>
|
||||
|
||||
{# Pagination controls #}
|
||||
<div
|
||||
x-show="totalPages > 1"
|
||||
class="mt-4 flex items-center justify-between px-2"
|
||||
>
|
||||
<span class="text-base-content/70 text-sm">
|
||||
Showing <span x-text="showingStart"></span>-<span
|
||||
x-text="showingEnd"
|
||||
></span>
|
||||
of <span x-text="totalItems"></span>
|
||||
</span>
|
||||
<div class="join">
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
:disabled="currentPage === 1"
|
||||
@click="prevPage()"
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<template x-for="item in pageNumbers" :key="item.key">
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
:class="{ 'btn-active': item.page === currentPage }"
|
||||
:disabled="item.isEllipsis"
|
||||
@click="!item.isEllipsis && goToPage(item.page)"
|
||||
x-text="item.page"
|
||||
></button>
|
||||
</template>
|
||||
<button
|
||||
class="join-item btn btn-sm"
|
||||
:disabled="currentPage === totalPages"
|
||||
@click="nextPage()"
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
13
templates/collections/joblog_paginated.html
Normal file
13
templates/collections/joblog_paginated.html
Normal file
@ -0,0 +1,13 @@
|
||||
{% extends "collections/paginated_base.html" %}
|
||||
|
||||
{% block page_title %}Jobs{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
{% endblock action_button %}
|
||||
|
||||
{% block action_modals %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
{% block description %}
|
||||
<p>List of Jobs submitted.</p>
|
||||
{% endblock description %}
|
||||
@ -37,7 +37,11 @@
|
||||
perPage: {{ per_page|default:50 }}
|
||||
})"
|
||||
>
|
||||
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||
{% if entity_type == 'joblog' %}
|
||||
{% include "collections/_joblog_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||
{% else %}
|
||||
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
||||
{% endif %}
|
||||
</div>
|
||||
{% else %}
|
||||
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
||||
|
||||
@ -123,6 +123,17 @@
|
||||
</div>
|
||||
</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 -->
|
||||
<template
|
||||
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>
|
||||
@ -64,7 +64,7 @@
|
||||
|
||||
import logging
|
||||
|
||||
from envipy_additional_information import HalfLife, HalfLifeWS
|
||||
from envipy_additional_information import HalfLife, HalfLifeWS, HalfLifeModel
|
||||
from envipy_additional_information.information import Interval
|
||||
from envipy_additional_information.parsers import (
|
||||
AcidityParser,
|
||||
@ -125,6 +125,7 @@ from envipy_additional_information.parsers import (
|
||||
LiquidMatrixSourceParser,
|
||||
OxygenUptakeRateParser,
|
||||
InitiatingOrganismParser,
|
||||
PFASConfidenceParser,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@ -141,7 +142,7 @@ def get_parameter(request, paramname):
|
||||
res = request.POST.get(paramname)
|
||||
if res is not None and res.strip() != "":
|
||||
return res
|
||||
return ValueError("Not all parameters are set!")
|
||||
raise ValueError("Not all parameters are set!")
|
||||
|
||||
|
||||
def get_parameter_or_empty_string(request, paramname):
|
||||
@ -473,17 +474,12 @@ def build_additional_information_from_request(request, type_):
|
||||
|
||||
comment = get_parameter_or_empty_string(request, "comment")
|
||||
source = get_parameter_or_empty_string(request, "source")
|
||||
first_order = get_parameter_or_empty_string(request, "firstOrder")
|
||||
# first_order = get_parameter_or_empty_string(request, "firstOrder")
|
||||
model = get_parameter_or_empty_string(request, "model")
|
||||
fit = get_parameter_or_empty_string(request, "fit")
|
||||
|
||||
if first_order != "":
|
||||
if model != "":
|
||||
raise ValueError("not both, model and firstOrder can be set!")
|
||||
if first_order == "true":
|
||||
model = "SFO"
|
||||
else:
|
||||
logger.info("firstOrder is set to false which is not meaningful")
|
||||
if model:
|
||||
model = HalfLifeModel(model.upper())
|
||||
|
||||
return HalfLife(model=model, fit=fit, comment=comment, dt50=i, source=source)
|
||||
|
||||
@ -508,6 +504,10 @@ def build_additional_information_from_request(request, type_):
|
||||
comment_ws = get_parameter_or_empty_string(request, "comment_ws")
|
||||
source_ws = get_parameter_or_empty_string(request, "source_ws")
|
||||
model_ws = get_parameter_or_empty_string(request, "model_ws")
|
||||
|
||||
if model_ws:
|
||||
model_ws = HalfLifeModel(model_ws.upper())
|
||||
|
||||
fit_ws = get_parameter_or_empty_string(request, "fit_ws")
|
||||
|
||||
dt50_total = IntervalParser.from_string(hl_ws_total)
|
||||
@ -674,7 +674,8 @@ def build_additional_information_from_request(request, type_):
|
||||
elif type_ == "studywst":
|
||||
# study_wst_cond = get_parameter(request, "studywstcond")
|
||||
raise ValueError("studywstcond is not yet implemented")
|
||||
|
||||
elif type_ == "pfasconfidence":
|
||||
return PFASConfidenceParser.from_string(get_parameter(request, "level"))
|
||||
else:
|
||||
raise ValueError(f"No corresponding AdditionalInformation for {type_} found!")
|
||||
|
||||
|
||||
34
uv.lock
generated
34
uv.lock
generated
@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
||||
[[package]]
|
||||
name = "envipy-additional-information"
|
||||
version = "0.4.2"
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#0a608c85c73a6ef5c38afea87d2b57fb43f01a70" }
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#f2f251e0214f016760348730c45e56183d961201" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
@ -2763,9 +2763,9 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "sys_platform != 'linux' and sys_platform != 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:a47b7986bee3f61ad217d8a8ce24605809ab425baf349f97de758815edd2ef54", upload-time = "2025-10-01T23:35:50Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:fbe2e149c5174ef90d29a5f84a554dfaf28e003cb4f61fa2c8c024c17ec7ca58", upload-time = "2025-10-01T23:35:52Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:057efd30a6778d2ee5e2374cd63a63f63311aa6f33321e627c655df60abdd390", upload-time = "2025-10-01T23:35:55Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -2785,19 +2785,19 @@ dependencies = [
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'linux' or sys_platform == 'win32'" },
|
||||
]
|
||||
wheels = [
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-linux_s390x.whl", hash = "sha256:0e34e276722ab7dd0dffa9e12fe2135a9b34a0e300c456ed7ad6430229404eb5", upload-time = "2025-10-01T23:33:41Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:610f600c102386e581327d5efc18c0d6edecb9820b4140d26163354a99cd800d", upload-time = "2025-10-01T23:33:45Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:cb9a8ba8137ab24e36bf1742cb79a1294bd374db570f09fc15a5e1318160db4e", upload-time = "2025-10-01T23:33:48Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_amd64.whl", hash = "sha256:2be20b2c05a0cce10430cc25f32b689259640d273232b2de357c35729132256d", upload-time = "2025-10-01T23:33:52Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp312-cp312-win_arm64.whl", hash = "sha256:99fc421a5d234580e45957a7b02effbf3e1c884a5dd077afc85352c77bf41434", upload-time = "2025-10-01T23:34:10Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-linux_s390x.whl", hash = "sha256:8b5882276633cf91fe3d2d7246c743b94d44a7e660b27f1308007fdb1bb89f7d", upload-time = "2025-10-01T23:34:15Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a5064b5e23772c8d164068cc7c12e01a75faf7b948ecd95a0d4007d7487e5f25", upload-time = "2025-10-01T23:34:19Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:8f81dedb4c6076ec325acc3b47525f9c550e5284a18eae1d9061c543f7b6e7de", upload-time = "2025-10-01T23:34:23Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_amd64.whl", hash = "sha256:e1ee1b2346ade3ea90306dfbec7e8ff17bc220d344109d189ae09078333b0856", upload-time = "2025-10-01T23:34:28Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313-win_arm64.whl", hash = "sha256:64c187345509f2b1bb334feed4666e2c781ca381874bde589182f81247e61f88", upload-time = "2025-10-01T23:34:45Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:af81283ac671f434b1b25c95ba295f270e72db1fad48831eb5e4748ff9840041", upload-time = "2025-10-01T23:34:50Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a9dbb6f64f63258bc811e2c0c99640a81e5af93c531ad96e95c5ec777ea46dab", upload-time = "2025-10-01T23:34:53Z" },
|
||||
{ url = "https://download-r2.pytorch.org/whl/cpu/torch-2.8.0%2Bcpu-cp313-cp313t-win_amd64.whl", hash = "sha256:6d93a7165419bc4b2b907e859ccab0dea5deeab261448ae9a5ec5431f14c0e64", upload-time = "2025-10-01T23:34:58Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in New Issue
Block a user