6 Commits

Author SHA1 Message Date
99c796813a wip
Some checks failed
CI / test (pull_request) Failing after 15s
API CI / api-tests (pull_request) Failing after 18s
2026-05-28 21:55:36 +02:00
3b5d299128 API PES
Some checks failed
CI / test (pull_request) Failing after 14s
API CI / api-tests (pull_request) Failing after 20s
2026-05-12 13:16:39 +02:00
38e901a51e PW interactions 2026-05-12 13:16:39 +02:00
c92fccaf8e minor 2026-05-12 13:16:39 +02:00
5eb3ebac89 Wip 2026-05-12 13:16:39 +02:00
d9530ce755 adjusted migration
Initial bayer app

Show Pack Classification

Adjusted docker compose to bayer specifics

Adjusted Dockerfile for Bayer

Adding secret flags to group, add secret pools to packages

Adjusted View for Package creation

Prep configs, added Package Create Modal

wip

More on PES

wip

wip
2026-05-12 13:16:39 +02:00
11 changed files with 46 additions and 341 deletions

View File

@ -1,26 +0,0 @@
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")

View File

@ -15,7 +15,6 @@ from .endpoints import (
additional_information,
settings,
groups,
joblogs,
)
# Main router with authentication
@ -38,7 +37,6 @@ 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

View File

@ -1,10 +1,7 @@
from datetime import datetime
from ninja import FilterSchema, FilterLookup, Schema
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):
@ -136,23 +133,3 @@ 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})

View File

@ -439,50 +439,23 @@ class PackageSchema(Schema):
@staticmethod
def resolve_readers(obj: Package):
readers = []
users = User.objects.filter(
id__in=UserPackagePermission.objects.filter(
package=obj, permission=UserPackagePermission.READ[0]
).values_list("user", flat=True)
).distinct()
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
return [{u.id: u.get_name()} for u in users]
@staticmethod
def resolve_writers(obj: Package):
writers = []
user_ids = UserPackagePermission.objects.filter(
package=obj,
permission__in=[UserPackagePermission.WRITE[0], UserPackagePermission.ALL[0]],
users = User.objects.filter(
id__in=UserPackagePermission.objects.filter(
package=obj, permission=UserPackagePermission.WRITE[0]
).values_list("user", flat=True)
).distinct()
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
return [{u.id: u.get_name()} for u in users]
@staticmethod
def resolve_review_comment(obj):
@ -899,27 +872,6 @@ 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}"
)
@ -1446,7 +1398,6 @@ 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"
@ -1590,7 +1541,6 @@ def create_package_additional_information(request, package_uuid):
scen = request.POST.get("scenario")
scenario = Scenario.objects.get(package=p, url=scen)
if request.POST.get("adInfoTypes[]"):
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
attach_obj = url_parser.get_object()
@ -1614,33 +1564,6 @@ def create_package_additional_information(request, package_uuid):
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}")
except ValueError:
@ -1844,29 +1767,6 @@ 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:
@ -2178,10 +2078,6 @@ 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 = None
if e.multistep and e.multistep.strip() == "true":
multi_step = True
new_e = Edge.create(
pathway=pw,
start_nodes=educts,
@ -2189,7 +2085,6 @@ 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

View File

@ -1703,7 +1703,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 description.strip() != "":
if description is not None and name.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r.multi_step = multi_step
@ -2200,7 +2200,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
depth_map = {}
depth_map[0] = list()
processed = set()
for n in self.nodes:
num_parents = in_count[str(n.uuid)]
@ -2223,11 +2222,9 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
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:
@ -2466,8 +2463,6 @@ 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
@ -2497,7 +2492,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=kwargs.get("multi_step", False),
multi_step=False,
)
e.edge_label = r
@ -4505,22 +4500,18 @@ class AdditionalInformation(models.Model):
return f"{self.scenario.url}/additional-information/{self.uuid}"
@staticmethod
def from_dict(ai_type: str, ai_data: Dict[str, Any]):
def get(self) -> "EnviPyModel":
from envipy_additional_information import registry
MAPPING = {c.__name__: c for c in registry.list_models().values()}
try:
inst = MAPPING[ai_type](**ai_data)
inst = MAPPING[self.type](**self.data)
except Exception as e:
print(f"Error loading {ai_type}: {e}")
print(f"Error loading {self.type}: {e}")
raise e
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:

View File

@ -3113,21 +3113,12 @@ 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")
# 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)
return render(request, "collections/joblog.html", context)
elif request.method == "POST":
job_name = request.POST.get("job-name")

View File

@ -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) + " days<br>"
tempContent += "<b>DT50 predicted via Pepper:</b> " + s["mean"].toFixed(2) + "<br>"
}
}
}

View File

@ -1,102 +0,0 @@
{# 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>

View File

@ -1,13 +0,0 @@
{% 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 %}

View File

@ -37,11 +37,7 @@
perPage: {{ per_page|default:50 }}
})"
>
{% 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) ===== #}

View File

@ -125,7 +125,6 @@ from envipy_additional_information.parsers import (
LiquidMatrixSourceParser,
OxygenUptakeRateParser,
InitiatingOrganismParser,
PFASConfidenceParser,
)
logger = logging.getLogger(__name__)
@ -142,7 +141,7 @@ def get_parameter(request, paramname):
res = request.POST.get(paramname)
if res is not None and res.strip() != "":
return res
raise ValueError("Not all parameters are set!")
return ValueError("Not all parameters are set!")
def get_parameter_or_empty_string(request, paramname):
@ -674,8 +673,7 @@ 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!")