14 Commits

Author SHA1 Message Date
ab43e0b5fe minor 2025-11-11 19:59:03 +01:00
91a52c254d minor 2025-11-11 19:57:50 +01:00
f7d88e4235 ..: 2025-11-11 19:26:23 +01:00
b627a3850b ... 2025-11-11 15:51:30 +01:00
1e5b8f5a63 ... 2025-11-11 15:47:03 +01:00
2a3e418f5b added missing migrations 2025-11-11 15:24:09 +01:00
393ec72523 add make migration step 2025-11-11 15:07:10 +01:00
932caa637b Merge branch 'develop' into enhancement/licenses 2025-11-12 03:00:54 +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
ac58d58479 Merge branch 'develop' into enhancement/licenses 2025-11-12 02:19:13 +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
94abfc90b5 comments 2025-11-10 14:57:31 +13:00
42b1e77fb9 refactor of license django model #119 2025-11-10 14:51:44 +13:00
23 changed files with 1943 additions and 842 deletions

View File

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

View File

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

View File

@ -21,6 +21,7 @@ from .models import (
ExternalDatabase, ExternalDatabase,
ExternalIdentifier, ExternalIdentifier,
JobLog, JobLog,
License,
) )
@ -62,6 +63,10 @@ class EnviFormerAdmin(EPAdmin):
pass pass
class LicenseAdmin(admin.ModelAdmin):
list_display = ["cc_string", "link", "image_link"]
class CompoundAdmin(EPAdmin): class CompoundAdmin(EPAdmin):
pass pass
@ -118,6 +123,7 @@ admin.site.register(JobLog, JobLogAdmin)
admin.site.register(Package, PackageAdmin) admin.site.register(Package, PackageAdmin)
admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin) admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin)
admin.site.register(EnviFormer, EnviFormerAdmin) admin.site.register(EnviFormer, EnviFormerAdmin)
admin.site.register(License, LicenseAdmin)
admin.site.register(Compound, CompoundAdmin) admin.site.register(Compound, CompoundAdmin)
admin.site.register(CompoundStructure, CompoundStructureAdmin) admin.site.register(CompoundStructure, CompoundStructureAdmin)
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin) 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, Permission,
User, User,
ExternalDatabase, ExternalDatabase,
License,
) )
class Command(BaseCommand): 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): def create_users(self):
# Anonymous User # Anonymous User
if not User.objects.filter(email="anon@envipath.com").exists(): if not User.objects.filter(email="anon@envipath.com").exists():
@ -83,6 +89,17 @@ class Command(BaseCommand):
return anon, admin, g, user0 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): def import_package(self, data, owner):
return PackageManager.import_legacy_package( return PackageManager.import_legacy_package(
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
@ -157,6 +174,10 @@ class Command(BaseCommand):
@transaction.atomic @transaction.atomic
def handle(self, *args, **options): def handle(self, *args, **options):
# Create licenses
self.create_licenses()
if options.get("only_licenses", False):
return
# Create users # Create users
anon, admin, g, user0 = self.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): 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") link = models.URLField(blank=False, null=False, verbose_name="link")
image_link = models.URLField(blank=False, null=False, verbose_name="Image link") image_link = models.URLField(blank=False, null=False, verbose_name="Image link")

View File

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

View File

@ -362,6 +362,18 @@ def index(request):
return render(request, "index/index.html", context) 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): def packages(request):
current_user = _anonymous_or_real(request) current_user = _anonymous_or_real(request)
@ -682,7 +694,6 @@ def search(request):
if request.method == "GET": if request.method == "GET":
package_urls = request.GET.getlist("packages") package_urls = request.GET.getlist("packages")
searchterm = request.GET.get("search", "").strip() searchterm = request.GET.get("search", "").strip()
mode = request.GET.get("mode") mode = request.GET.get("mode")
# add HTTP_ACCEPT check to differentiate between index and ajax call # 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": elif request.method == "POST":
log_post_params(request) log_post_params(request)
name = request.POST.get("model-name") name = request.POST.get("model-name")
description = request.POST.get("model-description") description = request.POST.get("model-description")
@ -1072,9 +1084,7 @@ def package(request, package_uuid):
write = request.POST.get("write") == "on" write = request.POST.get("write") == "on"
owner = request.POST.get("owner") == "on" owner = request.POST.get("owner") == "on"
license = request.POST.get("license") cc_string = request.POST.get("license")
license_link = request.POST.get("license-link")
license_image_link = request.POST.get("license-image-link")
if new_package_name: if new_package_name:
current_package.name = new_package_name current_package.name = new_package_name
@ -1102,24 +1112,15 @@ def package(request, package_uuid):
PackageManager.update_permissions(current_user, current_package, grantee, max_perm) PackageManager.update_permissions(current_user, current_package, grantee, max_perm)
return redirect(current_package.url) 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.license = None
current_package.save() current_package.save()
return redirect(current_package.url) return redirect(current_package.url)
else: else: # Get the license and assign it to the package
if current_package.license is not None: current_package.license = License.objects.get(cc_string=cc_string)
current_package.license.delete()
license = License()
license.link = license_link
license.image_link = license_image_link
license.save()
current_package.license = license
current_package.save() current_package.save()
return redirect(current_package.url) return redirect(current_package.url)
@ -1174,7 +1175,7 @@ def package_compounds(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
compound_name = request.POST.get("compound-name") 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") compound_description = request.POST.get("compound-description")
c = Compound.create(current_package, compound_smiles, compound_name, compound_description) c = Compound.create(current_package, compound_smiles, compound_name, compound_description)
@ -1301,7 +1302,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
elif request.method == "POST": elif request.method == "POST":
structure_name = request.POST.get("structure-name") 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") structure_description = request.POST.get("structure-description")
try: try:
@ -1483,11 +1484,11 @@ def package_rules(request, package_uuid):
# Obtain parameters as required by rule type # Obtain parameters as required by rule type
if rule_type == "SimpleAmbitRule": 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["reactant_filter_smarts"] = request.POST.get("rule-reactant-smarts")
params["product_filter_smarts"] = request.POST.get("rule-product-smarts") params["product_filter_smarts"] = request.POST.get("rule-product-smarts")
elif rule_type == "SimpleRDKitRule": 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": elif rule_type == "ParallelRule":
pass pass
elif rule_type == "SequentialRule": elif rule_type == "SequentialRule":
@ -1684,8 +1685,7 @@ def package_reactions(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
reaction_name = request.POST.get("reaction-name") reaction_name = request.POST.get("reaction-name")
reaction_description = request.POST.get("reaction-description") reaction_description = request.POST.get("reaction-description")
reactions_smirks = request.POST.get("reaction-smirks")
reactions_smirks = request.POST.get("reaction-smirks").strip()
educts = reactions_smirks.split(">>")[0].split(".") educts = reactions_smirks.split(">>")[0].split(".")
products = reactions_smirks.split(">>")[1].split(".") products = reactions_smirks.split(">>")[1].split(".")
@ -2259,6 +2259,7 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
elif request.method == "POST": elif request.method == "POST":
log_post_params(request) log_post_params(request)
edge_name = request.POST.get("edge-name") edge_name = request.POST.get("edge-name")
edge_description = request.POST.get("edge-description") edge_description = request.POST.get("edge-description")

View File

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

View File

@ -1,6 +1,6 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <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> <span class="glyphicon glyphicon-plus"></span> New Pathway</a>
</li> </li>
{% endif %} {% endif %}

View File

@ -1,11 +1,15 @@
<!DOCTYPE html> <!doctype html>
<html data-theme="envipath"> <html data-theme="envipath">
{% load static %} {% load static %}
<head> <head>
<title>{{ title }}</title> <title>{{ title }}</title>
<meta name="csrf-token" content="{{ csrf_token }}"> <meta name="csrf-token" content="{{ csrf_token }}" />
{# Favicon #} {# 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 #} {# Tailwind CSS disabled for legacy Bootstrap framework #}
{# Pages using this framework will be migrated to framework_modern.html incrementally #} {# 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 #} {# 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> <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://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> <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
<link rel="stylesheet" href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css"
href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.7.3/css/bootstrap-select.min.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://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> <script src="https://community.envipath.org/javascripts/embed-topics.js"></script>
<!-- CDN END --> <!-- CDN END -->
{# Bootstrap compatibility styles #} {# Bootstrap compatibility styles #}
<style> <style>
/* Ensure proper viewport behavior */ /* Ensure proper viewport behavior */
html, body { html,
height: 100%; /* ensure body fills viewport */ body {
overflow-x: hidden; /* prevent horizontal scroll */ height: 100%; /* ensure body fills viewport */
} overflow-x: hidden; /* prevent horizontal scroll */
}
</style> </style>
<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 // Setup CSRF header for all jQuery AJAX requests
$.ajaxSetup({ $.ajaxSetup({
beforeSend: function(xhr, settings) { beforeSend: function (xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) { if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken); xhr.setRequestHeader("X-CSRFToken", csrftoken);
} }
} },
}); });
</script> </script>
<!-- {# C3 CSS #}--> <!-- {# C3 CSS #}-->
@ -50,40 +63,44 @@
<!-- {# EP CSS #}--> <!-- {# EP CSS #}-->
<!-- <link id="css-pps_white_general" href="{% static 'css/epp.css' %}" rel="stylesheet" type="text/css"/>--> <!-- <link id="css-pps_white_general" href="{% static 'css/epp.css' %}" rel="stylesheet" type="text/css"/>-->
{# General EP JS #} {# General EP JS #}
<script src="{% static 'js/pps.js' %}"></script> <script src="{% static 'js/pps.js' %}"></script>
{# Modal Steps for Stepwise Modal Wizards #} {# Modal Steps for Stepwise Modal Wizards #}
<script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script> <script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script>
{% if not debug %} {% if not debug %}
<!-- Matomo --> <!-- Matomo -->
<script> <script>
var _paq = window._paq = window._paq || []; var _paq = (window._paq = window._paq || []);
/* tracker methods like "setCustomDimension" should be called before "trackPageView" */ /* tracker methods like "setCustomDimension" should be called before "trackPageView" */
_paq.push(['trackPageView']); _paq.push(["trackPageView"]);
_paq.push(['enableLinkTracking']); _paq.push(["enableLinkTracking"]);
(function () { (function () {
var u = "//matomo.envipath.com/"; var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']); _paq.push(["setTrackerUrl", u + "matomo.php"]);
_paq.push(['setSiteId', '{{ meta.site_id }}']); _paq.push(["setSiteId", "{{ meta.site_id }}"]);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; var d = document,
g.async = true; g = d.createElement("script"),
g.src = u + 'matomo.js'; s = d.getElementsByTagName("script")[0];
s.parentNode.insertBefore(g, s); g.async = true;
})(); g.src = u + "matomo.js";
</script> s.parentNode.insertBefore(g, s);
<!-- End Matomo Code --> })();
</script>
<!-- End Matomo Code -->
{% endif %} {% endif %}
</head>
</head> <body>
<body> <!-- Legacy Bootstrap navbar - isolated from Tailwind -->
<!-- Legacy Bootstrap navbar - isolated from Tailwind --> <div class="legacy-bootstrap">
<div class="legacy-bootstrap"> <nav
<nav class="navbar navbar-default navbar-inverse" style="border-radius:0px;" role="navigation"> class="navbar navbar-default navbar-inverse"
<div class="container-fluid"> style="border-radius:0px;"
<!-- Brand and toggle get grouped for better mobile display --> role="navigation"
<div class="navbar-header navbar-header-framework"> >
<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"--> <!-- <button type="button" class="navbar-toggle navbar-toggle-framework" data-toggle="collapse"-->
<!-- data-target="#navbarCollapse">--> <!-- data-target="#navbarCollapse">-->
<!-- <span class="sr-only">Toggle navigation</span>--> <!-- <span class="sr-only">Toggle navigation</span>-->
@ -91,204 +108,315 @@
<!-- <span class="icon-bar"></span>--> <!-- <span class="icon-bar"></span>-->
<!-- <span class="icon-bar"></span>--> <!-- <span class="icon-bar"></span>-->
<!-- </button>--> <!-- </button>-->
<a id="pictureLink" href="{{ meta.server_url }}" class="navbar-brand"> <a
<img id="image-logo-short-white.svg" src='{% static "/images/logo-short-white.svg" %}' width="100" id="pictureLink"
alt="enviPath"> 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> </a>
</div> </div>
<!-- Collect the nav links, forms, and other content for toggling --> <!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse collapse-framework navbar-collapse-framework" id="navbarCollapse"> <div
class="collapse navbar-collapse collapse-framework navbar-collapse-framework"
id="navbarCollapse"
>
<ul class="nav navbar-nav navbar-nav-framework"> <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> <li>
<a href="#" data-toggle="modal" data-target="#predict_modal"> <a
Predict Pathway href="{% url 'login' %}"
</a> id="loginButton"
style="margin-right:10px"
>Login</a
>
</li> </li>
{# <li class="dropdown">#} {% else %}
{# <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"> <li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Browse Data<b class="caret"></b></a> <a
<ul role="menu" class="dropdown-menu"> data-toggle="dropdown"
<li><a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a></li> id="loggedInButton"
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li> class="dropdown-toggle"
<li><a href="{{ meta.server_url }}/compound" id="compoundLink">Compound</a></li> id="logedInButton"
<li><a href="{{ meta.server_url }}/reaction" id="reactionLink">Reaction</a></li> href="#"
<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> <div id="username">
{# <li><a href="{{ meta.server_url }}/setting" id="settingLink">Setting</a></li>#} {{ user.username }}<b class="caret"></b>
{# <li><a href="{{ meta.server_url }}/user" id="userLink">User</a></li>#} </div>
{# <li><a href="{{ meta.server_url }}/group" id="groupLink">Group</a></li>#} </a>
</ul> <ul role="menu" class="dropdown-menu">
</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> <li>
<a href="{% url 'login' %}" id="loginButton" style="margin-right:10px">Login</a> <a href="{{ meta.user.url }}" id="accountbutton"
>My Account</a
>
</li> </li>
{% else %} <li class="divider"></li>
<li class="dropdown"> <form
<a data-toggle="dropdown" id="loggedInButton" class="dropdown-toggle" id="logedInButton" class="navbar-form navbar-left navbar-left-framework"
href="#"> role="logout"
<div id="username"> action="{% url 'logout' %}"
{{ user.username }}<b class="caret"></b> method="post"
</div> >
</a> {% csrf_token %}
<ul role="menu" class="dropdown-menu"> <div class="form-group">
<li> <input type="hidden" name="logout" value="true" />
<a href="{{ meta.user.url }}" id="accountbutton">My Account</a> </div>
</li> <button type="submit" class="btn btn-default">
<li class="divider"></li> Logout
<form class="navbar-form navbar-left navbar-left-framework" role="logout" </button>
action="{% url 'logout' %}" method="post"> </form>
{% csrf_token %} </ul>
<div class="form-group"> </li>
<input type="hidden" name="logout" value="true"> {% endif %}
</div>
<button type="submit" class="btn btn-default">Logout</button>
</form>
</ul>
</li>
{% endif %}
</ul> </ul>
</div>
</div> </div>
</nav>
</div> </div>
</nav> <!-- End legacy Bootstrap navbar -->
</div>
<!-- End legacy Bootstrap navbar -->
<div id="docContent" class="content container"> <div id="docContent" class="content container">
{% if breadcrumbs %} {% if breadcrumbs %}
<div id="bread"> <div id="bread">
<ol class="breadcrumb"> <ol class="breadcrumb">
{% for elem in breadcrumbs %} {% for elem in breadcrumbs %}
{% for name, url in elem.items %} {% for name, url in elem.items %}
{% if forloop.parentloop.last %} {% if forloop.parentloop.last %}
<li class="active">{{ name }}</li> <li class="active">{{ name }}</li>
{% else %} {% else %}
<li> <li>
<a href="{{ url }}">{{ name }}</a> <a href="{{ url }}">{{ name }}</a>
</li> </li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</ol> </ol>
</div> </div>
{% endif %} {% endif %}
{% if message %} {% if message %}
<div id="message"> <div id="message">{{ message }}</div>
{{ message }} {% endif %}
</div> {% block content %}
{% endif %} {% endblock content %}
{% block content %} {% if meta.url_contains_package and meta.current_package.license %}
{% endblock content %}
{% if meta.url_contains_package and meta.current_package.license %}
<p></p> <p></p>
<div class="panel-group" id="license_accordion"> <div class="panel-group" id="license_accordion">
<div class="panel panel-default list-group-item" style="background-color:#f5f5f5"> <div
<div class="panel-title"> class="panel panel-default list-group-item"
<a data-toggle="collapse" data-parent="#licence_accordion" href="#license">License</a> style="background-color:#f5f5f5"
</div> >
<div class="panel-title">
<a
data-toggle="collapse"
data-parent="#licence_accordion"
href="#license"
>License</a
>
</div> </div>
<div id="license" class="panel-collapse collapse in"> </div>
<div class="panel-body list-group-item"> <div id="license" class="panel-collapse collapse in">
<a target="_blank" href="{{ meta.current_package.license.link }}"> <div class="panel-body list-group-item">
<img src="{{ meta.current_package.license.image_link }}"> <a target="_blank" href="{{ meta.current_package.license.link }}">
</a> <img src="{{ meta.current_package.license.image_link }}" />
</div> </a>
</div> </div>
</div>
</div> </div>
{% endif %} {% 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>
</div> </div>
</div>
<div class="row"> <!-- FOOTER - Legacy Bootstrap -->
<div class="col-lg-12"> <div class="legacy-bootstrap">
<ul class="nav nav-pills nav-justified"> <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="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;--> <!-- <li><a href="http://envipath.com" target="_blank"> enviPath UG (haftungsbeschr&auml;nkt) &amp; Co. KG &copy;-->
<!-- {{ YEAR }}</a></li>--> <!-- {{ YEAR }}</a></li>-->
</ul> </ul>
</div>
</div>
</div> </div>
</div> <!-- End legacy Bootstrap footer -->
</div>
<!-- End legacy Bootstrap footer -->
<script> <script>
$(function () { $(function () {
// Hide actionsbutton if theres no action defined // Hide actionsbutton if theres no action defined
if ($('#actionsButton ul').children().length > 0) { if ($("#actionsButton ul").children().length > 0) {
$('#actionsButton').show(); $("#actionsButton").show();
} }
}); });
</script> </script>
{% block modals %} {% block modals %}
{% include "modals/cite_modal.html" %} {% include "modals/cite_modal.html" %}
{% include "modals/signup_modal.html" %} {% include "modals/predict_modal.html" %}
{% include "modals/predict_modal.html" %} {% include "modals/batch_predict_modal.html" %}
{% include "modals/batch_predict_modal.html" %} {% endblock %}
{% endblock %} </body>
</body>
</html> </html>

View File

@ -1,38 +1,49 @@
<!DOCTYPE html> <!doctype html>
<html data-theme="envipath" lang="en"> <html data-theme="envipath" lang="en">
{% load static %} {% load static %}
<head> <head>
<title>{{ title }}</title> <title>{{ title }}</title>
<meta charset="UTF-8"> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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 #} {# 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 #} {# 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 #} {# jQuery - Keep for compatibility with existing JS #}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
{# Font Awesome #} {# 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 #} {# Discourse embed for community #}
<script src="https://community.envipath.org/javascripts/embed-topics.js"></script> <script src="https://community.envipath.org/javascripts/embed-topics.js"></script>
<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 // Setup CSRF header for all jQuery AJAX requests
$.ajaxSetup({ $.ajaxSetup({
beforeSend: function(xhr, settings) { beforeSend: function (xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) { if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken); xhr.setRequestHeader("X-CSRFToken", csrftoken);
} }
} },
}); });
</script> </script>
{# General EP JS #} {# General EP JS #}
@ -41,136 +52,155 @@
<script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script> <script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script>
{% if not debug %} {% if not debug %}
<!-- Matomo --> <!-- Matomo -->
<script> <script>
var _paq = window._paq = window._paq || []; var _paq = (window._paq = window._paq || []);
_paq.push(['trackPageView']); _paq.push(["trackPageView"]);
_paq.push(['enableLinkTracking']); _paq.push(["enableLinkTracking"]);
(function () { (function () {
var u = "//matomo.envipath.com/"; var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']); _paq.push(["setTrackerUrl", u + "matomo.php"]);
_paq.push(['setSiteId', '{{ meta.site_id }}']); _paq.push(["setSiteId", "{{ meta.site_id }}"]);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0]; var d = document,
g.async = true; g = d.createElement("script"),
g.src = u + 'matomo.js'; s = d.getElementsByTagName("script")[0];
s.parentNode.insertBefore(g, s); g.async = true;
})(); g.src = u + "matomo.js";
</script> s.parentNode.insertBefore(g, s);
<!-- End Matomo Code --> })();
</script>
<!-- End Matomo Code -->
{% endif %} {% endif %}
</head> </head>
<body class="min-h-screen bg-base-300"> <body class="bg-base-300 min-h-screen">
{% include "includes/navbar.html" %} {% include "includes/navbar.html" %}
{# Main Content Area #} {# Main Content Area #}
<main class="w-full"> <main class="w-full">
{% block main_content %} {% block main_content %}
{# Breadcrumbs - outside main content, optional #} {# Breadcrumbs - outside main content, optional #}
{% if breadcrumbs %} {% if breadcrumbs %}
<div id="bread" class="max-w-7xl mx-auto px-4 py-4"> <div id="bread" class="max-w-7xl mx-auto px-4 py-4">
<div class="text-sm breadcrumbs"> <div class="breadcrumbs text-sm">
<ul> <ul>
{% for elem in breadcrumbs %} {% for elem in breadcrumbs %}
{% for name, url in elem.items %} {% for name, url in elem.items %}
{% if forloop.parentloop.last %} {% if forloop.parentloop.last %}
<li>{{ name }}</li> <li>{{ name }}</li>
{% else %} {% else %}
<li><a href="{{ url }}">{{ name }}</a></li> <li><a href="{{ url }}">{{ name }}</a></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
</ul> </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 %}
</div> </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> </main>
{% include "includes/footer.html" %} {% include "includes/footer.html" %}
{# Floating Help Tab #} {# Floating Help Tab #}
{% if not public_mode %} {% if not public_mode %}
<div class="fixed right-0 top-1/2 -translate-y-1/2 z-50"> <div class="fixed right-0 top-1/2 -translate-y-1/2 z-50">
<a href="https://community.envipath.org/" target="_blank" <a
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" href="https://community.envipath.org/"
title="Get Help from the Community"> target="_blank"
<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"/> 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"
<path d="M12 17h.01"/> title="Get Help from the Community"
</svg> >
<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> </a>
</div> </div>
{% endif %} {% endif %}
{# Modals - TODO: Convert these to DaisyUI modals #}
{% block modals %} {% block modals %}
{# Note: These modals still use Bootstrap markup and will need conversion #} {% include "modals/search_modal.html" %}
{% include "modals/cite_modal.html" %}
{% include "modals/signup_modal.html" %}
{% include "modals/predict_modal.html" %}
{% include "modals/batch_predict_modal.html" %}
{% endblock %} {% endblock %}
<script> <script>
$(function () { $(function () {
// Hide actionsbutton if there's no action defined // Hide actionsbutton if there's no action defined
if ($('#actionsButton ul').children().length > 0) { if ($("#actionsButton ul").children().length > 0) {
$('#actionsButton').show(); $("#actionsButton").show();
} }
});
}); // 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) if (isInputField) {
document.addEventListener('keydown', function(event) { return; // Don't trigger shortcut when typing in input fields
// 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) { // Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux)
return; // Don't trigger shortcut when typing in input fields const isMac = /Mac/.test(navigator.platform);
} const isCorrectModifier = isMac ? event.metaKey : event.ctrlKey;
// Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux) if (isCorrectModifier && event.key === "k") {
const isMac = /Mac/.test(navigator.platform); event.preventDefault();
const isCorrectModifier = isMac ? event.metaKey : event.ctrlKey; search_modal.showModal();
}
if (isCorrectModifier && event.key === 'k') { });
event.preventDefault();
window.location.href = '/search';
}
});
</script> </script>
</body> </body>
</html> </html>

View File

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

View File

@ -1,190 +1,269 @@
{% extends "framework_modern.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block main_content %} {% block main_content %}
<!-- Hero Section with Logo and Search -->
<!-- Hero Section with Logo and Search --> <section class="hero h-fit max-w-5xl w-full shadow-none mx-auto relative">
<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"
<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;"
style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;" >
> <div class="hero-overlay"></div>
<div class="hero-overlay"></div> <!-- Predict Pathway text over the image -->
<!-- Predict Pathway text over the image --> <div class="absolute bottom-40 left-1/8 -translate-x-8 z-10">
<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">
<h2 class="text-3xl text-base-100 text-shadow-lg text-left">Predict Your Pathway</h2> Predict Your Pathway
</div> </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> </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 --> <!-- Community News Section -->
<section class="py-16 bg-base-200 z-10 mx-8"> <section class="py-16 bg-base-200 z-10 mx-8">
<div class="max-w-7xl mx-auto px-4"> <div class="max-w-7xl mx-auto px-4">
<h2 class="h2 font-bold text-left mb-8">Community Updates</h2> <h2 class="h2 font-bold text-left mb-8">Community Updates</h2>
<div id="community-news-container" class="flex gap-4 justify-center"> <div id="community-news-container" class="flex gap-4 justify-center">
<!-- News cards will be populated here --> <!-- News cards will be populated here -->
<div id="loading" class="flex justify-center w-full"> <div id="loading" class="flex justify-center w-full">
<span class="loading loading-spinner loading-lg"></span> <span class="loading loading-spinner loading-lg"></span>
</div> </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> </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> </section>
<!-- Mission Statement Section --> <!-- Mission Statement Section -->
<section class="py-16 from-base-200 to-base-100 bg-gradient-to-b"> <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="mx-auto px-8 md:px-12">
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<div class="w-1/3"> <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" /> <img
</div> src="{% static "/images/ep-rule-artwork.png" %}"
<div class="space-y-4 text-left w-2/3 mr-8"> alt="rule-based iterative tree greneration"
<h2 class="h2 font-bold mb-8">About enviPath</h2> class="w-full h-full object-contain"
<p class=""> />
enviPath is a database and prediction system for the microbial </div>
biotransformation of organic environmental contaminants. The <div class="space-y-4 text-left w-2/3 mr-8">
database provides the possibility to store and view experimentally <h2 class="h2 font-bold mb-8">About enviPath</h2>
observed biotransformation pathways. <p class="">
</p> enviPath is a database and prediction system for the microbial
<p class=""> biotransformation of organic environmental contaminants. The
The pathway prediction system provides different relative reasoning models database provides the possibility to store and view experimentally
to predict likely biotransformation pathways and products. Explore our tools observed biotransformation pathways.
and contribute to advancing environmental biotransformation research. </p>
</p> <p class="">
<div class="flex flex-row gap-4 float-right"> The pathway prediction system provides different relative
<a href="/about" class="btn btn-ghost-neutral">Read More</a> reasoning models to predict likely biotransformation pathways and
<a href="/about" class="btn btn-neutral">Publications</a> products. Explore our tools and contribute to advancing
</div> environmental biotransformation research.
</div> </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> </div>
</div>
</section> </section>
<!-- Partners Section --> <!-- Partners Section -->
<section class="py-14 sm:py-12 bg-base-100"> <section class="py-14 sm:py-12 bg-base-100">
<div class="mx-auto px-6 lg:px-8"> <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="divider">
<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"> <h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2>
<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> </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> </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"> // Create cards for each topic
var currentPackage = "{{ meta.current_package.url }}"; 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 return `
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 `
<div class="card bg-white shadow-xs hover:shadow-lg transition-shadow duration-300 h-64 w-75 flex-shrink-0"> <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"> <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"> <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>
</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 // Show a brief feedback
window.renderDiscourseTopics = renderDiscourseTopics; const originalText = $(this).text();
$(this).text(`loaded!`);
setTimeout(() => {
$(this).text(originalText);
}, 1000);
});
// Toggle functionality with smooth animations // Handle form submission on Enter
function toggleInputMode() { $("#index-form").on("submit", function (e) {
const toggle = $('input[type="checkbox"]'); e.preventDefault();
const textContainer = $('#text-input-container');
const ketcherContainer = $('#ketcher-container');
const formCard = $('.card');
const fieldset = $('.fieldset');
if (toggle.is(':checked')) { var textSmiles = "";
// 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 // Check if we're in Ketcher mode and extract SMILES
fieldset.removeClass('p-8'); if ($('input[type="checkbox"]').is(":checked") && window.indexKetcher) {
fieldset.addClass('p-4'); textSmiles = window.indexKetcher.getSmiles().trim();
} else {
// Wait for fade out to complete, then hide and show new content textSmiles = $("#index-form-text-input").val().trim();
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 if (textSmiles === "") {
function indexKetcherToTextInput() { return;
$('#index-form-smiles').val(this.ketcher.getSmiles());
} }
$(function () { $("#index-form-smiles").val(textSmiles);
// Initialize fieldset with proper padding $("#index-form").attr("action", currentPackage + "/pathway");
$('.fieldset').addClass('p-8'); $("#index-form").attr("method", "POST");
this.submit();
});
// Toggle event listener // Discourse topics are now loaded automatically by discourse-api.js
$('input[type="checkbox"]').on('change', toggleInputMode); });
</script>
// 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>
{% endblock main_content %} {% endblock main_content %}

View File

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

View File

@ -0,0 +1,535 @@
{% load static %}
<dialog id="search_modal" class="modal @max-sm:modal-top justify-center">
<div class="modal-box w-lvw sm:w-[85vw] sm:max-w-5xl h-full sm:h-8/12 p-1" >
<!-- Search Input and Mode Selector -->
<div class="form-control mb-4 flex-shrink-0 w-full">
<div class="join w-full m-0 p-3 items-center">
<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:--anchor-1"
class="btn join-item btn-ghost">
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>
</button>
<ul tabindex="0"" class="dropdown dropdown-end menu bg-base-200 rounded-box z-[100] w-64 p-2 shadow-lg" popover id="search_dropdown_menu" style="position-anchor:--anchor-1">
<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 p-1 text-base-content/50">
<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 flex-shrink-0">
<!-- Pills Container -->
<div id="modal_package_pills_container"
class="flex flex-wrap gap-2 p-3 border-2 border-dashed border-base-300 rounded-lg m-3 min-h-[3rem] items-center">
<!-- Pills will be added here dynamically -->
</div>
<!-- Package Dropdown Menu -->
<ul class="dropdown dropdown-end menu bg-base-200 rounded-box z-[100] w-80 max-h-96 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 justify-between items-center"
data-package-url="{{ obj.url }}"
data-package-name="{{ obj.name }}">
<span>{{ obj.name }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 package-checkmark hidden" 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 justify-between items-center"
data-package-url="{{ obj.url }}"
data-package-name="{{ obj.name }}">
<span>{{ obj.name }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 package-checkmark hidden" 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 justify-between items-center"
data-package-url="{{ obj.url }}"
data-package-name="{{ obj.name }}">
<span>{{ obj.name }}</span>
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 package-checkmark hidden" 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 justify-center py-8 flex-shrink-0">
<span class="loading loading-spinner loading-lg text-primary"></span>
</div>
<!-- Results Container - scrollable -->
<div id="search_results" class="flex-1 overflow-y-auto min-h-0 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 });
// 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

@ -105,7 +105,7 @@
</div> </div>
</div> </div>
<form method="post" action="{% url 'register' %}" class="space-y-4"> <form method="post" action="{% url 'login' %}" class="space-y-4">
{% csrf_token %} {% csrf_token %}
<input type="hidden" name="register" value="true" /> <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 django.urls import reverse
from epdb.logic import UserManager 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): class PackageViewTest(TestCase):
@ -29,6 +36,15 @@ class PackageViewTest(TestCase):
add_to_group=True, add_to_group=True,
is_active=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): def setUp(self):
self.client.force_login(self.user1) self.client.force_login(self.user1)
@ -188,7 +204,28 @@ class PackageViewTest(TestCase):
self.client.post(package_url, {"license": "no-license"}) self.client.post(package_url, {"license": "no-license"})
self.assertIsNone(p.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): def test_delete_package(self):
response = self.client.post( response = self.client.post(

View File

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