This commit is contained in:
Tim Lorsbach
2025-11-12 10:27:17 +01:00
10 changed files with 137 additions and 276 deletions

View File

@ -142,6 +142,11 @@ urlpatterns = [
v.package_pathway, v.package_pathway,
name="package pathway detail", name="package pathway detail",
), ),
re_path(
rf"^package/(?P<package_uuid>{UUID})/predict$",
v.package_predict_pathway,
name="package predict pathway",
),
# Pathway Nodes # Pathway Nodes
re_path( re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node$", rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node$",

View File

@ -374,6 +374,22 @@ def predict_pathway(request):
return render(request, "predict_pathway.html", context) return render(request, "predict_pathway.html", context)
@package_permission_required()
def package_predict_pathway(request, package_uuid):
"""Package-specific predict pathway view."""
if request.method != "GET":
return HttpResponseNotAllowed(["GET"])
current_user = _anonymous_or_real(request)
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Predict Pathway"
context["meta"]["current_package"] = current_package
return render(request, "predict_pathway.html", context)
def packages(request): def packages(request):
current_user = _anonymous_or_real(request) current_user = _anonymous_or_real(request)

View File

@ -1,6 +1,8 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a href="{{ meta.server_url }}/predict"> <a
href="{% if meta.current_package %}{{ meta.current_package.url }}/predict{% else %}{{ meta.server_url }}/predict{% endif %}"
>
<span class="glyphicon glyphicon-plus"></span> New Pathway</a <span class="glyphicon glyphicon-plus"></span> New Pathway</a
> >
</li> </li>

View File

@ -178,6 +178,23 @@
} }
}); });
// Open search modal function
function openSearchModal() {
const searchModal = document.getElementById("search_modal");
if (searchModal) {
searchModal.showModal();
}
}
// Click handler for search badge
const searchTrigger = document.getElementById("search-trigger");
if (searchTrigger) {
searchTrigger.addEventListener("click", function (event) {
event.preventDefault();
openSearchModal();
});
}
// Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux) // Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux)
document.addEventListener("keydown", function (event) { document.addEventListener("keydown", function (event) {
// Check if user is typing in an input field // Check if user is typing in an input field
@ -198,7 +215,7 @@
if (isCorrectModifier && event.key === "k") { if (isCorrectModifier && event.key === "k") {
event.preventDefault(); event.preventDefault();
search_modal.showModal(); openSearchModal();
} }
}); });
</script> </script>

View File

@ -4,9 +4,8 @@
{% if not public_mode %} {% if not public_mode %}
<nav> <nav>
<h6 class="footer-title">Services</h6> <h6 class="footer-title">Services</h6>
<a class="link link-hover" href="/">Predict</a> <a class="link link-hover" href="/predict">Predict</a>
<a class="link link-hover" href="/search">Search</a> <a class="link link-hover" href="/package">Packages</a>
<a class="link link-hover" href="/package">Browse</a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="link link-hover" href="/model">Your Collections</a> <a class="link link-hover" href="/model">Your Collections</a>
{% endif %} {% endif %}
@ -36,7 +35,10 @@
<footer class="footer border-t-2 border-neutral-300 px-10 py-4"> <footer class="footer border-t-2 border-neutral-300 px-10 py-4">
<div class="flex w-full flex-row items-start justify-between"> <div class="flex w-full flex-row items-start justify-between">
<aside class="grid-flow-col items-center"> <aside class="grid-flow-col items-center">
<svg class="fill-neutral-content m-2 h-14 shrink-0" viewbox="0 0 65 65"> <svg
class="fill-neutral-content m-2 h-14 flex-shrink-0"
viewbox="0 0 65 65"
>
<use <use
href="{% static "/images/logo-square.svg" %}#ep-logo-square" href="{% static "/images/logo-square.svg" %}#ep-logo-square"
></use> ></use>

View File

@ -26,6 +26,9 @@
tabindex="-1" tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm" class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm"
> >
<li>
<a href="{{ meta.server_url }}/Package" id="packageLink">Package</a>
</li>
<li> <li>
<a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a> <a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a>
</li> </li>
@ -57,7 +60,7 @@
<div class="navbar-end"> <div class="navbar-end">
{% if not public_mode %} {% if not public_mode %}
<a href="/search" role="button"> <a id="search-trigger" role="button" class="cursor-pointer">
<div <div
class="badge badge-dash bg-base-200 text-base-content/50 m-1 flex items-center space-x-1 p-2" class="badge badge-dash bg-base-200 text-base-content/50 m-1 flex items-center space-x-1 p-2"
> >

View File

@ -26,44 +26,56 @@
class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out" class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out"
> >
<div class="card-body"> <div class="card-body">
<!-- Input Mode Toggle - Fixed position outside fieldset -->
<div class="my-4 ml-8 flex h-fit flex-row items-center justify-start"> <div class="my-4 ml-8 flex h-fit flex-row items-center justify-start">
<div class="flex items-center gap-2"> <div class="flex items-center gap-1">
<!-- <span class="text-sm text-neutral-500">Input Mode:</span> --> <label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
<label class="toggle text-base-content toggle-md">
<input type="checkbox" /> <input type="checkbox" />
<svg <span class="swap-on flex items-center gap-1">
aria-label="smiles mode" <div
xmlns="http://www.w3.org/2000/svg" class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
viewBox="0 0 20 20"
class="size-5"
>
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2"
fill="currentColor"
stroke="none"
> >
<path <svg
fill-rule="evenodd" aria-label="smiles mode"
d="M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z" xmlns="http://www.w3.org/2000/svg"
clip-rule="evenodd" viewBox="0 0 20 20"
/> class="size-5"
</g> >
</svg> <g
<svg stroke-linejoin="round"
aria-label="draw mode" stroke-linecap="round"
xmlns="http://www.w3.org/2000/svg" stroke-width="2"
viewBox="0 0 20 20" fill="currentColor"
fill="currentColor" stroke="none"
stroke="none" >
class="size-5" <path
> fill-rule="evenodd"
<path d="M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z"
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" clip-rule="evenodd"
/> />
</svg> </g>
</svg>
</div>
<span class="ext-xs">SMILES</span>
</span>
<span class="swap-off flex items-center gap-1">
<div
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
>
<svg
aria-label="draw mode"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
stroke="none"
class="size-5"
>
<path
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
/>
</svg>
</div>
<span class="text-base/50 text-xs">Draw</span>
</span>
</label> </label>
</div> </div>
</div> </div>
@ -128,7 +140,9 @@
> >
Predict! Predict!
</button> </button>
<a class="label mx-auto mt-1 w-full" href="#">Advanced</a> <div class="mt-1 flex w-full justify-end">
<a class="label justify-end" href="/predict">Advanced</a>
</div>
</div> </div>
<input <input
type="hidden" type="hidden"

View File

@ -2,7 +2,7 @@
<dialog id="search_modal" class="modal @max-sm:modal-top justify-center"> <dialog id="search_modal" class="modal @max-sm:modal-top justify-center">
<div class="modal-box h-full w-lvw p-1 sm:h-8/12 sm:w-[85vw] sm:max-w-5xl"> <div class="modal-box h-full w-lvw p-1 sm:h-8/12 sm:w-[85vw] sm:max-w-5xl">
<!-- Search Input and Mode Selector --> <!-- Search Input and Mode Selector -->
<div class="form-control mb-4 w-full flex-shrink-0"> <div class="form-control mb-4 w-full shrink-0">
<div class="join m-0 w-full items-center p-3"> <div class="join m-0 w-full items-center p-3">
<label class="input join-item input-ghost grow"> <label class="input join-item input-ghost grow">
<svg <svg
@ -37,7 +37,7 @@
tabindex="0" tabindex="0"
id="modal_mode_button" id="modal_mode_button"
popovertarget="search_dropdown_menu" popovertarget="search_dropdown_menu"
style="anchor-name:--anchor-1" style="anchor-name:--1"
class="btn join-item btn-ghost" class="btn join-item btn-ghost"
> >
Text Text
@ -57,10 +57,10 @@
</button> </button>
<ul <ul
tabindex="0" tabindex="0"
class="dropdown dropdown-end menu bg-base-200 rounded-box z-[100] w-64 p-2 shadow-lg" class="dropdown dropdown-end menu bg-base-200 rounded-box w-64 p-2 shadow-lg"
popover popover
id="search_dropdown_menu" id="search_dropdown_menu"
style="position-anchor:--anchor-1" style="position-anchor:--anchor-2"
> >
<li class="menu-title">Text</li> <li class="menu-title">Text</li>
<li> <li>
@ -140,18 +140,18 @@
</div> </div>
<!-- Package Selector with Pills --> <!-- Package Selector with Pills -->
<div class="form-control mb-4 flex-shrink-0"> <div class="form-control mb-4 shrink-0">
<!-- Pills Container --> <!-- Pills Container -->
<div <div
id="modal_package_pills_container" id="modal_package_pills_container"
class="border-base-300 m-3 flex min-h-[3rem] flex-wrap items-center gap-2 rounded-lg border-2 border-dashed p-3" class="border-base-300 m-3 flex min-h-12 flex-wrap items-center gap-2 rounded-lg border-2 border-dashed p-3"
> >
<!-- Pills will be added here dynamically --> <!-- Pills will be added here dynamically -->
</div> </div>
<!-- Package Dropdown Menu --> <!-- Package Dropdown Menu -->
<ul <ul
class="dropdown dropdown-end menu bg-base-200 rounded-box z-[100] max-h-96 w-80 overflow-y-auto p-2 shadow-lg" class="dropdown dropdown-center menu bg-base-200 rounded-box max-h-96 w-80 overflow-y-auto p-2 shadow-lg"
popover popover
id="package_dropdown_menu" id="package_dropdown_menu"
style="position-anchor:--anchor-packages" style="position-anchor:--anchor-packages"
@ -241,7 +241,7 @@
</div> </div>
<!-- Loading Indicator --> <!-- Loading Indicator -->
<div id="search_loading" class="hidden flex-shrink-0 justify-center py-8"> <div id="search_loading" class="hidden shrink-0 justify-center py-8">
<span class="loading loading-spinner loading-lg text-primary"></span> <span class="loading loading-spinner loading-lg text-primary"></span>
</div> </div>
@ -490,6 +490,26 @@
observer.observe(modal, { attributes: true }); observer.observe(modal, { attributes: true });
// Close modal when clicking outside (on the backdrop)
// According to DaisyUI docs: https://daisyui.com/components/modal/
// The backdrop form with method="dialog" should handle closing automatically when its button is clicked.
// We also handle clicks directly on the dialog element (backdrop area) or the backdrop form.
modal.addEventListener("click", function (event) {
const backdrop = modal.querySelector(".modal-backdrop");
const modalBox = modal.querySelector(".modal-box");
// Close if clicking directly on the dialog element (backdrop area)
// or on the backdrop form (but ensure we're not clicking on modal-box content)
if (
event.target === modal ||
(backdrop &&
(event.target === backdrop || backdrop.contains(event.target)) &&
!modalBox.contains(event.target))
) {
modal.close();
}
});
// Clear results when modal closes // Clear results when modal closes
modal.addEventListener("close", function () { modal.addEventListener("close", function () {
resultsDiv.innerHTML = ""; resultsDiv.innerHTML = "";

View File

@ -2,7 +2,13 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="mx-auto w-full p-8"> <div class="mx-auto w-full p-8">
<h1 class="h1 mb-4 text-3xl font-bold">Predict a Pathway</h1> <h1 class="h1 mb-4 text-3xl font-bold">
Predict a Pathway
<span class="text-base-content/50 text-xs"
>in <strong>{{ meta.current_package.name|safe }}</strong>
</span>
</h1>
<form <form
id="predict_form" id="predict_form"

View File

@ -1,224 +0,0 @@
{% extends "framework.html" %}
{% load static %}
{% block content %}
<div id="searchContent">
<div id="packSelector">
<label>Select Packages</label><br />
<select
id="selPackages"
name="selPackages"
data-actions-box="true"
class="selPackages"
multiple
data-width="100%"
>
{% if unreviewed_objects %}
<option disabled>Reviewed Packages</option>
{% endif %}
{% for obj in reviewed_objects %}
<option value="{{ obj.url }}" selected>{{ obj.name|safe }}</option>
{% endfor %}
{% if unreviewed_objects %}
<option disabled>Unreviewed Packages</option>
{% endif %}
{% for obj in unreviewed_objects %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endfor %}
</select>
</div>
<p></p>
<div>
<label>Search Term</label><br />
<div class="input-group" id="index-form-bar">
<input
type="text"
class="form-control"
id="searchbar"
placeholder="Benfuracarb"
/>
<div class="input-group-btn">
<button
type="button"
class="btn btn-default dropdown-toggle"
data-toggle="dropdown"
id="mode-button"
aria-haspopup="true"
aria-expanded="false"
>
Text <span class="caret"></span>
</button>
<ul class="dropdown-menu">
<li class="dropdown-header">Text</li>
<li><a id="dropdown-predict-text-text">Text</a></li>
<li class="dropdown-header">SMILES</li>
<li>
<a id="dropdown-search-smiles-default" data-toggle="tooltip"
>Default</a
>
</li>
<li><a id="dropdown-search-smiles-canonical">Canonical</a></li>
<li><a id="dropdown-search-smiles-exact">Exact</a></li>
<li class="dropdown-header">InChI</li>
<li><a id="dropdown-search-inchi-inchikey">InChIKey</a></li>
</ul>
<button
class="btn"
style="background-color:#222222;color:#9d9d9d"
type="button"
id="search-button"
>
Go!
</button>
</div>
</div>
<p></p>
<div id="results"></div>
<p></p>
<div id="loading"></div>
</div>
</div>
{# prettier-ignore-start #}
<script>
function modeDropdownClicked() {
var suffix = ' <span class="caret"></span>';
var dropdownVal = $(this).text();
$('#mode-button').html(dropdownVal + suffix);
}
function handleSearchResponse(id, data) {
content = `
<div class='panel-group' id='search-accordion'>
<div class='panel panel-default'>
<div class='panel-heading' id='headingPanel' style='font-size:2rem;height: 46px'>
Results
</div>
<div id='descDiv'></div>
</div>`;
function makeContent(objs) {
links = "";
for (idx in objs) {
obj = objs[idx];
links += `<a class='list-group-item' href='${obj.url}'>${obj.name}</a>`
}
return links;
}
allEmpty = true;
for (key in data) {
if (key === 'searchterm') {
continue;
}
if (data[key].length < 1) {
continue;
}
allEmpty = false;
content += `
<div class='panel panel-default panel-heading list-group-item' style='background-color:silver'>
<h4 class='panel-title'>
<a id='${key}_link' data-toggle='collapse' data-parent='#search-accordion' href='#${key}_panel'>
${key}
</a>
</h4>
</div>
<div id='${key}_panel' class='panel-collapse collapse in'>
<div class='panel-body list-group-item'>
${makeContent(data[key])}
</div>
</div>
`;
}
if (allEmpty) {
$('#' + id).append('<div class="alert alert-danger" role="alert"><p>' + "No results..." + '</p></div>');
} else {
$('#' + id).append(content);
}
}
function search(e) {
e.preventDefault();
query = $("#searchbar").val()
if (!query) {
// Nothing to search...
console.log("Search phrase empty, won't do search")
return;
}
var selPacks = [];
$("#selPackages :selected").each(function () {
var id = this.value;
selPacks.push(id);
});
if (selPacks.length < 1) {
console.log("No package selected, won't do search")
return;
}
var mode = $('#mode-button').text().trim().toLowerCase();
var par = {};
par['packages'] = selPacks;
par['search'] = query;
par['mode'] = mode;
console.log(par);
var queryString = $.param(par, true);
makeLoadingGif("#loading", "{% static '/images/wait.gif' %}");
$("#results").empty();
$.getJSON("{{ SERVER_BASE }}/search?" + queryString, function (result) {
handleSearchResponse("results", result);
$("#loading").empty();
}).fail(function (d) {
$("#loading").empty();
console.log(d.responseText);
handleError(JSON.parse(d.responseText));
});
}
$(function () {
tooltips = {
'dropdown-predict-text-text': 'The inserted pattern will be searched on all enviPath object names and descriptions',
'dropdown-search-smiles-default': 'Search by SMILES, stereochemistry and charge are ignored',
'dropdown-search-smiles-canonical': 'Search by SMILES, stereochemistry is ignored but charge is preserved',
'dropdown-search-smiles-exact': 'Search by SMILES, exact match for stereochemistry and charge',
'dropdown-search-inchi-inchikey': 'Search by InChIKey',
}
Object.keys(tooltips).forEach(key => {
$('#' + key).tooltip({
placement: "top",
title: tooltips[key]
});
$('#' + key).on('click', modeDropdownClicked);
});
$("#selPackages").selectpicker();
$("#search-button").on("click", search);
$("#searchbar").on("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
search(e);
}
});
});
{% if search_result %}
$('#searchbar').val('{{ search_result.searchterm }}')
handleSearchResponse("results", {{ search_result|safe }});
{% endif %}
</script>
{# prettier-ignore-end #}
{% endblock content %}