7 Commits

Author SHA1 Message Date
eb6c5ade29 fix: open and close search modal
Modal now opens on badge click.
Modal now closes on random click around
2025-11-12 15:37:35 +13:00
305fdc41fb [Fix] Replace datetime.now() with Djangos timezone.now() to get rid of NaiveTimestamp warning (#191)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#191
2025-11-12 11:04:00 +13:00
9deca8867e [Feature] Possibility to Retrain Models (#190)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#190
2025-11-12 10:28:35 +13:00
df6056fb86 [Enhancement] Refactor of license django model (#187)
Fixes #119

Licenses are now created in the bootstrap management command. To only create the licenses use the command line argument `-ol` or `--only-licenses`.
These licenses are then fetched by there `cc_string` when adding a license to a package.

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Co-authored-by: jebus <lorsbach@envipath.com>
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#187
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-11-12 08:20:43 +13:00
c1553d9cd4 [Feature] Add Predict Pathway Page in Modern UI (#188)
## Major Changes
- Predict Pathway is now a separate view as it is meant as adavanced prediction interface

## Current status
![image.png](/attachments/c5bc3f5c-cf30-4b5f-acb3-a70117a96dae)

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#188
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 02:38:57 +13:00
2b79adc2f7 [Feature] Implement Search modal in Modern UI (#185)
Implementing a search modal (stretching the level of dynamic that is possible without going to frameworks).

## Major Change
- Search needs packages and is available everywhere now; so had to add reviewed and user packages to global context.

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#185
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 01:52:41 +13:00
ddf1fd3515 Frontpage update (#179)
This PR introduces an overhaul for the front page and login features while keeping the rest of the application intact.

## Major Changes

- TailwindCSS + DaisyUI Integration: Add  modern CSS framework for component-based utility styling
- Build System: Added pnpm for CSS building; can be extended for updated frontend builds in the future
- Navbar + Footer: Redesigned and includable; old version retained for unstyled elements
- Optimized Assets: Added minified and CSS-stylable logos

## New Features

- Static Pages: Added comprehensive mockups of static pages (legal, privacy policy, terms of use, contact, etc.). **Note:** These have to be fixed before a public release, as their content is largely unreviewed and incorrect. Probably best to do in a separate PR that only contains updates to these.
- Discourse API: Implement minimal features based on RestAPI for controllable results.

## Current bugs
- [x] The static pages include the default navbar and footer on the login page. This will likely not work, as users need to access it before logging in; no good workaround so far (problem with Django templating system).
- [ ] The front page predict link is currently non-functional; the redesigned page is almost ready but better done in a separate PR as it also touches Django code.
- [x] Visual bug with the news cards. Still intend to fix for this PR

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#179
Reviewed-by: jebus <lorsbach@envipath.com>
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 01:09:39 +13:00
26 changed files with 2167 additions and 1077 deletions

View File

@ -105,7 +105,7 @@ jobs:
until pg_isready -h postgres -U postgres; do sleep 2; done
# until redis-cli -h redis ping; do sleep 2; done
- name: Run Django migrations
- name: Run Django Migrations
run: |
source .venv/bin/activate
python manage.py migrate --noinput

View File

@ -87,6 +87,7 @@ TEMPLATES = [
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"epdb.context_processors.package_context",
],
},
},

View File

@ -21,6 +21,7 @@ from .models import (
ExternalDatabase,
ExternalIdentifier,
JobLog,
License,
)
@ -62,6 +63,10 @@ class EnviFormerAdmin(EPAdmin):
pass
class LicenseAdmin(admin.ModelAdmin):
list_display = ["cc_string", "link", "image_link"]
class CompoundAdmin(EPAdmin):
pass
@ -118,6 +123,7 @@ admin.site.register(JobLog, JobLogAdmin)
admin.site.register(Package, PackageAdmin)
admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin)
admin.site.register(EnviFormer, EnviFormerAdmin)
admin.site.register(License, LicenseAdmin)
admin.site.register(Compound, CompoundAdmin)
admin.site.register(CompoundStructure, CompoundStructureAdmin)
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)

View File

@ -0,0 +1,32 @@
"""
Context processors for enviPy application.
Context processors automatically make variables available to all templates.
"""
from .logic import PackageManager
from .models import Package
def package_context(request):
"""
Provides package data for the search modal which is included globally
in framework_modern.html.
Returns:
dict: Context dictionary with reviewed and unreviewed packages
"""
current_user = request.user
reviewed_package_qs = PackageManager.get_reviewed_packages()
unreviewed_package_qs = Package.objects.none()
# Only get user-specific packages if user is authenticated
if current_user.is_authenticated:
unreviewed_package_qs = PackageManager.get_all_readable_packages(current_user)
return {
"reviewed_packages": reviewed_package_qs,
"unreviewed_packages": unreviewed_package_qs,
}

View File

@ -12,10 +12,16 @@ from epdb.models import (
Permission,
User,
ExternalDatabase,
License,
)
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"-ol", "--only-licenses", action="store_true", help="Only create licenses."
)
def create_users(self):
# Anonymous User
if not User.objects.filter(email="anon@envipath.com").exists():
@ -83,6 +89,17 @@ class Command(BaseCommand):
return anon, admin, g, user0
def create_licenses(self):
"""Create the six default licenses supported by enviPath"""
cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"]
for cc_string in cc_strings:
if not License.objects.filter(cc_string=cc_string).exists():
new_license = License()
new_license.cc_string = cc_string
new_license.link = f"https://creativecommons.org/licenses/{cc_string}/4.0/"
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
new_license.save()
def import_package(self, data, owner):
return PackageManager.import_legacy_package(
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
@ -157,6 +174,10 @@ class Command(BaseCommand):
@transaction.atomic
def handle(self, *args, **options):
# Create licenses
self.create_licenses()
if options.get("only_licenses", False):
return
# Create users
anon, admin, g, user0 = self.create_users()

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.7 on 2025-11-11 14:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0009_joblog"),
]
operations = [
migrations.AddField(
model_name="license",
name="cc_string",
field=models.TextField(default="by-nc-sa", verbose_name="CC string"),
preserve_default=False,
),
]

View File

@ -0,0 +1,59 @@
# Generated by Django 5.2.7 on 2025-11-11 14:13
import re
from django.contrib.postgres.aggregates import ArrayAgg
from django.db import migrations
from django.db.models import Min
def set_cc(apps, schema_editor):
License = apps.get_model("epdb", "License")
# For all existing licenses extract cc_string from link
for license in License.objects.all():
pattern = r"/licenses/([^/]+)/4\.0"
match = re.search(pattern, license.link)
if match:
license.cc_string = match.group(1)
license.save()
else:
raise ValueError(f"Could not find license for {license.link}")
# Ensure we have all licenses
cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"]
for cc_string in cc_strings:
if not License.objects.filter(cc_string=cc_string).exists():
new_license = License()
new_license.cc_string = cc_string
new_license.link = f"https://creativecommons.org/licenses/{cc_string}/4.0/"
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
new_license.save()
# As we might have existing Licenses representing the same License,
# get min pk and all pks as a list
license_lookup_qs = License.objects.values("cc_string").annotate(
lowest_pk=Min("id"), all_pks=ArrayAgg("id", order_by=("id",))
)
license_lookup = {
row["cc_string"]: (row["lowest_pk"], row["all_pks"]) for row in license_lookup_qs
}
Packages = apps.get_model("epdb", "Package")
for k, v in license_lookup.items():
# Set min pk to all packages pointing to any of the duplicates
Packages.objects.filter(pk__in=v[1]).update(license_id=v[0])
# remove the min pk from "other" pks as we use them for deletion
v[1].remove(v[0])
# Delete redundant License objects
License.objects.filter(pk__in=v[1]).delete()
class Migration(migrations.Migration):
dependencies = [
("epdb", "0010_license_cc_string"),
]
operations = [migrations.RunPython(set_cc)]

View File

@ -655,6 +655,7 @@ class ScenarioMixin(models.Model):
class License(models.Model):
cc_string = models.TextField(blank=False, null=False, verbose_name="CC string")
link = models.URLField(blank=False, null=False, verbose_name="link")
image_link = models.URLField(blank=False, null=False, verbose_name="Image link")

View File

@ -1,15 +1,15 @@
import csv
import io
import logging
from datetime import datetime
from typing import Any, Callable, List, Optional
from uuid import uuid4
from celery import shared_task
from celery.utils.functional import LRUCache
from django.utils import timezone
from epdb.logic import SPathway
from epdb.models import EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User, Edge
from epdb.models import Edge, EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User
logger = logging.getLogger(__name__)
ML_CACHE = LRUCache(3) # Cache the three most recent ML models to reduce load times.
@ -29,7 +29,7 @@ def dispatch_eager(user: "User", job: Callable, *args, **kwargs):
log.task_id = uuid4()
log.job_name = job.__name__
log.status = "SUCCESS"
log.done_at = datetime.now()
log.done_at = timezone.now()
log.task_result = str(x) if x else None
log.save()

View File

@ -48,6 +48,7 @@ urlpatterns = [
re_path(r"^user$", v.users, name="users"),
re_path(r"^group$", v.groups, name="groups"),
re_path(r"^search$", v.search, name="search"),
re_path(r"^predict$", v.predict_pathway, name="predict_pathway"),
# User Detail
re_path(rf"^user/(?P<user_uuid>{UUID})", v.user, name="user"),
# Group Detail

View File

@ -362,6 +362,18 @@ def index(request):
return render(request, "index/index.html", context)
def predict_pathway(request):
"""Top-level predict pathway view using user's default package."""
if request.method != "GET":
return HttpResponseNotAllowed(["GET"])
context = get_base_context(request)
context["title"] = "enviPath - Predict Pathway"
context["meta"]["current_package"] = context["meta"]["user"].default_package
return render(request, "predict_pathway.html", context)
def packages(request):
current_user = _anonymous_or_real(request)
@ -682,7 +694,6 @@ def search(request):
if request.method == "GET":
package_urls = request.GET.getlist("packages")
searchterm = request.GET.get("search", "").strip()
mode = request.GET.get("mode")
# add HTTP_ACCEPT check to differentiate between index and ajax call
@ -783,6 +794,7 @@ def package_models(request, package_uuid):
elif request.method == "POST":
log_post_params(request)
name = request.POST.get("model-name")
description = request.POST.get("model-description")
@ -943,6 +955,12 @@ def package_model(request, package_uuid, model_uuid):
]
dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids)
return redirect(current_model.url)
elif hidden == "retrain":
from .tasks import dispatch, retrain
dispatch(current_user, retrain, current_model.pk)
return redirect(current_model.url)
else:
return HttpResponseBadRequest()
@ -1072,9 +1090,7 @@ def package(request, package_uuid):
write = request.POST.get("write") == "on"
owner = request.POST.get("owner") == "on"
license = request.POST.get("license")
license_link = request.POST.get("license-link")
license_image_link = request.POST.get("license-image-link")
cc_string = request.POST.get("license")
if new_package_name:
current_package.name = new_package_name
@ -1102,24 +1118,15 @@ def package(request, package_uuid):
PackageManager.update_permissions(current_user, current_package, grantee, max_perm)
return redirect(current_package.url)
elif license is not None:
if license == "no-license":
if current_package.license is not None:
current_package.license.delete()
elif cc_string is not None:
cc_string = cc_string.strip()
if cc_string == "no-license": # Reset the package's license
current_package.license = None
current_package.save()
return redirect(current_package.url)
else:
if current_package.license is not None:
current_package.license.delete()
license = License()
license.link = license_link
license.image_link = license_image_link
license.save()
current_package.license = license
else: # Get the license and assign it to the package
current_package.license = License.objects.get(cc_string=cc_string)
current_package.save()
return redirect(current_package.url)
@ -1174,7 +1181,7 @@ def package_compounds(request, package_uuid):
elif request.method == "POST":
compound_name = request.POST.get("compound-name")
compound_smiles = request.POST.get("compound-smiles").strip()
compound_smiles = request.POST.get("compound-smiles")
compound_description = request.POST.get("compound-description")
c = Compound.create(current_package, compound_smiles, compound_name, compound_description)
@ -1301,7 +1308,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
elif request.method == "POST":
structure_name = request.POST.get("structure-name")
structure_smiles = request.POST.get("structure-smiles").strip()
structure_smiles = request.POST.get("structure-smiles")
structure_description = request.POST.get("structure-description")
try:
@ -1483,11 +1490,11 @@ def package_rules(request, package_uuid):
# Obtain parameters as required by rule type
if rule_type == "SimpleAmbitRule":
params["smirks"] = request.POST.get("rule-smirks").strip()
params["smirks"] = request.POST.get("rule-smirks")
params["reactant_filter_smarts"] = request.POST.get("rule-reactant-smarts")
params["product_filter_smarts"] = request.POST.get("rule-product-smarts")
elif rule_type == "SimpleRDKitRule":
params["reaction_smarts"] = request.POST.get("rule-reaction-smarts").strip()
params["reaction_smarts"] = request.POST.get("rule-reaction-smarts")
elif rule_type == "ParallelRule":
pass
elif rule_type == "SequentialRule":
@ -1684,8 +1691,7 @@ def package_reactions(request, package_uuid):
elif request.method == "POST":
reaction_name = request.POST.get("reaction-name")
reaction_description = request.POST.get("reaction-description")
reactions_smirks = request.POST.get("reaction-smirks").strip()
reactions_smirks = request.POST.get("reaction-smirks")
educts = reactions_smirks.split(">>")[0].split(".")
products = reactions_smirks.split(">>")[1].split(".")
@ -2259,6 +2265,7 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
elif request.method == "POST":
log_post_params(request)
edge_name = request.POST.get("edge-name")
edge_description = request.POST.get("edge-description")

View File

@ -30,6 +30,7 @@
/* Import DaisyUI plugin */
@plugin "daisyui" {
logs: true;
exclude: rootscrollgutter;
}
@import "./daisyui-theme.css";

View File

@ -1,6 +1,6 @@
{% if meta.can_edit %}
<li>
<a role="button" data-toggle="modal" data-target="#predict_modal">
<a href="{{ meta.server_url }}/predict">
<span class="glyphicon glyphicon-plus"></span> New Pathway</a>
</li>
{% endif %}
{% endif %}

View File

@ -1,11 +1,15 @@
<!DOCTYPE html>
<!doctype html>
<html data-theme="envipath">
{% load static %}
<head>
{% load static %}
<head>
<title>{{ title }}</title>
<meta name="csrf-token" content="{{ csrf_token }}">
<meta name="csrf-token" content="{{ csrf_token }}" />
{# Favicon #}
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
<link
rel="shortcut icon"
type="image/png"
href="{% static 'images/favicon.ico' %}"
/>
{# Tailwind CSS disabled for legacy Bootstrap framework #}
{# Pages using this framework will be migrated to framework_modern.html incrementally #}
@ -13,36 +17,45 @@
{# Legacy Bootstrap 3.3.7 - scoped to .legacy-bootstrap #}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<link
rel="stylesheet"
href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css"
/>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
<script src="https://ajax.aspnetcdn.com/ajax/jquery.templates/beta1/jquery.tmpl.js"></script>
<link href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.7.3/css/bootstrap-select.min.css">
<link
href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.7.3/css/bootstrap-select.min.css"
/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.7.3/js/bootstrap-select.min.js"></script>
<script src="https://community.envipath.org/javascripts/embed-topics.js"></script>
<!-- CDN END -->
{# Bootstrap compatibility styles #}
<style>
/* Ensure proper viewport behavior */
html, body {
height: 100%; /* ensure body fills viewport */
overflow-x: hidden; /* prevent horizontal scroll */
}
/* Ensure proper viewport behavior */
html,
body {
height: 100%; /* ensure body fills viewport */
overflow-x: hidden; /* prevent horizontal scroll */
}
</style>
<script>
const csrftoken = document.querySelector('[name=csrf-token]').content;
const csrftoken = document.querySelector("[name=csrf-token]").content;
// Setup CSRF header for all jQuery AJAX requests
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
// Setup CSRF header for all jQuery AJAX requests
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
},
});
</script>
<!-- {# C3 CSS #}-->
@ -50,40 +63,44 @@
<!-- {# EP CSS #}-->
<!-- <link id="css-pps_white_general" href="{% static 'css/epp.css' %}" rel="stylesheet" type="text/css"/>-->
{# General EP JS #}
<script src="{% static 'js/pps.js' %}"></script>
{# Modal Steps for Stepwise Modal Wizards #}
<script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script>
{% if not debug %}
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function () {
var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '{{ meta.site_id }}']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->
<!-- Matomo -->
<script>
var _paq = (window._paq = window._paq || []);
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(["trackPageView"]);
_paq.push(["enableLinkTracking"]);
(function () {
var u = "//matomo.envipath.com/";
_paq.push(["setTrackerUrl", u + "matomo.php"]);
_paq.push(["setSiteId", "{{ meta.site_id }}"]);
var d = document,
g = d.createElement("script"),
s = d.getElementsByTagName("script")[0];
g.async = true;
g.src = u + "matomo.js";
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->
{% endif %}
</head>
<body>
<!-- Legacy Bootstrap navbar - isolated from Tailwind -->
<div class="legacy-bootstrap">
<nav class="navbar navbar-default navbar-inverse" style="border-radius:0px;" role="navigation">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header navbar-header-framework">
</head>
<body>
<!-- Legacy Bootstrap navbar - isolated from Tailwind -->
<div class="legacy-bootstrap">
<nav
class="navbar navbar-default navbar-inverse"
style="border-radius:0px;"
role="navigation"
>
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header navbar-header-framework">
<!-- <button type="button" class="navbar-toggle navbar-toggle-framework" data-toggle="collapse"-->
<!-- data-target="#navbarCollapse">-->
<!-- <span class="sr-only">Toggle navigation</span>-->
@ -91,204 +108,315 @@
<!-- <span class="icon-bar"></span>-->
<!-- <span class="icon-bar"></span>-->
<!-- </button>-->
<a id="pictureLink" href="{{ meta.server_url }}" class="navbar-brand">
<img id="image-logo-short-white.svg" src='{% static "/images/logo-short-white.svg" %}' width="100"
alt="enviPath">
<a
id="pictureLink"
href="{{ meta.server_url }}"
class="navbar-brand"
>
<img
id="image-logo-short-white.svg"
src="{% static "/images/logo-short-white.svg" %}"
width="100"
alt="enviPath"
/>
</a>
</div>
</div>
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse collapse-framework navbar-collapse-framework" id="navbarCollapse">
<!-- Collect the nav links, forms, and other content for toggling -->
<div
class="collapse navbar-collapse collapse-framework navbar-collapse-framework"
id="navbarCollapse"
>
<ul class="nav navbar-nav navbar-nav-framework">
<li>
<a href="{{ meta.server_url }}/predict"> Predict Pathway </a>
</li>
{# <li class="dropdown">#}
{# <a data-toggle="dropdown" class="dropdown-toggle" href="#">Predict Pathway<b class="caret"></b></a>#}
{# <ul role="menu" class="dropdown-menu">#}
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#predict_modal">#}
{# <i class=" glyphicon glyphicon-tag"></i> Predict Pathway#}
{# </a>#}
{# </li>#}
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#batch_predict_modal">#}
{# <i class=" glyphicon glyphicon-tags"></i> Batch Prediction#}
{# </a>#}
{# </li>#}
{# </ul>#}
{# </li>#}
<li>
<a href="{{ meta.server_url }}/package" id="packageLink"
>Package</a
>
</li>
<li>
<a href="{{ meta.server_url }}/search" id="searchLink"
>Search</a
>
</li>
<li>
<a href="{{ meta.server_url }}/model" id="modelLink"
>Modelling</a
>
</li>
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="#"
>Browse Data<b class="caret"></b
></a>
<ul role="menu" class="dropdown-menu">
<li>
<a href="{{ meta.server_url }}/pathway" id="pathwayLink"
>Pathway</a
>
</li>
<li>
<a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a>
</li>
<li>
<a href="{{ meta.server_url }}/compound" id="compoundLink"
>Compound</a
>
</li>
<li>
<a href="{{ meta.server_url }}/reaction" id="reactionLink"
>Reaction</a
>
</li>
<li>
<a
href="{{ meta.server_url }}/model"
id="relative-reasoningLink"
>Model</a
>
</li>
<li>
<a href="{{ meta.server_url }}/scenario" id="scenarioLink"
>Scenario</a
>
</li>
{# <li><a href="{{ meta.server_url }}/setting" id="settingLink">Setting</a></li>#}
{# <li><a href="{{ meta.server_url }}/user" id="userLink">User</a></li>#}
{# <li><a href="{{ meta.server_url }}/group" id="groupLink">Group</a></li>#}
</ul>
</li>
</ul>
<ul
class="nav navbar-nav navbar-right navbar-nav-framework navbar-right-framework"
>
<li>
<a href="https://community.envipath.org/" id="communityLink"
>Community</a
>
</li>
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="#"
>Info <b class="caret"></b
></a>
<ul role="menu" class="dropdown-menu">
<!--<li><a href="{{ meta.server_url }}/funding" id="fundingLink">Funding</a></li>-->
<li>
<a
href="https://community.envipath.org/t/envipath-license/109"
id="licenceLink"
>Licences</a
>
</li>
<li class="divider"></li>
<li>
<a
target="_blank"
href="https://wiki.envipath.org/"
id="wikiLink"
>Documentation Wiki</a
>
</li>
<li>
<a
href="#"
id="citeButton"
data-toggle="modal"
data-target="#citemodal"
>How to cite enviPath</a
>
</li>
<li class="divider"></li>
<li><a>Version: {{ meta.version }}</a></li>
</ul>
</li>
{% if meta.user.username == 'anonymous' %}
<li>
<a href="#" data-toggle="modal" data-target="#predict_modal">
Predict Pathway
</a>
<a
href="{% url 'login' %}"
id="loginButton"
style="margin-right:10px"
>Login</a
>
</li>
{# <li class="dropdown">#}
{# <a data-toggle="dropdown" class="dropdown-toggle" href="#">Predict Pathway<b class="caret"></b></a>#}
{# <ul role="menu" class="dropdown-menu">#}
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#predict_modal">#}
{# <i class=" glyphicon glyphicon-tag"></i> Predict Pathway#}
{# </a>#}
{# </li>#}
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#batch_predict_modal">#}
{# <i class=" glyphicon glyphicon-tags"></i> Batch Prediction#}
{# </a>#}
{# </li>#}
{# </ul>#}
{# </li>#}
<li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li>
<li><a href="{{ meta.server_url }}/search" id="searchLink">Search</a></li>
<li><a href="{{ meta.server_url }}/model" id="modelLink">Modelling</a></li>
{% else %}
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Browse Data<b class="caret"></b></a>
<ul role="menu" class="dropdown-menu">
<li><a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a></li>
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li>
<li><a href="{{ meta.server_url }}/compound" id="compoundLink">Compound</a></li>
<li><a href="{{ meta.server_url }}/reaction" id="reactionLink">Reaction</a></li>
<li><a href="{{ meta.server_url }}/model" id="relative-reasoningLink">Model</a></li>
<li><a href="{{ meta.server_url }}/scenario" id="scenarioLink">Scenario</a></li>
{# <li><a href="{{ meta.server_url }}/setting" id="settingLink">Setting</a></li>#}
{# <li><a href="{{ meta.server_url }}/user" id="userLink">User</a></li>#}
{# <li><a href="{{ meta.server_url }}/group" id="groupLink">Group</a></li>#}
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right navbar-nav-framework navbar-right-framework">
<li><a href="https://community.envipath.org/" id="communityLink">Community</a></li>
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Info <b class="caret"></b></a>
<ul role="menu" class="dropdown-menu">
<!--<li><a href="{{ meta.server_url }}/funding" id="fundingLink">Funding</a></li>-->
<li><a href="https://community.envipath.org/t/envipath-license/109" id="licenceLink">Licences</a></li>
<li class="divider"></li>
<li><a target="_blank" href="https://wiki.envipath.org/" id="wikiLink">Documentation Wiki</a>
</li>
<li><a href="#" id="citeButton" data-toggle="modal" data-target="#citemodal">How to cite
enviPath</a></li>
<li class="divider"></li>
<li><a>Version: {{ meta.version }}</a></li>
</ul>
</li>
{% if meta.user.username == 'anonymous' %}
<a
data-toggle="dropdown"
id="loggedInButton"
class="dropdown-toggle"
id="logedInButton"
href="#"
>
<div id="username">
{{ user.username }}<b class="caret"></b>
</div>
</a>
<ul role="menu" class="dropdown-menu">
<li>
<a href="{% url 'login' %}" id="loginButton" style="margin-right:10px">Login</a>
<a href="{{ meta.user.url }}" id="accountbutton"
>My Account</a
>
</li>
{% else %}
<li class="dropdown">
<a data-toggle="dropdown" id="loggedInButton" class="dropdown-toggle" id="logedInButton"
href="#">
<div id="username">
{{ user.username }}<b class="caret"></b>
</div>
</a>
<ul role="menu" class="dropdown-menu">
<li>
<a href="{{ meta.user.url }}" id="accountbutton">My Account</a>
</li>
<li class="divider"></li>
<form class="navbar-form navbar-left navbar-left-framework" role="logout"
action="{% url 'logout' %}" method="post">
{% csrf_token %}
<div class="form-group">
<input type="hidden" name="logout" value="true">
</div>
<button type="submit" class="btn btn-default">Logout</button>
</form>
</ul>
</li>
{% endif %}
<li class="divider"></li>
<form
class="navbar-form navbar-left navbar-left-framework"
role="logout"
action="{% url 'logout' %}"
method="post"
>
{% csrf_token %}
<div class="form-group">
<input type="hidden" name="logout" value="true" />
</div>
<button type="submit" class="btn btn-default">
Logout
</button>
</form>
</ul>
</li>
{% endif %}
</ul>
</div>
</div>
</nav>
</div>
</nav>
</div>
<!-- End legacy Bootstrap navbar -->
<!-- End legacy Bootstrap navbar -->
<div id="docContent" class="content container">
{% if breadcrumbs %}
<div id="docContent" class="content container">
{% if breadcrumbs %}
<div id="bread">
<ol class="breadcrumb">
{% for elem in breadcrumbs %}
{% for name, url in elem.items %}
{% if forloop.parentloop.last %}
<li class="active">{{ name }}</li>
{% else %}
<li>
<a href="{{ url }}">{{ name }}</a>
</li>
{% endif %}
{% endfor %}
{% endfor %}
</ol>
<ol class="breadcrumb">
{% for elem in breadcrumbs %}
{% for name, url in elem.items %}
{% if forloop.parentloop.last %}
<li class="active">{{ name }}</li>
{% else %}
<li>
<a href="{{ url }}">{{ name }}</a>
</li>
{% endif %}
{% endfor %}
{% endfor %}
</ol>
</div>
{% endif %}
{% if message %}
<div id="message">
{{ message }}
</div>
{% endif %}
{% block content %}
{% endblock content %}
{% if meta.url_contains_package and meta.current_package.license %}
{% endif %}
{% if message %}
<div id="message">{{ message }}</div>
{% endif %}
{% block content %}
{% endblock content %}
{% if meta.url_contains_package and meta.current_package.license %}
<p></p>
<div class="panel-group" id="license_accordion">
<div class="panel panel-default list-group-item" style="background-color:#f5f5f5">
<div class="panel-title">
<a data-toggle="collapse" data-parent="#licence_accordion" href="#license">License</a>
</div>
<div
class="panel panel-default list-group-item"
style="background-color:#f5f5f5"
>
<div class="panel-title">
<a
data-toggle="collapse"
data-parent="#licence_accordion"
href="#license"
>License</a
>
</div>
<div id="license" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<a target="_blank" href="{{ meta.current_package.license.link }}">
<img src="{{ meta.current_package.license.image_link }}">
</a>
</div>
</div>
<div id="license" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<a target="_blank" href="{{ meta.current_package.license.link }}">
<img src="{{ meta.current_package.license.image_link }}" />
</a>
</div>
</div>
</div>
{% endif %}
</div>
<!-- FOOTER - Legacy Bootstrap -->
<div class="legacy-bootstrap">
<div class="container text-center">
<hr/>
<div class="row">
<div class="col-lg-12">
<ul class="nav nav-pills nav-justified">
<li>
<a href="http://ml.auckland.ac.nz" target="_blank">
<img id="image-uoalogo" height="60" src='{% static "/images/UoA-Logo-Primary-RGB-Small.png" %}'
alt="The Univserity of Auckland"/>
</a>
</li>
<li>
<a href="https://eawag.ch" target="_blank">
<img id="image-ealogo" height="60" src='{% static "/images/ealogo.gif" %}' alt="Eawag"/>
</a>
</li>
<li>
<a href="https://www.uzh.ch/" target="_blank">
<img id="image-ufzlogo" height="60" src='{% static "/images/uzh-logo.svg" %}'
alt="University of Zurich"/>
</a>
</li>
</ul>
</div>
{% endif %}
</div>
</div>
<div class="row">
<div class="col-lg-12">
<ul class="nav nav-pills nav-justified">
<!-- FOOTER - Legacy Bootstrap -->
<div class="legacy-bootstrap">
<div class="container text-center">
<hr />
<div class="row">
<div class="col-lg-12">
<ul class="nav nav-pills nav-justified">
<li>
<a href="http://ml.auckland.ac.nz" target="_blank">
<img
id="image-uoalogo"
height="60"
src="{% static "/images/UoA-Logo-Primary-RGB-Small.png" %}"
alt="The Univserity of Auckland"
/>
</a>
</li>
<li>
<a href="https://eawag.ch" target="_blank">
<img
id="image-ealogo"
height="60"
src="{% static "/images/ealogo.gif" %}"
alt="Eawag"
/>
</a>
</li>
<li>
<a href="https://www.uzh.ch/" target="_blank">
<img
id="image-ufzlogo"
height="60"
src="{% static "/images/uzh-logo.svg" %}"
alt="University of Zurich"
/>
</a>
</li>
</ul>
</div>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<ul class="nav nav-pills nav-justified">
<!-- <li><a href="https://envipath.com/imprint/" target="_blank">Impressum/Imprint</a></li>-->
<li><a href="mailto:admin@envipath.org" target="_blank">Contact</a></li>
<li>
<a href="mailto:admin@envipath.org" target="_blank">Contact</a>
</li>
<!-- <li><a href="http://envipath.com" target="_blank"> enviPath UG (haftungsbeschr&auml;nkt) &amp; Co. KG &copy;-->
<!-- {{ YEAR }}</a></li>-->
</ul>
</ul>
</div>
</div>
</div>
</div>
</div>
<!-- End legacy Bootstrap footer -->
<!-- End legacy Bootstrap footer -->
<script>
$(function () {
<script>
$(function () {
// Hide actionsbutton if theres no action defined
if ($('#actionsButton ul').children().length > 0) {
$('#actionsButton').show();
if ($("#actionsButton ul").children().length > 0) {
$("#actionsButton").show();
}
});
</script>
{% block modals %}
{% include "modals/cite_modal.html" %}
{% include "modals/signup_modal.html" %}
{% include "modals/predict_modal.html" %}
{% include "modals/batch_predict_modal.html" %}
{% endblock %}
</body>
});
</script>
{% block modals %}
{% include "modals/cite_modal.html" %}
{% include "modals/predict_modal.html" %}
{% include "modals/batch_predict_modal.html" %}
{% endblock %}
</body>
</html>

View File

@ -1,38 +1,49 @@
<!DOCTYPE html>
<!doctype html>
<html data-theme="envipath" lang="en">
{% load static %}
<head>
{% load static %}
<head>
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="csrf-token" content="{{ csrf_token }}">
<meta name="csrf-token" content="{{ csrf_token }}" />
{# Favicon #}
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
<link
rel="shortcut icon"
type="image/png"
href="{% static 'images/favicon.ico' %}"
/>
{# Tailwind CSS + DaisyUI Output #}
<link href="{% static 'css/output.css' %}" rel="stylesheet" type="text/css"/>
<link
href="{% static 'css/output.css' %}"
rel="stylesheet"
type="text/css"
/>
{# jQuery - Keep for compatibility with existing JS #}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
{# Font Awesome #}
<link href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
<link
href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"
rel="stylesheet"
/>
{# Discourse embed for community #}
<script src="https://community.envipath.org/javascripts/embed-topics.js"></script>
<script>
const csrftoken = document.querySelector('[name=csrf-token]').content;
const csrftoken = document.querySelector("[name=csrf-token]").content;
// Setup CSRF header for all jQuery AJAX requests
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
// Setup CSRF header for all jQuery AJAX requests
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
},
});
</script>
{# General EP JS #}
@ -41,136 +52,172 @@
<script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script>
{% if not debug %}
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function () {
var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '{{ meta.site_id }}']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->
<!-- Matomo -->
<script>
var _paq = (window._paq = window._paq || []);
_paq.push(["trackPageView"]);
_paq.push(["enableLinkTracking"]);
(function () {
var u = "//matomo.envipath.com/";
_paq.push(["setTrackerUrl", u + "matomo.php"]);
_paq.push(["setSiteId", "{{ meta.site_id }}"]);
var d = document,
g = d.createElement("script"),
s = d.getElementsByTagName("script")[0];
g.async = true;
g.src = u + "matomo.js";
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->
{% endif %}
</head>
<body class="min-h-screen bg-base-300">
</head>
<body class="bg-base-300 min-h-screen">
{% include "includes/navbar.html" %}
{# Main Content Area #}
<main class="w-full">
{% block main_content %}
{# Breadcrumbs - outside main content, optional #}
{% if breadcrumbs %}
<div id="bread" class="max-w-7xl mx-auto px-4 py-4">
<div class="text-sm breadcrumbs">
<ul>
{% for elem in breadcrumbs %}
{% for name, url in elem.items %}
{% if forloop.parentloop.last %}
<li>{{ name }}</li>
{% else %}
<li><a href="{{ url }}">{{ name }}</a></li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{# Main content container - paper effect on medium+ screens #}
<div id="docContent" class="w-full xl:w-xl md:mx-auto md:my-8 bg-base-100 md:shadow-2xl md:rounded-lg border-2">
{# Messages - inside paper #}
{% if message %}
<div id="message" class="alert alert-info m-4">
{{ message }}
</div>
{% endif %}
{# Page content - no enforced styles #}
{% block content %}
{% endblock content %}
{# License - inside paper if present #}
{% if meta.url_contains_package and meta.current_package.license %}
<div class="collapse collapse-arrow bg-base-200 m-8">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
License
</div>
<div class="collapse-content">
<a target="_blank" href="{{ meta.current_package.license.link }}">
<img src="{{ meta.current_package.license.image_link }}" alt="License">
</a>
</div>
</div>
{% endif %}
{% block main_content %}
{# Breadcrumbs - outside main content, optional #}
{% if breadcrumbs %}
<div id="bread" class="max-w-7xl mx-auto px-4 py-4">
<div class="breadcrumbs text-sm">
<ul>
{% for elem in breadcrumbs %}
{% for name, url in elem.items %}
{% if forloop.parentloop.last %}
<li>{{ name }}</li>
{% else %}
<li><a href="{{ url }}">{{ name }}</a></li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
</div>
{% endblock main_content %}
</div>
{% endif %}
{# Main content container - paper effect on medium+ screens #}
<div
id="docContent"
class="bg-base-100 mx-auto md:my-8 md:max-w-6xl md:rounded-lg md:shadow-xl"
>
{# Messages - inside paper #}
{% if message %}
<div id="message" class="alert alert-info m-4">{{ message }}</div>
{% endif %}
{# Page content - no enforced styles #}
{% block content %}
{% endblock content %}
{# License - inside paper if present #}
{% if meta.url_contains_package and meta.current_package.license %}
<div class="collapse collapse-arrow bg-base-200 m-8">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">License</div>
<div class="collapse-content">
<a
target="_blank"
href="{{ meta.current_package.license.link }}"
>
<img
src="{{ meta.current_package.license.image_link }}"
alt="License"
/>
</a>
</div>
</div>
{% endif %}
</div>
{% endblock main_content %}
</main>
{% include "includes/footer.html" %}
{# Floating Help Tab #}
{% if not public_mode %}
<div class="fixed right-0 top-1/2 -translate-y-1/2 z-50">
<a href="https://community.envipath.org/" target="_blank"
class="flex items-center justify-center btn btn-secondary hover:btn-secondary-focus text-secondary-content text-sm shadow-lg transition-all duration-300 hover:scale-105 hover:-translate-x-1"
title="Get Help from the Community">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-question-mark-icon lucide-message-circle-question-mark"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<path d="M12 17h.01"/>
</svg>
<div class="fixed right-0 top-1/2 -translate-y-1/2 z-50">
<a
href="https://community.envipath.org/"
target="_blank"
class="btn btn-secondary hover:btn-secondary-focus text-secondary-content flex items-center justify-center text-sm shadow-lg transition-all duration-300 hover:-translate-x-1 hover:scale-105"
title="Get Help from the Community"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-message-circle-question-mark-icon lucide-message-circle-question-mark"
>
<path
d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"
/>
<path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3" />
<path d="M12 17h.01" />
</svg>
</a>
</div>
</div>
{% endif %}
{# Modals - TODO: Convert these to DaisyUI modals #}
{% block modals %}
{# Note: These modals still use Bootstrap markup and will need conversion #}
{% include "modals/cite_modal.html" %}
{% include "modals/signup_modal.html" %}
{% include "modals/predict_modal.html" %}
{% include "modals/batch_predict_modal.html" %}
{% include "modals/search_modal.html" %}
{% endblock %}
<script>
$(function () {
// Hide actionsbutton if there's no action defined
if ($('#actionsButton ul').children().length > 0) {
$('#actionsButton').show();
}
$(function () {
// Hide actionsbutton if there's no action defined
if ($("#actionsButton ul").children().length > 0) {
$("#actionsButton").show();
}
});
// 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)
document.addEventListener('keydown', function(event) {
// Check if user is typing in an input field
const activeElement = document.activeElement;
const isInputField = activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.contentEditable === 'true'
);
// Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux)
document.addEventListener("keydown", function (event) {
// Check if user is typing in an input field
const activeElement = document.activeElement;
const isInputField =
activeElement &&
(activeElement.tagName === "INPUT" ||
activeElement.tagName === "TEXTAREA" ||
activeElement.contentEditable === "true");
if (isInputField) {
return; // Don't trigger shortcut when typing in input fields
}
if (isInputField) {
return; // Don't trigger shortcut when typing in input fields
}
// Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux)
const isMac = /Mac/.test(navigator.platform);
const isCorrectModifier = isMac ? event.metaKey : event.ctrlKey;
// Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux)
const isMac = /Mac/.test(navigator.platform);
const isCorrectModifier = isMac ? event.metaKey : event.ctrlKey;
if (isCorrectModifier && event.key === 'k') {
event.preventDefault();
window.location.href = '/search';
}
});
if (isCorrectModifier && event.key === "k") {
event.preventDefault();
openSearchModal();
}
});
</script>
</body>
</body>
</html>

View File

@ -1,71 +1,147 @@
{% load static %}
{# Modern DaisyUI Navbar #}
<div class="navbar bg-neutral-50 text-neutral-950 shadow-lg x-50">
<div class="navbar-start">
<a href="{{ meta.server_url }}" class="btn btn-ghost normal-case text-xl">
<svg class="h-8 fill-base-content" viewBox="0 0 104 26" role="img">
<use href='{% static "/images/logo-name.svg" %}#ep-logo-name' />
</svg>
</a>
</div>
<div class="navbar-start">
<a href="{{ meta.server_url }}" class="btn btn-ghost normal-case text-xl">
<svg class="h-8 fill-base-content" viewBox="0 0 104 26" role="img">
<use href="{% static "/images/logo-name.svg" %}#ep-logo-name" />
</svg>
</a>
</div>
{% if not public_mode %}
{% if not public_mode %}
<div class="navbar-center hidden lg:flex">
<a href="#" role="button" class="btn btn-ghost" id="predictLink">Predict</a>
<!-- <li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li> -->
<!--<li><a href="{{ meta.server_url }}/browse" id="browseLink">Browse</a></li>-->
<div class="dropdown dropdown-center">
<div tabindex="0" role="button" class="btn btn-ghost">Browse</div>
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
<li><a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a></li>
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li>
<li><a href="{{ meta.server_url }}/compound" id="compoundLink">Compound</a></li>
<li><a href="{{ meta.server_url }}/reaction" id="reactionLink">Reaction</a></li>
<li><a href="{{ meta.server_url }}/model" id="relative-reasoningLink">Model</a></li>
<li><a href="{{ meta.server_url }}/scenario" id="scenarioLink">Scenario</a></li>
</ul>
</div>
<a
href="{{ meta.server_url }}/predict"
role="button"
class="btn btn-ghost"
id="predictLink"
>Predict</a
>
<!-- <li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li> -->
<!--<li><a href="{{ meta.server_url }}/browse" id="browseLink">Browse</a></li>-->
<div class="dropdown dropdown-center">
<div tabindex="0" role="button" class="btn btn-ghost">Browse</div>
<ul
tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm"
>
<li>
<a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a>
</li>
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li>
<li>
<a href="{{ meta.server_url }}/compound" id="compoundLink"
>Compound</a
>
</li>
<li>
<a href="{{ meta.server_url }}/reaction" id="reactionLink"
>Reaction</a
>
</li>
<li>
<a href="{{ meta.server_url }}/model" id="relative-reasoningLink"
>Model</a
>
</li>
<li>
<a href="{{ meta.server_url }}/scenario" id="scenarioLink"
>Scenario</a
>
</li>
</ul>
</div>
</div>
{% endif %}
{% endif %}
<div class="navbar-end">
{% if not public_mode %}
<a href="/search" role="button" >
<div class="flex items-center badge badge-dash space-x-1 bg-base-200 text-base-content/50 p-2 m-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
<span id="search-shortcut">⌘K</span>
</div>
</a>
{% endif %}
{% if meta.user.username == 'anonymous' or public_mode %}
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a>
{% else %}
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost m-1 btn-circle" id="loggedInButton">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg>
</div>
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-sm">
<li><a href="{{ meta.user.url }}" id="accountbutton">Settings</a></li>
<li>
<form id="logoutForm" action="{% url 'logout' %}" method="post" style="display: none;">
{% csrf_token %}
<input type="hidden" name="logout" value="true">
</form>
<a href="#" id="logoutButton" onclick="event.preventDefault(); document.getElementById('logoutForm').submit();">Logout</a>
</li>
</ul>
</div>
{% endif %}
</div>
<div class="navbar-end">
{% if not public_mode %}
<a id="search-trigger" role="button" class="cursor-pointer">
<div
class="flex items-center badge badge-dash space-x-1 bg-base-200 text-base-content/50 p-2 m-1"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-search-icon lucide-search"
>
<path d="m21 21-4.34-4.34" />
<circle cx="11" cy="11" r="8" />
</svg>
<span id="search-shortcut">⌘K</span>
</div>
</a>
{% endif %}
{% if meta.user.username == 'anonymous' or public_mode %}
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a>
{% else %}
<div class="dropdown dropdown-end">
<div
tabindex="0"
role="button"
class="btn btn-ghost m-1 btn-circle"
id="loggedInButton"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-circle-user-icon lucide-circle-user"
>
<circle cx="12" cy="12" r="10" />
<circle cx="12" cy="10" r="3" />
<path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662" />
</svg>
</div>
<ul
tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-sm"
>
<li><a href="{{ meta.user.url }}" id="accountbutton">Settings</a></li>
<li>
<form
id="logoutForm"
action="{% url 'logout' %}"
method="post"
style="display: none;"
>
{% csrf_token %}
<input type="hidden" name="logout" value="true" />
</form>
<a
href="#"
id="logoutButton"
onclick="event.preventDefault(); document.getElementById('logoutForm').submit();"
>Logout</a
>
</li>
</ul>
</div>
{% endif %}
</div>
</div>
<script>
// OS-aware search shortcut display
(function() {
const isMac = /Mac/.test(navigator.platform);
const shortcutElement = document.getElementById('search-shortcut');
if (shortcutElement) {
shortcutElement.textContent = isMac ? '⌘K' : 'Ctrl+K';
}
})();
// OS-aware search shortcut display
(function () {
const isMac = /Mac/.test(navigator.platform);
const shortcutElement = document.getElementById("search-shortcut");
if (shortcutElement) {
shortcutElement.textContent = isMac ? "⌘K" : "Ctrl+K";
}
})();
</script>

View File

@ -1,190 +1,269 @@
{% extends "framework_modern.html" %}
{% load static %}
{% block main_content %}
<!-- Hero Section with Logo and Search -->
<section class="hero h-fit max-w-5xl w-full shadow-none mx-auto relative">
<div class="hero min-h-[calc(100vh*0.4)] bg-gradient-to-br from-primary-800 to-primary-600"
style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;"
>
<div class="hero-overlay"></div>
<!-- Predict Pathway text over the image -->
<div class="absolute bottom-40 left-1/8 -translate-x-8 z-10">
<h2 class="text-3xl text-base-100 text-shadow-lg text-left">Predict Your Pathway</h2>
</div>
</div>
</section>
<div class="shadow-md max-w-5xl mx-auto bg-base-200">
<!-- Predict Pathway Section -->
<div class="flex-col lg:flex-row-reverse w-full mx-auto -mt-32 relative z-20 mb-10 ">
<div class="card bg-base-100 shrink-0 shadow-xl w-3/4 mx-auto transition-all duration-300 ease-in-out">
<div class="card-body">
<!-- Input Mode Toggle - Fixed position outside fieldset -->
<div class="flex flex-row justify-start items-center h-fit ml-8 my-4">
<div class="flex items-center gap-2">
<!-- <span class="text-sm text-neutral-500">Input Mode:</span> -->
<label class="toggle text-base-content toggle-md">
<input type="checkbox" />
<svg aria-label="smiles mode" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="size-5">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2"
fill="currentColor"
stroke="none"
>
<path fill-rule="evenodd" 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" clip-rule="evenodd" />
</g>
</svg>
<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>
</label>
</div>
</div>
<fieldset class="fieldset transition-all duration-300 ease-in-out overflow-hidden">
<form id="index-form" action="{{ meta.current_package.url }}/pathway" method="POST">
{% csrf_token %}
<div id="text-input-container" class="transition-all duration-300 ease-in-out opacity-100 transform scale-100">
<div class="join w-full mx-auto">
<input type="text" id="index-form-text-input" placeholder="canonical SMILES string" class="input grow input-md join-item" />
<button class="btn btn-neutral join-item">Predict!</button>
</div>
<div class="label relative w-full mt-1" >
<div class="flex gap-2">
<a href="#" class="example-link cursor-pointer hover:text-primary"
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
title="load example">Caffeine</a>
<a href="#" class="example-link cursor-pointer hover:text-primary"
data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
title="load example">Ibuprofen</a>
</div>
<a class="absolute top-0 left-[calc(100%-5.4rem)]" href="#">Advanced</a>
</div>
</div>
<div id="ketcher-container" class="hidden w-full transition-all duration-300 ease-in-out opacity-0 transform scale-95">
<iframe id="index-ketcher" src="{% static '/js/ketcher2/ketcher.html' %}"
width="100%" height="400" class="rounded-lg"></iframe>
<button class="btn btn-lg bg-primary-950 text-primary-50 join-item w-full mt-2">Predict!</button>
<a class="label mx-auto w-full mt-1" href="#">Advanced</a>
</div>
<input type="hidden" id="index-form-smiles" name="smiles" value="smiles">
<input type="hidden" id="index-form-predict" name="predict" value="predict">
<input type="hidden" id="current-action" value="predict">
</form>
</fieldset>
</div>
</div>
<!-- Hero Section with Logo and Search -->
<section class="hero h-fit max-w-5xl w-full shadow-none mx-auto relative">
<div
class="hero min-h-[calc(100vh*0.4)] bg-gradient-to-br from-primary-800 to-primary-600"
style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;"
>
<div class="hero-overlay"></div>
<!-- Predict Pathway text over the image -->
<div class="absolute bottom-40 left-1/8 -translate-x-8 z-10">
<h2 class="text-3xl text-base-100 text-shadow-lg text-left">
Predict Your Pathway
</h2>
</div>
</div>
</section>
<div class="shadow-md max-w-5xl mx-auto bg-base-200">
<!-- Predict Pathway Section -->
<div
class="flex-col lg:flex-row-reverse w-full mx-auto -mt-32 relative z-20 mb-10 "
>
<div
class="card bg-base-100 shrink-0 shadow-xl w-3/4 mx-auto transition-all duration-300 ease-in-out"
>
<div class="card-body">
<!-- Input Mode Toggle - Fixed position outside fieldset -->
<div class="flex flex-row justify-start items-center h-fit ml-8 my-4">
<div class="flex items-center gap-2">
<!-- <span class="text-sm text-neutral-500">Input Mode:</span> -->
<label class="toggle text-base-content toggle-md">
<input type="checkbox" />
<svg
aria-label="smiles mode"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
class="size-5"
>
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2"
fill="currentColor"
stroke="none"
>
<path
fill-rule="evenodd"
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"
clip-rule="evenodd"
/>
</g>
</svg>
<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>
</label>
</div>
</div>
<fieldset
class="fieldset transition-all duration-300 ease-in-out overflow-hidden"
>
<form
id="index-form"
action="{{ meta.current_package.url }}/pathway"
method="POST"
>
{% csrf_token %}
<div
id="text-input-container"
class="transition-all duration-300 ease-in-out opacity-100 transform scale-100"
>
<div class="join w-full mx-auto">
<input
type="text"
id="index-form-text-input"
placeholder="canonical SMILES string"
class="input grow input-md join-item"
/>
<button class="btn btn-neutral join-item">Predict!</button>
</div>
<div class="label relative w-full mt-1">
<div class="flex gap-2">
<a
href="#"
class="example-link cursor-pointer hover:text-primary"
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
title="load example"
>Caffeine</a
>
<a
href="#"
class="example-link cursor-pointer hover:text-primary"
data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
title="load example"
>Ibuprofen</a
>
</div>
<a class="absolute top-0 left-[calc(100%-5.4rem)]" href="#"
>Advanced</a
>
</div>
</div>
<div
id="ketcher-container"
class="hidden w-full transition-all duration-300 ease-in-out opacity-0 transform scale-95"
>
<iframe
id="index-ketcher"
src="{% static '/js/ketcher2/ketcher.html' %}"
width="100%"
height="400"
class="rounded-lg"
></iframe>
<button
class="btn btn-lg bg-primary-950 text-primary-50 join-item w-full mt-2"
>
Predict!
</button>
<a class="label mx-auto w-full mt-1" href="#">Advanced</a>
</div>
<input
type="hidden"
id="index-form-smiles"
name="smiles"
value="smiles"
/>
<input
type="hidden"
id="index-form-predict"
name="predict"
value="predict"
/>
<input type="hidden" id="current-action" value="predict" />
</form>
</fieldset>
</div>
</div>
</div>
<!-- Community News Section -->
<section class="py-16 bg-base-200 z-10 mx-8">
<div class="max-w-7xl mx-auto px-4">
<h2 class="h2 font-bold text-left mb-8">Community Updates</h2>
<div class="max-w-7xl mx-auto px-4">
<h2 class="h2 font-bold text-left mb-8">Community Updates</h2>
<div id="community-news-container" class="flex gap-4 justify-center">
<!-- News cards will be populated here -->
<div id="loading" class="flex justify-center w-full">
<span class="loading loading-spinner loading-lg"></span>
</div>
</div>
<div class="text-right mt-6">
<a href="https://community.envipath.org/c/announcements/10" target="_blank" class="btn btn-ghost btn-sm">
Read More Announcements
</a>
</div>
<!-- Discourse API integration -->
<script src="{% static 'js/discourse-api.js' %}"></script>
<div id="community-news-container" class="flex gap-4 justify-center">
<!-- News cards will be populated here -->
<div id="loading" class="flex justify-center w-full">
<span class="loading loading-spinner loading-lg"></span>
</div>
</div>
<div class="text-right mt-6">
<a
href="https://community.envipath.org/c/announcements/10"
target="_blank"
class="btn btn-ghost btn-sm"
>
Read More Announcements
</a>
</div>
<!-- Discourse API integration -->
<script src="{% static 'js/discourse-api.js' %}"></script>
</div>
</section>
<!-- Mission Statement Section -->
<section class="py-16 from-base-200 to-base-100 bg-gradient-to-b">
<div class="mx-auto px-8 md:px-12">
<div class="flex flex-row gap-4">
<div class="w-1/3">
<img src="{% static "/images/ep-rule-artwork.png" %}" alt="rule-based iterative tree greneration" class="w-full h-full object-contain" />
</div>
<div class="space-y-4 text-left w-2/3 mr-8">
<h2 class="h2 font-bold mb-8">About enviPath</h2>
<p class="">
enviPath is a database and prediction system for the microbial
biotransformation of organic environmental contaminants. The
database provides the possibility to store and view experimentally
observed biotransformation pathways.
</p>
<p class="">
The pathway prediction system provides different relative reasoning models
to predict likely biotransformation pathways and products. Explore our tools
and contribute to advancing environmental biotransformation research.
</p>
<div class="flex flex-row gap-4 float-right">
<a href="/about" class="btn btn-ghost-neutral">Read More</a>
<a href="/about" class="btn btn-neutral">Publications</a>
</div>
</div>
<div class="mx-auto px-8 md:px-12">
<div class="flex flex-row gap-4">
<div class="w-1/3">
<img
src="{% static "/images/ep-rule-artwork.png" %}"
alt="rule-based iterative tree greneration"
class="w-full h-full object-contain"
/>
</div>
<div class="space-y-4 text-left w-2/3 mr-8">
<h2 class="h2 font-bold mb-8">About enviPath</h2>
<p class="">
enviPath is a database and prediction system for the microbial
biotransformation of organic environmental contaminants. The
database provides the possibility to store and view experimentally
observed biotransformation pathways.
</p>
<p class="">
The pathway prediction system provides different relative
reasoning models to predict likely biotransformation pathways and
products. Explore our tools and contribute to advancing
environmental biotransformation research.
</p>
<div class="flex flex-row gap-4 float-right">
<a href="/about" class="btn btn-ghost-neutral">Read More</a>
<a href="/about" class="btn btn-neutral">Publications</a>
</div>
</div>
</div>
</div>
</section>
<!-- Partners Section -->
<section class="py-14 sm:py-12 bg-base-100">
<div class="mx-auto px-6 lg:px-8">
<div class="divider"><h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2></div>
<div class="mx-auto mt-10 grid max-w-lg grid-cols-4 items-center gap-x-8 gap-y-10 sm:max-w-xl sm:grid-cols-6 sm:gap-x-10 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<img src="{% static "/images/uoa-logo-small.png" %}" alt="The University of Auckland" class=" max-h-20 w-full object-contain lg:col-span-1" />
<img src="{% static "/images/logo-eawag.svg" %}" alt="Eawag" class=" max-h-12 w-full object-contain lg:col-span-1" />
<img src="{% static "/images/uzh-logo.svg" %}" alt="University of Zurich" class="2 max-h-16 w-full object-contain lg:col-span-1" />
</div>
<div class="mx-auto px-6 lg:px-8">
<div class="divider">
<h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2>
</div>
<div
class="mx-auto mt-10 grid max-w-lg grid-cols-4 items-center gap-x-8 gap-y-10 sm:max-w-xl sm:grid-cols-6 sm:gap-x-10 lg:mx-0 lg:max-w-none lg:grid-cols-3"
>
<img
src="{% static "/images/uoa-logo-small.png" %}"
alt="The University of Auckland"
class=" max-h-20 w-full object-contain lg:col-span-1"
/>
<img
src="{% static "/images/logo-eawag.svg" %}"
alt="Eawag"
class=" max-h-12 w-full object-contain lg:col-span-1"
/>
<img
src="{% static "/images/uzh-logo.svg" %}"
alt="University of Zurich"
class="2 max-h-16 w-full object-contain lg:col-span-1"
/>
</div>
</div>
</section>
</div>
<script language="javascript">
var currentPackage = "{{ meta.current_package.url }}";
// Discourse API integration is now handled by discourse-api.js
// Function to render Discourse topics into cards
function renderDiscourseTopics(topics) {
const container = document.getElementById("community-news-container");
if (!container) return;
</div>
// Clear container
container.innerHTML = "";
<script language="javascript">
var currentPackage = "{{ meta.current_package.url }}";
// Create cards for each topic
topics.forEach((topic) => {
const card = createDiscourseCard(topic);
container.insertAdjacentHTML("beforeend", card);
});
}
// Discourse API integration is now handled by discourse-api.js
// Function to create HTML card for a topic
function createDiscourseCard(topic) {
const date = new Date(topic.created_at).toLocaleDateString();
// Function to render Discourse topics into cards
function renderDiscourseTopics(topics) {
const container = document.getElementById('community-news-container');
if (!container) return;
// Clear container
container.innerHTML = '';
// Create cards for each topic
topics.forEach(topic => {
const card = createDiscourseCard(topic);
container.insertAdjacentHTML('beforeend', card);
});
}
// Function to create HTML card for a topic
function createDiscourseCard(topic) {
const date = new Date(topic.created_at).toLocaleDateString();
return `
return `
<div class="card bg-white shadow-xs hover:shadow-lg transition-shadow duration-300 h-64 w-75 flex-shrink-0">
<div class="card-body flex flex-col h-full">
<h3 class="card-title leading-tight font-normal tracking-tight h-12 mb-2 line-clamp-2 text-ellipsis wrap-break-word overflow-hidden">
@ -215,146 +294,146 @@
</div>
</div>
`;
}
// Make render function globally available
window.renderDiscourseTopics = renderDiscourseTopics;
// Toggle functionality with smooth animations
function toggleInputMode() {
const toggle = $('input[type="checkbox"]');
const textContainer = $("#text-input-container");
const ketcherContainer = $("#ketcher-container");
const formCard = $(".card");
const fieldset = $(".fieldset");
if (toggle.is(":checked")) {
// Draw mode - show Ketcher, hide text input
textContainer.addClass("opacity-0 transform scale-95");
textContainer.removeClass("opacity-100 transform scale-100");
// Adjust fieldset padding for Ketcher mode - reduce padding and make more compact
fieldset.removeClass("p-8");
fieldset.addClass("p-4");
// Wait for fade out to complete, then hide and show new content
setTimeout(() => {
textContainer.addClass("hidden");
ketcherContainer.removeClass("hidden opacity-0 transform scale-95");
ketcherContainer.addClass("opacity-100 transform scale-100");
// Force re-evaluation of iframe size
const iframe = document.getElementById("index-ketcher");
if (iframe) {
iframe.style.height = "400px";
}
}, 300);
} else {
// SMILES mode - show text input, hide Ketcher
ketcherContainer.addClass("opacity-0 transform scale-95");
ketcherContainer.removeClass("opacity-100 transform scale-100");
// Restore fieldset padding for text input mode
fieldset.removeClass("p-4");
fieldset.addClass("p-8");
// Wait for fade out to complete, then hide and show new content
setTimeout(() => {
ketcherContainer.addClass("hidden");
textContainer.removeClass("hidden opacity-0 transform scale-95");
textContainer.addClass("opacity-100 transform scale-100");
}, 300);
// Transfer SMILES from Ketcher to text input if available
if (window.indexKetcher && window.indexKetcher.getSmiles) {
const smiles = window.indexKetcher.getSmiles();
if (smiles && smiles.trim() !== "") {
$("#index-form-text-input").val(smiles);
}
}
}
}
// Ketcher integration
function indexKetcherToTextInput() {
$("#index-form-smiles").val(this.ketcher.getSmiles());
}
$(function () {
// Initialize fieldset with proper padding
$(".fieldset").addClass("p-8");
// Toggle event listener
$('input[type="checkbox"]').on("change", toggleInputMode);
// Ketcher iframe load handler
$("#index-ketcher").on("load", function () {
const checkKetcherReady = () => {
const win = this.contentWindow;
if (win.ketcher && "editor" in win.ketcher) {
window.indexKetcher = win.ketcher;
win.ketcher.editor.event.change.handlers.push({
once: false,
priority: 0,
f: indexKetcherToTextInput,
ketcher: win.ketcher,
});
} else {
setTimeout(checkKetcherReady, 100);
}
};
checkKetcherReady();
});
// Handle example link clicks
$(".example-link").on("click", function (e) {
e.preventDefault();
const smiles = $(this).data("smiles");
const title = $(this).attr("title");
// Check if we're in Ketcher mode or text input mode
if ($('input[type="checkbox"]').is(":checked")) {
// In Ketcher mode - set the SMILES in Ketcher
if (window.indexKetcher && window.indexKetcher.setMolecule) {
window.indexKetcher.setMolecule(smiles);
}
} else {
// In text input mode - set the SMILES in the text input
$("#index-form-text-input").val(smiles);
}
// Make render function globally available
window.renderDiscourseTopics = renderDiscourseTopics;
// Show a brief feedback
const originalText = $(this).text();
$(this).text(`loaded!`);
setTimeout(() => {
$(this).text(originalText);
}, 1000);
});
// Toggle functionality with smooth animations
function toggleInputMode() {
const toggle = $('input[type="checkbox"]');
const textContainer = $('#text-input-container');
const ketcherContainer = $('#ketcher-container');
const formCard = $('.card');
const fieldset = $('.fieldset');
// Handle form submission on Enter
$("#index-form").on("submit", function (e) {
e.preventDefault();
if (toggle.is(':checked')) {
// Draw mode - show Ketcher, hide text input
textContainer.addClass('opacity-0 transform scale-95');
textContainer.removeClass('opacity-100 transform scale-100');
var textSmiles = "";
// Adjust fieldset padding for Ketcher mode - reduce padding and make more compact
fieldset.removeClass('p-8');
fieldset.addClass('p-4');
// Wait for fade out to complete, then hide and show new content
setTimeout(() => {
textContainer.addClass('hidden');
ketcherContainer.removeClass('hidden opacity-0 transform scale-95');
ketcherContainer.addClass('opacity-100 transform scale-100');
// Force re-evaluation of iframe size
const iframe = document.getElementById('index-ketcher');
if (iframe) {
iframe.style.height = '400px';
}
}, 300);
} else {
// SMILES mode - show text input, hide Ketcher
ketcherContainer.addClass('opacity-0 transform scale-95');
ketcherContainer.removeClass('opacity-100 transform scale-100');
// Restore fieldset padding for text input mode
fieldset.removeClass('p-4');
fieldset.addClass('p-8');
// Wait for fade out to complete, then hide and show new content
setTimeout(() => {
ketcherContainer.addClass('hidden');
textContainer.removeClass('hidden opacity-0 transform scale-95');
textContainer.addClass('opacity-100 transform scale-100');
}, 300);
// Transfer SMILES from Ketcher to text input if available
if (window.indexKetcher && window.indexKetcher.getSmiles) {
const smiles = window.indexKetcher.getSmiles();
if (smiles && smiles.trim() !== '') {
$('#index-form-text-input').val(smiles);
}
}
}
// Check if we're in Ketcher mode and extract SMILES
if ($('input[type="checkbox"]').is(":checked") && window.indexKetcher) {
textSmiles = window.indexKetcher.getSmiles().trim();
} else {
textSmiles = $("#index-form-text-input").val().trim();
}
// Ketcher integration
function indexKetcherToTextInput() {
$('#index-form-smiles').val(this.ketcher.getSmiles());
if (textSmiles === "") {
return;
}
$(function () {
// Initialize fieldset with proper padding
$('.fieldset').addClass('p-8');
$("#index-form-smiles").val(textSmiles);
$("#index-form").attr("action", currentPackage + "/pathway");
$("#index-form").attr("method", "POST");
this.submit();
});
// Toggle event listener
$('input[type="checkbox"]').on('change', toggleInputMode);
// Ketcher iframe load handler
$('#index-ketcher').on('load', function() {
const checkKetcherReady = () => {
const win = this.contentWindow;
if (win.ketcher && 'editor' in win.ketcher) {
window.indexKetcher = win.ketcher;
win.ketcher.editor.event.change.handlers.push({
once: false,
priority: 0,
f: indexKetcherToTextInput,
ketcher: win.ketcher
});
} else {
setTimeout(checkKetcherReady, 100);
}
};
checkKetcherReady();
});
// Handle example link clicks
$('.example-link').on('click', function(e) {
e.preventDefault();
const smiles = $(this).data('smiles');
const title = $(this).attr('title');
// Check if we're in Ketcher mode or text input mode
if ($('input[type="checkbox"]').is(':checked')) {
// In Ketcher mode - set the SMILES in Ketcher
if (window.indexKetcher && window.indexKetcher.setMolecule) {
window.indexKetcher.setMolecule(smiles);
}
} else {
// In text input mode - set the SMILES in the text input
$('#index-form-text-input').val(smiles);
}
// Show a brief feedback
const originalText = $(this).text();
$(this).text(`loaded!`);
setTimeout(() => {
$(this).text(originalText);
}, 1000);
});
// Handle form submission on Enter
$('#index-form').on("submit", function (e) {
e.preventDefault();
var textSmiles = '';
// Check if we're in Ketcher mode and extract SMILES
if ($('input[type="checkbox"]').is(':checked') && window.indexKetcher) {
textSmiles = window.indexKetcher.getSmiles().trim();
} else {
textSmiles = $('#index-form-text-input').val().trim();
}
if (textSmiles === '') {
return;
}
$('#index-form-smiles').val(textSmiles);
$("#index-form").attr("action", currentPackage + "/pathway");
$("#index-form").attr("method", 'POST');
this.submit();
});
// Discourse topics are now loaded automatically by discourse-api.js
});
</script>
// Discourse topics are now loaded automatically by discourse-api.js
});
</script>
{% endblock main_content %}

View File

@ -1,43 +1,55 @@
<div class="modal fade" tabindex="-1" id="retrain_model_modal" role="dialog" aria-labelledby="retrain_model_modal"
aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title">Retrain Model</h4>
</div>
<div class="modal-body">
<form id="retrain_model_form" accept-charset="UTF-8" action="{{ meta.current_package.url }}/model"
data-remote="true" method="post">
<div class="jumbotron">
To reflect changes in the rule or data packages, you can use the "Retrain" button,
to let the model reflect the changes without creating a new model.
While the model is retraining, it will be unavailable for prediction.
</div>
{% csrf_token %}
<input type="hidden" name="action" value="retrain">
</form>
</div>
<div class="modal-footer">
<a id="retrain_model_form_submit" class="btn btn-primary" href="#">Retrain</a>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div>
<div
class="modal fade"
tabindex="-1"
id="retrain_model_modal"
role="dialog"
aria-labelledby="retrain_model_modal"
aria-hidden="true"
>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title">Retrain Model</h4>
</div>
<div class="modal-body">
<form
id="retrain_model_form"
accept-charset="UTF-8"
action="{{ meta.current_object.url }}"
data-remote="true"
method="post"
>
<div class="jumbotron">
To reflect changes in the rule or data packages, you can use the
"Retrain" button, to let the model reflect the changes without
creating a new model. While the model is retraining, it will be
unavailable for prediction.
</div>
{% csrf_token %}
<input type="hidden" name="hidden" value="retrain" />
</form>
</div>
<div class="modal-footer">
<a id="retrain_model_form_submit" class="btn btn-primary" href="#"
>Retrain</a
>
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel
</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#retrain_model_form_submit').on('click', function (e) {
e.preventDefault();
$('#retrain_model_form').submit();
});
$(function () {
$("#retrain_model_form_submit").on("click", function (e) {
e.preventDefault();
$("#retrain_model_form").submit();
});
});
</script>

View File

@ -52,8 +52,6 @@
</div>
</div>
<input type="hidden" id="license" name="license">
<input type="hidden" id="license-link" name="license-link">
<input type="hidden" id="license-image-link" name="license-image-link">
</form>
</div>
<div class="modal-footer">
@ -128,8 +126,6 @@ function cc() {
$('#ccfig').append(img_tpl);
$('#license').val(ccstr);
$('#license-link').val(link);
$('#license-image-link').val(imageLink);
} else {
$('#ccfig').empty();
$('#set_license_form_submit').prop('disabled', true);

View File

@ -0,0 +1,686 @@
{% load static %}
<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">
<!-- Search Input and Mode Selector -->
<div class="form-control mb-4 w-full shrink-0">
<div class="join m-0 w-full items-center p-3">
<label class="input join-item input-ghost grow">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-search-icon lucide-search"
>
<path d="m21 21-4.34-4.34" />
<circle cx="11" cy="11" r="8" />
</svg>
<input
type="text"
autofocus
id="modal_searchbar"
placeholder="Benfuracarb"
class="grow"
aria-label="Search"
/>
</label>
<!-- Mode Dropdown -->
<div>
<button
type="button"
tabindex="0"
id="modal_mode_button"
popovertarget="search_dropdown_menu"
style="anchor-name:--1"
class="btn join-item btn-ghost"
>
Text
<svg
class="ml-1 h-4 w-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M19 9l-7 7-7-7"
/>
</svg>
</button>
<ul
tabindex="0"
class="dropdown dropdown-end menu bg-base-200 rounded-box w-64 p-2 shadow-lg"
popover
id="search_dropdown_menu"
style="position-anchor:--anchor-2"
>
<li class="menu-title">Text</li>
<li>
<a
id="modal_dropdown_text"
class="tooltip tooltip-left"
data-tip="Search on object names and descriptions"
>
Text
</a>
</li>
<li class="menu-title">SMILES</li>
<li>
<a
id="modal_dropdown_smiles_default"
class="tooltip tooltip-left"
data-tip="Ignores stereochemistry and charge"
>
Default
</a>
</li>
<li>
<a
id="modal_dropdown_smiles_canonical"
class="tooltip tooltip-left"
data-tip="Ignores stereochemistry, preserves charge"
>
Canonical
</a>
</li>
<li>
<a
id="modal_dropdown_smiles_exact"
class="tooltip tooltip-left"
data-tip="Exact match for stereochemistry and charge"
>
Exact
</a>
</li>
<li class="menu-title">InChI</li>
<li>
<a
id="modal_dropdown_inchikey"
class="tooltip tooltip-left"
data-tip="Search by InChIKey"
>
InChIKey
</a>
</li>
</ul>
</div>
<button
type="button"
id="modal_search_button"
class="btn btn-xs btn-ghost join-item"
>
<kbd class="kbd kbd-sm text-base-content/50 p-1">
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-corner-down-left-icon lucide-corner-down-left"
>
<path d="M20 4v7a4 4 0 0 1-4 4H4" />
<path d="m9 10-5 5 5 5" />
</svg>
</kbd>
</button>
</div>
</div>
<!-- Package Selector with Pills -->
<div class="form-control mb-4 shrink-0">
<!-- Pills Container -->
<div
id="modal_package_pills_container"
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 -->
</div>
<!-- Package Dropdown Menu -->
<ul
class="dropdown dropdown-center menu bg-base-200 rounded-box max-h-96 w-80 overflow-y-auto p-2 shadow-lg"
popover
id="package_dropdown_menu"
style="position-anchor:--anchor-packages"
>
{% if unreviewed_packages %}
<li class="menu-title">Reviewed Packages</li>
{% for obj in reviewed_packages %}
<li>
<a
class="package-option flex items-center justify-between"
data-package-url="{{ obj.url }}"
data-package-name="{{ obj.name }}"
>
<span>{{ obj.name }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="package-checkmark hidden h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
</a>
</li>
{% endfor %}
<li class="menu-title">Unreviewed Packages</li>
{% for obj in unreviewed_packages %}
<li>
<a
class="package-option flex items-center justify-between"
data-package-url="{{ obj.url }}"
data-package-name="{{ obj.name }}"
>
<span>{{ obj.name }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="package-checkmark hidden h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
</a>
</li>
{% endfor %}
{% else %}
<li class="menu-title">Reviewed Packages</li>
{% for obj in reviewed_packages %}
<li>
<a
class="package-option flex items-center justify-between"
data-package-url="{{ obj.url }}"
data-package-name="{{ obj.name }}"
>
<span>{{ obj.name }}</span>
<svg
xmlns="http://www.w3.org/2000/svg"
class="package-checkmark hidden h-4 w-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 13l4 4L19 7"
/>
</svg>
</a>
</li>
{% endfor %}
{% endif %}
</ul>
</div>
<!-- Loading Indicator -->
<div id="search_loading" class="hidden shrink-0 justify-center py-8">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
<!-- Results Container - scrollable -->
<div id="search_results" class="min-h-0 flex-1 overflow-y-auto p-2"></div>
</div>
<!-- Backdrop to close -->
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>
<script>
(function () {
// Package Selector Module - Data-driven multiselect package selection
const PackageSelector = {
// Single source of truth: array of selected packages
selectedPackages: [],
elements: {
pillsContainer: null,
packageDropdown: null,
packageOptions: null,
},
init() {
this.cacheElements();
this.loadInitialSelection();
this.attachEventListeners();
this.render();
},
cacheElements() {
this.elements.pillsContainer = document.getElementById(
"modal_package_pills_container",
);
this.elements.packageDropdown = document.getElementById(
"package_dropdown_menu",
);
this.elements.packageOptions =
document.querySelectorAll(".package-option");
},
loadInitialSelection() {
// Load pre-selected packages from server-rendered pills
const existingPills =
this.elements.pillsContainer.querySelectorAll(".badge");
existingPills.forEach((pill) => {
this.selectedPackages.push({
url: pill.dataset.packageUrl,
name: pill.dataset.packageName,
});
});
// If no pills found, select all reviewed packages by default
if (this.selectedPackages.length === 0) {
// Iterate through all menu items and collect reviewed packages
const menuItems =
this.elements.packageDropdown.querySelectorAll("li");
for (const item of menuItems) {
// Check if this is the "Unreviewed Packages" menu title
if (
item.classList.contains("menu-title") &&
item.textContent.trim() === "Unreviewed Packages"
) {
break; // Stop processing after this point
}
// Check for package options (only reviewed packages reach here)
const packageOption = item.querySelector(".package-option");
if (packageOption) {
this.selectedPackages.push({
url: packageOption.dataset.packageUrl,
name: packageOption.dataset.packageName,
});
}
}
}
},
attachEventListeners() {
// Toggle package selection on dropdown item click
this.elements.packageOptions.forEach((option) => {
option.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation(); // Prevent dropdown from closing
const packageUrl = option.dataset.packageUrl;
const packageName = option.dataset.packageName;
this.togglePackageSelection(packageUrl, packageName);
});
});
// Remove package when X is clicked (using event delegation)
this.elements.pillsContainer.addEventListener("click", (e) => {
if (
e.target.classList.contains("package-remove-btn") ||
e.target.closest(".package-remove-btn")
) {
const pill = e.target.closest(".badge");
if (pill) {
const packageUrl = pill.dataset.packageUrl;
this.removePackage(packageUrl);
}
}
});
},
togglePackageSelection(packageUrl, packageName) {
const index = this.selectedPackages.findIndex(
(pkg) => pkg.url === packageUrl,
);
if (index !== -1) {
// Remove from selection
this.selectedPackages.splice(index, 1);
} else {
// Add to selection
this.selectedPackages.push({ url: packageUrl, name: packageName });
}
this.render();
},
removePackage(packageUrl) {
const index = this.selectedPackages.findIndex(
(pkg) => pkg.url === packageUrl,
);
if (index !== -1) {
this.selectedPackages.splice(index, 1);
this.render();
}
},
render() {
this.renderPills();
this.renderAddButton();
this.renderCheckmarks();
},
renderPills() {
// Clear existing pills and button (except placeholder)
const pills = this.elements.pillsContainer.querySelectorAll(".badge");
pills.forEach((pill) => pill.remove());
const existingButton = this.elements.pillsContainer.querySelector(
"#modal_package_add_button",
);
if (existingButton) {
existingButton.remove();
}
// Create pills from data
this.selectedPackages.forEach((pkg) => {
const pill = this.createPillElement(pkg.url, pkg.name);
this.elements.pillsContainer.appendChild(pill);
});
},
renderAddButton() {
// Only render button if there are packages available
if (this.elements.packageOptions.length === 0) {
return;
}
const button = document.createElement("button");
button.type = "button";
button.id = "modal_package_add_button";
button.setAttribute("popovertarget", "package_dropdown_menu");
button.style.cssText = "anchor-name:--anchor-packages";
button.className = "btn btn-sm btn-ghost gap-2 text-base-content/50";
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-plus-icon lucide-plus"><path d="M5 12h14"/><path d="M12 5v14"/></svg>
Add Package
`;
this.elements.pillsContainer.appendChild(button);
},
createPillElement(packageUrl, packageName) {
const pill = document.createElement("span");
pill.className = "badge badge-outline gap-2 max-w-xs";
pill.dataset.packageUrl = packageUrl;
pill.dataset.packageName = packageName;
pill.innerHTML = `
<span class="truncate" title="${packageName}">${packageName}</span>
<svg xmlns="http://www.w3.org/2000/svg"
class="h-4 w-4 cursor-pointer hover:text-error package-remove-btn flex-shrink-0 rotate-45"
viewBox="0 0 24 24"
fill="none" stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M5 12h14"/><path d="M12 5v14"/>
</svg>
`;
return pill;
},
renderCheckmarks() {
// Update all checkmarks based on selected packages
this.elements.packageOptions.forEach((option) => {
const packageUrl = option.dataset.packageUrl;
const isSelected = this.selectedPackages.some(
(pkg) => pkg.url === packageUrl,
);
const checkmark = option.querySelector(".package-checkmark");
if (checkmark) {
checkmark.classList.toggle("hidden", !isSelected);
}
});
},
getSelectedPackages() {
return this.selectedPackages.map((pkg) => pkg.url);
},
};
// Modal and Search Management
const modal = document.getElementById("search_modal");
const searchbar = document.getElementById("modal_searchbar");
const searchButton = document.getElementById("modal_search_button");
const modeButton = document.getElementById("modal_mode_button");
const resultsDiv = document.getElementById("search_results");
const loadingDiv = document.getElementById("search_loading");
// MutationObserver to detect when modal opens
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "open" && modal.open) {
PackageSelector.render();
// Delay focus to allow CSS transitions to complete (modal has 0.3s transition)
setTimeout(() => {
searchbar.focus();
}, 320);
}
});
});
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
modal.addEventListener("close", function () {
resultsDiv.innerHTML = "";
loadingDiv.classList.add("hidden");
searchbar.value = "";
});
// Mode dropdown handlers
const dropdownMenu = document.getElementById("search_dropdown_menu");
const modeButtons = [
{ id: "modal_dropdown_text", text: "Text" },
{ id: "modal_dropdown_smiles_default", text: "Default" },
{ id: "modal_dropdown_smiles_canonical", text: "Canonical" },
{ id: "modal_dropdown_smiles_exact", text: "Exact" },
{ id: "modal_dropdown_inchikey", text: "InChIKey" },
];
modeButtons.forEach(({ id, text }) => {
document.getElementById(id).addEventListener("click", function (e) {
e.preventDefault();
modeButton.innerHTML =
text +
` <svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
</svg>`;
// Close dropdown using popover API
if (dropdownMenu && typeof dropdownMenu.hidePopover === "function") {
dropdownMenu.hidePopover();
}
});
});
// Initialize Package Selector
PackageSelector.init();
// Search Response Handler
function handleSearchResponse(data) {
resultsDiv.innerHTML = "";
function makeContent(objs) {
let links = "";
objs.forEach((obj) => {
links += `<a href="${obj.url}" class="block px-4 py-2 hover:bg-base-300 rounded-lg transition-colors">${obj.name}</a>`;
});
return links;
}
let allEmpty = true;
let content = "";
// Category order for better UX
const categoryOrder = [
"Compounds",
"Compound Structures",
"Rules",
"Reactions",
"Pathways",
];
categoryOrder.forEach((key) => {
if (!data[key] || data[key].length < 1) {
return;
}
allEmpty = false;
content += `
<div class="collapse collapse-arrow bg-base-200 mb-2">
<input type="checkbox" checked />
<div class="collapse-title font-medium">
${key} <span class="badge badge-neutral badge-sm ml-2">${data[key].length}</span>
</div>
<div class="collapse-content">
${makeContent(data[key])}
</div>
</div>
`;
});
if (allEmpty) {
resultsDiv.innerHTML = `
<div class="alert alert-warning">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
</svg>
<span>No results found</span>
</div>
`;
} else {
resultsDiv.innerHTML = `
<div class="mb-2">
<div class="text-sm font-semibold text-base-content/70 mb-2">Results</div>
${content}
</div>
`;
}
}
// Search Execution
function performSearch(e) {
e.preventDefault();
const query = searchbar.value.trim();
if (!query) {
console.log("Search phrase empty, won't do search");
return;
}
const selPacks = PackageSelector.getSelectedPackages();
if (selPacks.length < 1) {
console.log("No package selected, won't do search");
resultsDiv.innerHTML = `
<div class="alert alert-info">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Please select at least one package</span>
</div>
`;
return;
}
const mode = modeButton.textContent.trim().toLowerCase();
const params = new URLSearchParams();
selPacks.forEach((pack) => params.append("packages", pack));
params.append("search", query);
params.append("mode", mode);
// Show loading
loadingDiv.classList.remove("hidden");
resultsDiv.innerHTML = "";
fetch(`{{ SERVER_BASE }}/search?${params.toString()}`, {
method: "GET",
headers: {
Accept: "application/json",
},
})
.then((response) => {
if (!response.ok) {
throw new Error("Search request failed");
}
return response.json();
})
.then((result) => {
loadingDiv.classList.add("hidden");
handleSearchResponse(result);
})
.catch((error) => {
loadingDiv.classList.add("hidden");
console.error("Search error:", error);
resultsDiv.innerHTML = `
<div class="alert alert-error">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Search failed. Please try again.</span>
</div>
`;
});
}
// Event listeners for search
searchButton.addEventListener("click", performSearch);
searchbar.addEventListener("keydown", function (e) {
if (e.key === "Enter") {
performSearch(e);
}
});
})();
</script>

View File

@ -1,110 +0,0 @@
<div class="modal fade bs-modal-sm" id="signupmodal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel"
aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<br>
<div class="bs-example bs-example-tabs">
<ul id="myTab" class="nav nav-tabs">
<li class="active">
<a href="#signin" data-toggle="tab">Sign In</a>
</li>
<li class="">
<a href="#signup" data-toggle="tab">Register</a>
</li>
<li class="">
<a href="#why" data-toggle="tab">Why?</a>
</li>
</ul>
</div>
<div class="modal-body">
<div id="myTabContent" class="tab-content">
<div class="tab-pane fade active in" id="signin">
<form class="form-horizontal" method="post" action="{% url 'login' %}">
{% csrf_token %}
<fieldset>
<input type="hidden" name="login" id="login" value="true"/>
<div class="control-group">
<label class="control-label" for="username">Username:</label>
<div class="controls">
<input required id="username" name="username" type="text" class="form-control"
placeholder="username" autocomplete="username">
</div>
<label class="control-label" for="passwordinput">Password:</label>
<div class="controls">
<input required id="passwordinput" name="password" class="form-control"
type="password" placeholder="********" autocomplete="current-password">
</div>
</div>
<!-- Button -->
<div class="control-group">
<label class="control-label" for="signin"></label>
<div class="controls">
<button id="signin" name="signin" class="btn btn-success">Sign In</button>
</div>
</div>
</fieldset>
</form>
</div>
<!-- Why tab -->
<div class="tab-pane fade in" id="why">
<p>After you register, you have more permissions on
this site, e.g., can create your own
packages, submit data for review, and set access
permissions to your data.</p>
<p></p>
<p>
<br> Please
contact <a href="mailto:admin@envipath.org">admin@envipath.org</a>
if you have any questions.</p>
</div>
<!-- Register -->
<div class="tab-pane fade"
id="signup">
<div id="passwordGuideline" class="alert alert-info">
The password must contain 8 to 30 characters<br>
The following characters are allowed:
- Upper and lower case characters<br>
- Digits<br>
- Special characters _, -, +<br>
</div>
<form id="signup-action" class="form-horizontal" action="{% url 'login' %}" method="post">
{% csrf_token %}
<input type="hidden" name="register" id="register" value="true"/>
<label class="control-label" for="userid">Username:</label>
<input id="userid" name="username" class="form-control" type="text" placeholder="user" required autocomplete="username">
<label class="control-label" for="email">Email:</label>
<input id="email" name="email" class="form-control" type="email" placeholder="user@envipath.org" required>
<label class="control-label" for="password">Password:</label>
<input id="password" name="password" class="form-control" type="password" placeholder="********" required autocomplete="new-password">
<label class="control-label" for="rpassword">Repeat Password:</label>
<input id="rpassword" name="rpassword" class="form-control" type="password" placeholder="********" required autocomplete="new-password">
<div class="control-group">
<label class="control-label" for="confirmsignup"></label>
<div class="controls">
<button id="confirmsignup" name="confirmsignup" class="btn btn-success">Sign Up
</button>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
<center>
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
</center>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,188 @@
{% extends "framework_modern.html" %}
{% load static %}
{% block content %}
<div class="mx-auto w-full p-8">
<h1 class="h1 mb-4 text-3xl font-bold">Predict a Pathway</h1>
<form
id="predict_form"
accept-charset="UTF-8"
action="{{ meta.current_package.url }}/pathway"
method="post"
>
{% csrf_token %}
<div class="mb-8 flex flex-col gap-8 md:flex-row md:items-end">
<fieldset class="flex flex-col gap-4 md:flex-3/4">
<label class="floating-label" for="name">
<input
type="text"
name="name"
placeholder="Name"
id="name"
class="input input-md w-full"
/>
<span>Name</span>
</label>
<label class="floating-label" for="description">
<input
type="text"
name="description"
placeholder="Description"
id="description"
class="input input-md w-full"
/>
<span>Description</span>
</label>
</fieldset>
<fieldset
class="fieldset flex shrink-0 flex-row items-start gap-3 md:flex-1/4 md:flex-col"
>
<label class="fieldset-label text-base-content/50">Mode</label>
<label class="label">
<input
type="radio"
name="predict"
id="radioPredict"
value="predict"
checked
class="radio radio-neutral"
/>
Predict
</label>
<label class="label">
<input
type="radio"
name="predict"
id="radioIncremental"
value="incremental"
class="radio radio-neutral"
/>
Incremental
</label>
<label class="label">
<input
type="radio"
name="predict"
id="radioBuild"
value="build"
class="radio radio-neutral"
/>
Build
</label>
</fieldset>
</div>
<label class="floating-label" for="predict-smiles">
<input
type="text"
name="smiles"
placeholder="SMILES"
id="predict-smiles"
class="input input-md w-full"
/>
<span>SMILES</span>
</label>
<div class="divider text-base-content/50">OR</div>
<div class="mb-8 w-full">
<label class="text-base-content/50 mb-4 text-xs font-medium"
>Draw Structure</label
>
<iframe
id="predict-ketcher"
src="{% static '/js/ketcher2/ketcher.html' %}"
width="100%"
height="510"
></iframe>
</div>
<label class="select mb-8 w-full">
<span class="label">Predictor</span>
<select id="prediction-setting" name="prediction-setting">
<option disabled>Select a Setting</option>
{% for s in meta.available_settings %}
<option
value="{{ s.url }}"
{% if s.id == meta.user.default_setting.id %}selected{% endif %}
>
{{ s.name }}{% if s.id == meta.user.default_setting.id %}
(User default)
{% endif %}
</option>
{% endfor %}
</select>
</label>
<div class="flex justify-end gap-2">
<a href="{{ meta.current_package.url }}/pathway" class="btn btn-outline"
>Cancel</a
>
<button
type="submit"
id="predict-submit-button"
class="btn btn-primary"
>
Predict
</button>
</div>
</form>
</div>
<script>
function predictKetcherToTextInput() {
$("#predict-smiles").val(this.ketcher.getSmiles());
}
$(function () {
$("#predict-ketcher").on("load", function () {
const checkKetcherReady = () => {
const win = this.contentWindow;
if (win.ketcher && "editor" in win.ketcher) {
window.predictKetcher = win.ketcher;
win.ketcher.editor.event.change.handlers.push({
once: false,
priority: 0,
f: predictKetcherToTextInput,
ketcher: win.ketcher,
});
} else {
setTimeout(checkKetcherReady, 100);
}
};
checkKetcherReady();
});
$("#predict-submit-button").on("click", function (e) {
e.preventDefault();
const button = $(this);
button.prop("disabled", true);
button.text("Predicting...");
// Get SMILES from either input or Ketcher
let smiles = $("#predict-smiles").val().trim();
// If SMILES input is empty, try to get from Ketcher
if (!smiles && window.predictKetcher) {
smiles = window.predictKetcher.getSmiles().trim();
if (smiles) {
$("#predict-smiles").val(smiles);
}
}
// Basic validation
if (!smiles) {
alert("Please enter a SMILES string or draw a structure.");
button.prop("disabled", false);
button.text("Predict");
return;
}
// Submit form
$("#predict_form").submit();
});
});
</script>
{% endblock content %}

View File

@ -1,197 +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>
<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>
{% endblock content %}

View File

@ -105,7 +105,7 @@
</div>
</div>
<form method="post" action="{% url 'register' %}" class="space-y-4">
<form method="post" action="{% url 'login' %}" class="space-y-4">
{% csrf_token %}
<input type="hidden" name="register" value="true" />

View File

@ -4,7 +4,14 @@ from django.test import TestCase, tag
from django.urls import reverse
from epdb.logic import UserManager
from epdb.models import Package, UserPackagePermission, Permission, GroupPackagePermission, Group
from epdb.models import (
Package,
UserPackagePermission,
Permission,
GroupPackagePermission,
Group,
License,
)
class PackageViewTest(TestCase):
@ -29,6 +36,15 @@ class PackageViewTest(TestCase):
add_to_group=True,
is_active=True,
)
# Create the default license set.
cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"]
for cc_string in cc_strings:
if not License.objects.filter(cc_string=cc_string).exists():
new_license = License()
new_license.cc_string = cc_string
new_license.link = f"https://creativecommons.org/licenses/{cc_string}/4.0/"
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
new_license.save()
def setUp(self):
self.client.force_login(self.user1)
@ -188,7 +204,28 @@ class PackageViewTest(TestCase):
self.client.post(package_url, {"license": "no-license"})
self.assertIsNone(p.license)
# TODO test others
# Test other possible licenses
cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"]
for cc_string in cc_strings:
self.client.post(package_url, {"license": cc_string})
# Without this, the instance of p doesn't have the license. However, the one retrieved with get does
p = Package.objects.get(url=package_url)
self.assertEqual(
p.license.link, f"https://creativecommons.org/licenses/{cc_string}/4.0/"
)
# Test again to ensure that Licenses are reused
cc_strings = ["by", "by-nc", "by-nc-nd", "by-nc-sa", "by-nd", "by-sa"]
for cc_string in cc_strings:
self.client.post(package_url, {"license": cc_string})
# Without this, the instance of p doesn't have the license. However, the one retrieved with get does
p = Package.objects.get(url=package_url)
self.assertEqual(
p.license.link, f"https://creativecommons.org/licenses/{cc_string}/4.0/"
)
self.assertEqual(License.objects.count(), len(cc_strings))
def test_delete_package(self):
response = self.client.post(

View File

@ -714,6 +714,7 @@ class PackageImporter:
license_obj, _ = License.objects.get_or_create(
name=license_data["name"],
defaults={
"cc_string": license_data.get("cc_string", ""),
"link": license_data.get("link", ""),
"image_link": license_data.get("image_link", ""),
},