8 Commits

Author SHA1 Message Date
d12b908014 Merge branch 'develop' into fix/frontpage_predict 2025-11-12 20:54:27 +13:00
2799718951 fix: open and close search modal (#192)
Modal now opens on badge click.
Modal now closes on random click around

Reviewed-on: enviPath/enviPy#192
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 20:53:52 +13:00
25c10bbf77 refactor: add tooltip 2025-11-12 16:24:35 +13:00
c1ca734e81 refactor: change input toggle style 2025-11-12 16:22:02 +13:00
e5d2bc596b refactor: move Advanced to right 2025-11-12 16:01:31 +13:00
305fdc41fb [Fix] Replace datetime.now() with Djangos timezone.now() to get rid of NaiveTimestamp warning (#191)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#191
2025-11-12 11:04:00 +13:00
9deca8867e [Feature] Possibility to Retrain Models (#190)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#190
2025-11-12 10:28:35 +13:00
df6056fb86 [Enhancement] Refactor of license django model (#187)
Fixes #119

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

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Co-authored-by: jebus <lorsbach@envipath.com>
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#187
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-11-12 08:20:43 +13:00
17 changed files with 857 additions and 726 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

@ -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

@ -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

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

View File

@ -955,6 +955,12 @@ def package_model(request, package_uuid, model_uuid):
] ]
dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids) dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids)
return redirect(current_model.url)
elif hidden == "retrain":
from .tasks import dispatch, retrain
dispatch(current_user, retrain, current_model.pk)
return redirect(current_model.url) return redirect(current_model.url)
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -1084,9 +1090,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
@ -1114,24 +1118,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)

View File

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

View File

@ -57,7 +57,7 @@
<div class="navbar-end"> <div class="navbar-end">
{% if not public_mode %} {% if not public_mode %}
<a href="/search" role="button"> <a id="search-trigger" role="button" class="cursor-pointer">
<div <div
class="flex items-center badge badge-dash space-x-1 bg-base-200 text-base-content/50 p-2 m-1" class="flex items-center badge badge-dash space-x-1 bg-base-200 text-base-content/50 p-2 m-1"
> >

View File

@ -2,74 +2,86 @@
{% 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 relative mx-auto h-fit w-full max-w-5xl shadow-none">
<div <div
class="hero min-h-[calc(100vh*0.4)] bg-gradient-to-br from-primary-800 to-primary-600" class="hero from-primary-800 to-primary-600 min-h-[calc(100vh*0.4)] bg-gradient-to-br"
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 z-10 -translate-x-8">
<h2 class="text-3xl text-base-100 text-shadow-lg text-left"> <h2 class="text-base-100 text-left text-3xl text-shadow-lg">
Predict Your Pathway Predict Your Pathway
</h2> </h2>
</div> </div>
</div> </div>
</section> </section>
<div class="shadow-md max-w-5xl mx-auto bg-base-200"> <div class="bg-base-200 mx-auto max-w-5xl shadow-md">
<!-- Predict Pathway Section --> <!-- Predict Pathway Section -->
<div <div
class="flex-col lg:flex-row-reverse w-full mx-auto -mt-32 relative z-20 mb-10 " class="relative z-20 mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
> >
<div <div
class="card bg-base-100 shrink-0 shadow-xl w-3/4 mx-auto transition-all duration-300 ease-in-out" class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out"
> >
<div class="card-body"> <div class="card-body">
<!-- Input Mode Toggle - Fixed position outside fieldset --> <div class="my-4 ml-8 flex h-fit flex-row items-center justify-start">
<div class="flex flex-row justify-start items-center h-fit ml-8 my-4"> <div class="flex items-center gap-1">
<div class="flex items-center gap-2"> <label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
<!-- <span class="text-sm text-neutral-500">Input Mode:</span> -->
<label class="toggle text-base-content toggle-md">
<input type="checkbox" /> <input type="checkbox" />
<svg <span class="swap-on flex items-center gap-1">
aria-label="smiles mode" <div
xmlns="http://www.w3.org/2000/svg" class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
viewBox="0 0 20 20"
class="size-5"
>
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2"
fill="currentColor"
stroke="none"
> >
<path <svg
fill-rule="evenodd" aria-label="smiles mode"
d="M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z" xmlns="http://www.w3.org/2000/svg"
clip-rule="evenodd" viewBox="0 0 20 20"
/> class="size-5"
</g> >
</svg> <g
<svg stroke-linejoin="round"
aria-label="draw mode" stroke-linecap="round"
xmlns="http://www.w3.org/2000/svg" stroke-width="2"
viewBox="0 0 20 20" fill="currentColor"
fill="currentColor" stroke="none"
stroke="none" >
class="size-5" <path
> fill-rule="evenodd"
<path d="M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z"
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" clip-rule="evenodd"
/> />
</svg> </g>
</svg>
</div>
<span class="ext-xs">SMILES</span>
</span>
<span class="swap-off flex items-center gap-1">
<div
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
>
<svg
aria-label="draw mode"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
stroke="none"
class="size-5"
>
<path
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
/>
</svg>
</div>
<span class="text-base/50 text-xs">Draw</span>
</span>
</label> </label>
</div> </div>
</div> </div>
<fieldset <fieldset
class="fieldset transition-all duration-300 ease-in-out overflow-hidden" class="fieldset overflow-hidden transition-all duration-300 ease-in-out"
> >
<form <form
id="index-form" id="index-form"
@ -79,29 +91,29 @@
{% csrf_token %} {% csrf_token %}
<div <div
id="text-input-container" id="text-input-container"
class="transition-all duration-300 ease-in-out opacity-100 transform scale-100" class="scale-100 transform opacity-100 transition-all duration-300 ease-in-out"
> >
<div class="join w-full mx-auto"> <div class="join mx-auto w-full">
<input <input
type="text" type="text"
id="index-form-text-input" id="index-form-text-input"
placeholder="canonical SMILES string" placeholder="canonical SMILES string"
class="input grow input-md join-item" class="input input-md join-item grow"
/> />
<button class="btn btn-neutral join-item">Predict!</button> <button class="btn btn-neutral join-item">Predict!</button>
</div> </div>
<div class="label relative w-full mt-1"> <div class="label relative mt-1 w-full">
<div class="flex gap-2"> <div class="flex gap-2">
<a <a
href="#" href="#"
class="example-link cursor-pointer hover:text-primary" class="example-link hover:text-primary cursor-pointer"
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C" data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
title="load example" title="load example"
>Caffeine</a >Caffeine</a
> >
<a <a
href="#" href="#"
class="example-link cursor-pointer hover:text-primary" class="example-link hover:text-primary cursor-pointer"
data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O" data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
title="load example" title="load example"
>Ibuprofen</a >Ibuprofen</a
@ -114,7 +126,7 @@
</div> </div>
<div <div
id="ketcher-container" id="ketcher-container"
class="hidden w-full transition-all duration-300 ease-in-out opacity-0 transform scale-95" class="hidden w-full scale-95 transform opacity-0 transition-all duration-300 ease-in-out"
> >
<iframe <iframe
id="index-ketcher" id="index-ketcher"
@ -124,11 +136,13 @@
class="rounded-lg" class="rounded-lg"
></iframe> ></iframe>
<button <button
class="btn btn-lg bg-primary-950 text-primary-50 join-item w-full mt-2" class="btn btn-lg bg-primary-950 text-primary-50 join-item mt-2 w-full"
> >
Predict! Predict!
</button> </button>
<a class="label mx-auto w-full mt-1" href="#">Advanced</a> <div class="mt-1 flex w-full justify-end">
<a class="label justify-end" href="/predict">Advanced</a>
</div>
</div> </div>
<input <input
type="hidden" type="hidden"
@ -150,18 +164,18 @@
</div> </div>
<!-- Community News Section --> <!-- Community News Section -->
<section class="py-16 bg-base-200 z-10 mx-8"> <section class="bg-base-200 z-10 mx-8 py-16">
<div class="max-w-7xl mx-auto px-4"> <div class="mx-auto max-w-7xl px-4">
<h2 class="h2 font-bold text-left mb-8">Community Updates</h2> <h2 class="h2 mb-8 text-left font-bold">Community Updates</h2>
<div id="community-news-container" class="flex gap-4 justify-center"> <div id="community-news-container" class="flex justify-center gap-4">
<!-- 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 w-full justify-center">
<span class="loading loading-spinner loading-lg"></span> <span class="loading loading-spinner loading-lg"></span>
</div> </div>
</div> </div>
<div class="text-right mt-6"> <div class="mt-6 text-right">
<a <a
href="https://community.envipath.org/c/announcements/10" href="https://community.envipath.org/c/announcements/10"
target="_blank" target="_blank"
@ -177,18 +191,18 @@
</section> </section>
<!-- Mission Statement Section --> <!-- Mission Statement Section -->
<section class="py-16 from-base-200 to-base-100 bg-gradient-to-b"> <section class="from-base-200 to-base-100 bg-gradient-to-b py-16">
<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 <img
src="{% static "/images/ep-rule-artwork.png" %}" src="{% static "/images/ep-rule-artwork.png" %}"
alt="rule-based iterative tree greneration" alt="rule-based iterative tree greneration"
class="w-full h-full object-contain" class="h-full w-full object-contain"
/> />
</div> </div>
<div class="space-y-4 text-left w-2/3 mr-8"> <div class="mr-8 w-2/3 space-y-4 text-left">
<h2 class="h2 font-bold mb-8">About enviPath</h2> <h2 class="h2 mb-8 font-bold">About enviPath</h2>
<p class=""> <p class="">
enviPath is a database and prediction system for the microbial enviPath is a database and prediction system for the microbial
biotransformation of organic environmental contaminants. The biotransformation of organic environmental contaminants. The
@ -201,7 +215,7 @@
products. Explore our tools and contribute to advancing products. Explore our tools and contribute to advancing
environmental biotransformation research. environmental biotransformation research.
</p> </p>
<div class="flex flex-row gap-4 float-right"> <div class="float-right flex flex-row gap-4">
<a href="/about" class="btn btn-ghost-neutral">Read More</a> <a href="/about" class="btn btn-ghost-neutral">Read More</a>
<a href="/about" class="btn btn-neutral">Publications</a> <a href="/about" class="btn btn-neutral">Publications</a>
</div> </div>
@ -211,7 +225,7 @@
</section> </section>
<!-- Partners Section --> <!-- Partners Section -->
<section class="py-14 sm:py-12 bg-base-100"> <section class="bg-base-100 py-14 sm:py-12">
<div class="mx-auto px-6 lg:px-8"> <div class="mx-auto px-6 lg:px-8">
<div class="divider"> <div class="divider">
<h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2> <h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2>
@ -222,12 +236,12 @@
<img <img
src="{% static "/images/uoa-logo-small.png" %}" src="{% static "/images/uoa-logo-small.png" %}"
alt="The University of Auckland" alt="The University of Auckland"
class=" max-h-20 w-full object-contain lg:col-span-1" class="max-h-20 w-full object-contain lg:col-span-1"
/> />
<img <img
src="{% static "/images/logo-eawag.svg" %}" src="{% static "/images/logo-eawag.svg" %}"
alt="Eawag" alt="Eawag"
class=" max-h-12 w-full object-contain lg:col-span-1" class="max-h-12 w-full object-contain lg:col-span-1"
/> />
<img <img
src="{% static "/images/uzh-logo.svg" %}" src="{% static "/images/uzh-logo.svg" %}"

View File

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

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);

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -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", ""),
}, },