12 Commits

Author SHA1 Message Date
1a2c9bb543 [Feature] Modern UI roll out (#236)
This PR moves all the collection pages into the new UI in a rough push.
I did not put the same amount of care into these as into search, index, and predict.

## Major changes

- All modals are now migrated to a state based alpine.js implementation.
- jQuery is no longer present in the base layout; ajax is replace by native fetch api
- most of the pps.js is now obsolte (as I understand it; the code is not referenced any more @jebus  please double check)
- in-memory pagination for large result lists (set to 50; we can make that configurable later; performance degrades at around 1k) stukk a bit rough tracked in #235

## Minor things

- Sarch and index also use alpine now
- The loading spinner is now CSS animated (not sure if it currently gets correctly called)

## Not done

- Ihave not even cheked the admin pages. Not sure If these need migrations
- The temporary migration pages still use the old template. Not sure what is supposed to happen with those? @jebus

## What I did to test

- opend all pages in browse, and user ; plus all pages reachable from there.
- Interacted and tested the functionality of each modal superfically with exception of the API key modal (no functional test).

---
This PR is massive sorry for that; just did not want to push half-brokenn state.
@jebus @liambrydon I would be glad if you could click around and try to break it :)

Finally closes #133

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#236
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-26 23:16:44 +13:00
7f6f209b4a [Feature] Frontend Testing #140 (#218)
I added playwright for frontend testing and got a couple simple test cases working.
I have updated pyproject.toml but it can also be installed with `pip install pytest-playwright` followed by `playwright install`

With the django server running you can do `playwright codegen http://localhost:8000/` which will generate test code based on the actions you take on the webpage it opens. Be sure to change the target to pytest in the code pop up.

I will add more test cases but @jebus and @t03i feel free to add more. Especially once we are done with the full front-end redesign.

I have put the tests under `tests/frontend/` but I am not sure how to add them to the CI. They give steps for CI integration but maybe we want to somehow include them in our exisiting CI yaml? https://playwright.dev/python/docs/ci-intro

Reviewed-on: enviPath/enviPy#218
Reviewed-by: Tobias O <tobias.olenyi@envipath.com>
Co-authored-by: Liam Brydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: Liam Brydon <lbry121@aucklanduni.ac.nz>
2025-11-26 19:44:35 +13:00
b6c35fea76 [Feature] Search API Endpoint (#227)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#227
2025-11-20 09:56:11 +13:00
fa8a191383 [Fix] Show the User who ran the Job for Admins (#226)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#226
2025-11-20 08:05:15 +13:00
67b1baa5b0 [Feature] Legacy API (#224)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#224
2025-11-19 20:45:16 +13:00
89c194dcca [Enhancement] Restyle Discourse Cards for title only (#220)
Excerpts are only delivered for pinned posts. So all cards apart from pinned look empty.
Changed to only display (more of) the title now.

closes  #214

Reviewed-on: enviPath/enviPy#220
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-14 21:43:52 +13:00
a8554c903c [Enhancement] Swappable Packages (#216)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#216
Reviewed-by: liambrydon <lbry121@aucklanduni.ac.nz>
Reviewed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-14 21:42:39 +13:00
d584791ee8 [Fix] Ketcher submission now recognized (#213)
This will hack the ketcher submission to work again (see #207).
The problem seems to be that the iframe loads slower than the script tag so the reference is not available on page load.

Registering from within the code to poll until ketcher is ready is a bit messy.
Tracked the introduced dept in #212.

Reviewed-on: enviPath/enviPy#213
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-13 23:27:29 +13:00
e60052b05c [Fix] Remove Search from Old Framework Navbar (#211)
fixes #204

Reviewed-on: enviPath/enviPy#211
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-13 23:16:52 +13:00
3ff8d938d6 [Fix] Advanced now redirects to predict_pathway. (#210)
fixes #208

Reviewed-on: enviPath/enviPy#210
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-13 23:15:50 +13:00
a7f48c2cf9 [Fix] Predict page scrolls to submit button (#209)
Autofocus on form is automatically placed on cancel button. Now it is on Name.

fixes #205

Reviewed-on: enviPath/enviPy#209
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-13 23:15:08 +13:00
39faab3d11 [Fix] Add extra styles to make show login form (#203)
FIx display on the login page

Reviewed-on: enviPath/enviPy#203
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-13 09:11:32 +13:00
136 changed files with 12185 additions and 9694 deletions

View File

@ -8,6 +8,7 @@ on:
jobs: jobs:
test: test:
if: ${{ !contains(gitea.event.pull_request.title, 'WIP') }}
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
@ -99,6 +100,18 @@ jobs:
- name: Setup venv - name: Setup venv
run: | run: |
uv sync --locked --all-extras --dev uv sync --locked --all-extras --dev
source .venv/bin/activate
playwright install --with-deps
- name: Run PNPM Commands
run: |
uv run python scripts/pnpm_wrapper.py install
cat << 'EOF' > pnpm-workspace.yaml
onlyBuiltDependencies:
- '@parcel/watcher'
- '@tailwindcss/oxide'
EOF
uv run python scripts/pnpm_wrapper.py run build
- name: Wait for services - name: Wait for services
run: | run: |
@ -110,7 +123,12 @@ jobs:
source .venv/bin/activate source .venv/bin/activate
python manage.py migrate --noinput python manage.py migrate --noinput
- name: Run frontend tests
run: |
source .venv/bin/activate
python manage.py test --tag frontend
- name: Run Django tests - name: Run Django tests
run: | run: |
source .venv/bin/activate source .venv/bin/activate
python manage.py test tests --exclude-tag slow python manage.py test tests --exclude-tag slow --exclude-tag frontend

1
.gitignore vendored
View File

@ -6,6 +6,7 @@ static/django_extensions/
.env .env
debug.log debug.log
scratches/ scratches/
test-results/
data/ data/

View File

@ -49,9 +49,23 @@ INSTALLED_APPS = [
"oauth2_provider", "oauth2_provider",
# Custom # Custom
"epdb", "epdb",
"migration", # "migration",
] ]
TENANT = os.environ.get("TENANT", "public")
if TENANT != "public":
INSTALLED_APPS.append(TENANT)
EPDB_PACKAGE_MODEL = os.environ.get("EPDB_PACKAGE_MODEL", "epdb.Package")
def GET_PACKAGE_MODEL():
from django.apps import apps
return apps.get_model(EPDB_PACKAGE_MODEL)
AUTHENTICATION_BACKENDS = [ AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend", "django.contrib.auth.backends.ModelBackend",
] ]

View File

@ -23,12 +23,20 @@ from .api import api_v1, api_legacy
urlpatterns = [ urlpatterns = [
path("", include("epdb.urls")), path("", include("epdb.urls")),
path("", include("migration.urls")),
path("admin/", admin.site.urls), path("admin/", admin.site.urls),
path("api/v1/", api_v1.urls), path("api/v1/", api_v1.urls),
path("api/legacy/", api_legacy.urls), path("api/legacy/", api_legacy.urls),
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")), path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
] ]
if "migration" in s.INSTALLED_APPS:
urlpatterns.append(path("", include("migration.urls")))
if s.MS_ENTRA_ENABLED: if s.MS_ENTRA_ENABLED:
urlpatterns.append(path("", include("epauth.urls"))) urlpatterns.append(path("", include("epauth.urls")))
# Custom error handlers
handler400 = "epdb.views.handler400"
handler403 = "epdb.views.handler403"
handler404 = "epdb.views.handler404"
handler500 = "epdb.views.handler500"

View File

@ -1,29 +1,31 @@
from django.conf import settings as s
from django.contrib import admin from django.contrib import admin
from .models import ( from .models import (
User,
UserPackagePermission,
Group,
GroupPackagePermission,
Package,
MLRelativeReasoning,
EnviFormer,
Compound, Compound,
CompoundStructure, CompoundStructure,
SimpleAmbitRule,
ParallelRule,
Reaction,
Pathway,
Node,
Edge, Edge,
Scenario, EnviFormer,
Setting,
ExternalDatabase, ExternalDatabase,
ExternalIdentifier, ExternalIdentifier,
Group,
GroupPackagePermission,
JobLog, JobLog,
License, License,
MLRelativeReasoning,
Node,
ParallelRule,
Pathway,
Reaction,
Scenario,
Setting,
SimpleAmbitRule,
User,
UserPackagePermission,
) )
Package = s.GET_PACKAGE_MODEL()
class UserAdmin(admin.ModelAdmin): class UserAdmin(admin.ModelAdmin):
list_display = ["username", "email", "is_active"] list_display = ["username", "email", "is_active"]

View File

@ -1,4 +1,9 @@
import logging
from django.apps import AppConfig from django.apps import AppConfig
from django.conf import settings
logger = logging.getLogger(__name__)
class EPDBConfig(AppConfig): class EPDBConfig(AppConfig):
@ -7,3 +12,6 @@ class EPDBConfig(AppConfig):
def ready(self): def ready(self):
import epdb.signals # noqa: F401 import epdb.signals # noqa: F401
model_name = getattr(settings, "EPDB_PACKAGE_MODEL", "epdb.Package")
logger.info(f"Using Package model: {model_name}")

View File

@ -5,7 +5,7 @@ Context processors automatically make variables available to all templates.
""" """
from .logic import PackageManager from .logic import PackageManager
from .models import Package from django.conf import settings as s
def package_context(request): def package_context(request):
@ -20,7 +20,7 @@ def package_context(request):
reviewed_package_qs = PackageManager.get_reviewed_packages() reviewed_package_qs = PackageManager.get_reviewed_packages()
unreviewed_package_qs = Package.objects.none() unreviewed_package_qs = s.GET_PACKAGE_MODEL().objects.none()
# Only get user-specific packages if user is authenticated # Only get user-specific packages if user is authenticated
if current_user.is_authenticated: if current_user.is_authenticated:

View File

@ -1,27 +1,35 @@
from typing import List, Dict, Optional, Any from typing import Any, Dict, List, Optional
import nh3
from django.conf import settings as s
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.http import HttpResponse from django.http import HttpResponse
from django.shortcuts import redirect from django.shortcuts import redirect
from ninja import Router, Schema, Field, Form from ninja import Field, Form, Router, Schema, Query
from ninja.security import SessionAuth
from utilities.chem import FormatConverter from utilities.chem import FormatConverter
from .logic import PackageManager, UserManager, SettingManager from utilities.misc import PackageExporter
from .logic import GroupManager, PackageManager, SettingManager, UserManager, SearchManager
from .models import ( from .models import (
Compound, Compound,
CompoundStructure, CompoundStructure,
Package, Edge,
EPModel,
Node,
Pathway,
Reaction,
Rule,
Scenario,
SimpleAmbitRule,
User, User,
UserPackagePermission, UserPackagePermission,
Rule, ParallelRule,
Reaction,
Scenario,
Pathway,
Node,
Edge,
SimpleAmbitRule,
) )
Package = s.GET_PACKAGE_MODEL()
def _anonymous_or_real(request): def _anonymous_or_real(request):
if request.user.is_authenticated and not request.user.is_anonymous: if request.user.is_authenticated and not request.user.is_anonymous:
@ -29,8 +37,7 @@ def _anonymous_or_real(request):
return get_user_model().objects.get(username="anonymous") return get_user_model().objects.get(username="anonymous")
# router = Router(auth=SessionAuth()) router = Router(auth=SessionAuth(csrf=False))
router = Router()
class Error(Schema): class Error(Schema):
@ -118,13 +125,16 @@ class SimpleEdge(SimpleObject):
identifier: str = "edge" identifier: str = "edge"
class SimpleModel(SimpleObject):
identifier: str = "relative-reasoning"
################ ################
# Login/Logout # # Login/Logout #
################ ################
@router.post("/", response={200: SimpleUser, 403: Error}) @router.post("/", response={200: SimpleUser, 403: Error}, auth=None)
def login(request, loginusername: Form[str], loginpassword: Form[str]): def login(request, loginusername: Form[str], loginpassword: Form[str]):
from django.contrib.auth import authenticate from django.contrib.auth import authenticate, login
from django.contrib.auth import login
email = User.objects.get(username=loginusername).email email = User.objects.get(username=loginusername).email
user = authenticate(username=email, password=loginpassword) user = authenticate(username=email, password=loginpassword)
@ -167,9 +177,13 @@ class UserSchema(Schema):
return SettingManager.get_all_settings(obj) return SettingManager.get_all_settings(obj)
class Me(Schema):
whoami: str | None = None
@router.get("/user", response={200: UserWrapper, 403: Error}) @router.get("/user", response={200: UserWrapper, 403: Error})
def get_users(request, whoami: str = None): def get_users(request, me: Query[Me]):
if whoami: if me.whoami:
return {"user": [request.user]} return {"user": [request.user]}
else: else:
return {"user": User.objects.all()} return {"user": User.objects.all()}
@ -186,6 +200,61 @@ def get_user(request, user_uuid):
} }
class Search(Schema):
packages: List[str] = Field(alias="packages[]")
search: str
method: str
@router.get("/search", response={200: Any, 403: Error})
def search(request, search: Query[Search]):
try:
packs = []
for package in search.packages:
packs.append(PackageManager.get_package_by_url(request.user, package))
method = None
if search.method == "text":
method = "text"
elif search.method == "inchikey":
method = "inchikey"
elif search.method == "defaultSmiles":
method = "default"
elif search.method == "canonicalSmiles":
method = "canonical"
elif search.method == "exactSmiles":
method = "exact"
if method is None:
raise ValueError(f"Search method {search.method} is not supported!")
search_res = SearchManager.search(packs, search.search, method)
res = {}
if "Compounds" in search_res:
res["compound"] = search_res["Compounds"]
if "Compound Structures" in search_res:
res["structure"] = search_res["Compound Structures"]
if "Reaction" in search_res:
res["reaction"] = search_res["Reaction"]
if "Pathway" in search_res:
res["pathway"] = search_res["Pathway"]
if "Rules" in search_res:
res["rule"] = search_res["Rules"]
for key in res:
for v in res[key]:
v["id"] = v["url"].replace("simple-ambit-rule", "simple-rule")
return res
except ValueError as e:
return 403, {"message": f"Search failed due to {e}"}
########### ###########
# Package # # Package #
########### ###########
@ -251,67 +320,110 @@ def get_packages(request):
} }
@router.get("/package/{uuid:package_uuid}", response={200: PackageSchema, 403: Error}) class GetPackage(Schema):
def get_package(request, package_uuid): exportAsJson: str | None = None
@router.get("/package/{uuid:package_uuid}", response={200: PackageSchema | Any, 403: Error})
def get_package(request, package_uuid, gp: Query[GetPackage]):
try: try:
return PackageManager.get_package_by_id(request.user, package_uuid) p = PackageManager.get_package_by_id(request.user, package_uuid)
if gp.exportAsJson and gp.exportAsJson.strip() == "true":
return PackageExporter(p).do_export()
return p
except ValueError: except ValueError:
return 403, { return 403, {
"message": f"Getting Package with id {package_uuid} failed due to insufficient rights!" "message": f"Getting Package with id {package_uuid} failed due to insufficient rights!"
} }
class CreatePackage(Schema):
packageName: str
packageDescription: str | None = None
@router.post("/package") @router.post("/package")
def create_packages( def create_packages(
request, packageName: Form[str], packageDescription: Optional[str] = Form(None) request,
p: Form[CreatePackage],
): ):
try: try:
if packageName.strip() == "": if p.packageName.strip() == "":
raise ValueError("Package name cannot be empty!") raise ValueError("Package name cannot be empty!")
new_pacakge = PackageManager.create_package(request.user, packageName, packageDescription) new_pacakge = PackageManager.create_package(
request.user, p.packageName, p.packageDescription
)
return redirect(new_pacakge.url) return redirect(new_pacakge.url)
except ValueError as e: except ValueError as e:
return 400, {"message": str(e)} return 400, {"message": str(e)}
class UpdatePackage(Schema):
packageDescription: str | None = None
hiddenMethod: str | None = None
permissions: str | None = None
ppsURI: str | None = None
read: str | None = None
write: str | None = None
@router.post("/package/{uuid:package_uuid}", response={200: PackageSchema | Any, 400: Error}) @router.post("/package/{uuid:package_uuid}", response={200: PackageSchema | Any, 400: Error})
def update_package( def update_package(request, package_uuid, pack: Form[UpdatePackage]):
request,
package_uuid,
packageDescription: Optional[str] = Form(None),
hiddenMethod: Optional[str] = Form(None),
exportAsJson: Optional[str] = Form(None),
permissions: Optional[str] = Form(None),
ppsURI: Optional[str] = Form(None),
read: Optional[str] = Form(None),
write: Optional[str] = Form(None),
):
try: try:
p = PackageManager.get_package_by_id(request.user, package_uuid) p = PackageManager.get_package_by_id(request.user, package_uuid)
if hiddenMethod: if pack.hiddenMethod:
if hiddenMethod == "DELETE": if pack.hiddenMethod == "DELETE":
p.delete() p.delete()
elif packageDescription and packageDescription.strip() != "": elif pack.packageDescription is not None:
p.description = packageDescription description = nh3.clean(pack.packageDescription, tags=s.ALLOWED_HTML_TAGS).strip()
p.save()
return
elif exportAsJson == "true":
pack_json = PackageManager.export_package(
p, include_models=False, include_external_identifiers=False
)
return pack_json
elif all([permissions, ppsURI, read]):
PackageManager.update_permissions
elif all([permissions, ppsURI, write]):
pass
if description:
p.description = description
p.save()
return HttpResponse(status=200)
else:
raise ValueError("Package description cannot be empty!")
elif all([pack.permissions, pack.ppsURI, pack.read]):
if "group" in pack.ppsURI:
grantee = GroupManager.get_group_lp(pack.ppsURI)
else:
grantee = UserManager.get_user_lp(pack.ppsURI)
PackageManager.grant_read(request.user, p, grantee)
return HttpResponse(status=200)
elif all([pack.permissions, pack.ppsURI, pack.write]):
if "group" in pack.ppsURI:
grantee = GroupManager.get_group_lp(pack.ppsURI)
else:
grantee = UserManager.get_user_lp(pack.ppsURI)
PackageManager.grant_write(request.user, p, grantee)
return HttpResponse(status=200)
except ValueError as e: except ValueError as e:
return 400, {"message": str(e)} return 400, {"message": str(e)}
@router.delete("/package/{uuid:package_uuid}")
def delete_package(request, package_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.administrable(request.user, p):
p.delete()
return redirect(f"{s.SERVER_URL}/package")
else:
raise ValueError("You do not have the rights to delete this Package!")
except ValueError:
return 403, {
"message": f"Deleting Package with id {package_uuid} failed due to insufficient rights!"
}
################################ ################################
# Compound / CompoundStructure # # Compound / CompoundStructure #
################################ ################################
@ -509,6 +621,83 @@ def get_package_compound_structure(request, package_uuid, compound_uuid, structu
} }
class CreateCompound(Schema):
compoundSmiles: str
compoundName: str | None = None
compoundDescription: str | None = None
inchi: str | None = None
@router.post("/package/{uuid:package_uuid}/compound")
def create_package_compound(
request,
package_uuid,
c: Form[CreateCompound],
):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
# inchi is not used atm
c = Compound.create(
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
)
return redirect(c.url)
except ValueError as e:
return 400, {"message": str(e)}
@router.delete("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}")
def delete_compound(request, package_uuid, compound_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
c = Compound.objects.get(package=p, uuid=compound_uuid)
c.delete()
return redirect(f"{p.url}/compound")
else:
raise ValueError("You do not have the rights to delete this Compound!")
except ValueError:
return 403, {
"message": f"Deleting Compound with id {compound_uuid} failed due to insufficient rights!"
}
@router.delete(
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
)
def delete_compound_structure(request, package_uuid, compound_uuid, structure_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
c = Compound.objects.get(package=p, uuid=compound_uuid)
cs = CompoundStructure.objects.get(compound=c, uuid=structure_uuid)
# Check if we have to delete the compound as no structure is left
if len(cs.compound.structures.all()) == 1:
# This will delete the structure as well
c.delete()
return redirect(p.url + "/compound")
else:
if cs.normalized_structure:
c.delete()
return redirect(p.url + "/compound")
else:
if c.default_structure == cs:
cs.delete()
c.default_structure = c.structures.all().first()
return redirect(c.url + "/structure")
else:
cs.delete()
return redirect(c.url + "/structure")
else:
raise ValueError("You do not have the rights to delete this CompoundStructure!")
except ValueError:
return 403, {
"message": f"Deleting CompoundStructure with id {compound_uuid} failed due to insufficient rights!"
}
######### #########
# Rules # # Rules #
######### #########
@ -672,6 +861,73 @@ def _get_package_rule(request, package_uuid, rule_uuid):
# POST # POST
class CreateSimpleRule(Schema):
smirks: str
name: str | None = None
description: str | None = None
reactantFilterSmarts: str | None = None
productFilterSmarts: str | None = None
immediate: str | None = None
rdkitrule: str | None = None
@router.post("/package/{uuid:package_uuid}/simple-rule")
def create_package_simple_rule(
request,
package_uuid,
r: Form[CreateSimpleRule],
):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if r.rdkitrule and r.rdkitrule.strip() == "true":
raise ValueError("Not yet implemented!")
else:
sr = SimpleAmbitRule.create(
p, r.name, r.description, r.smirks, r.reactantFilterSmarts, r.productFilterSmarts
)
return redirect(sr.url)
except ValueError as e:
return 400, {"message": str(e)}
class CreateParallelRule(Schema):
simpleRules: str
name: str | None = None
description: str | None = None
reactantFilterSmarts: str | None = None
productFilterSmarts: str | None = None
immediate: str | None = None
@router.post("/package/{uuid:package_uuid}/parallel-rule")
def create_package_parallel_rule(
request,
package_uuid,
r: Form[CreateParallelRule],
):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
srs = SimpleRule.objects.filter(package=p, url__in=r.simpleRules)
if srs.count() != len(r.simpleRules):
raise ValueError(
f"Not all SimpleRules could be found in Package with id {package_uuid}!"
)
sr = ParallelRule.create(
p, list(srs), r.name, r.description, r.reactantFilterSmarts, r.productFilterSmarts
)
return redirect(sr.url)
except ValueError as e:
return 400, {"message": str(e)}
@router.post( @router.post(
"/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error} "/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error}
) )
@ -721,6 +977,41 @@ def _post_package_rule(request, package_uuid, rule_uuid, compound: Form[str]):
} }
@router.delete("/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}")
def delete_rule(request, package_uuid, rule_uuid):
return _delete_rule(request, package_uuid, rule_uuid)
@router.delete(
"/package/{uuid:package_uuid}/simple-rule/{uuid:rule_uuid}",
)
def delete_simple_rule(request, package_uuid, rule_uuid):
return _delete_rule(request, package_uuid, rule_uuid)
@router.delete(
"/package/{uuid:package_uuid}/parallel-rule/{uuid:rule_uuid}",
)
def delete_parallel_rule(request, package_uuid, rule_uuid):
return _delete_rule(request, package_uuid, rule_uuid)
def _delete_rule(request, package_uuid, rule_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
r = Rule.objects.get(package=p, uuid=rule_uuid)
r.delete()
return redirect(f"{p.url}/rule")
else:
raise ValueError("You do not have the rights to delete this Rule!")
except ValueError:
return 403, {
"message": f"Deleting Rule with id {rule_uuid} failed due to insufficient rights!"
}
############ ############
# Reaction # # Reaction #
############ ############
@ -809,6 +1100,82 @@ def get_package_reaction(request, package_uuid, reaction_uuid):
} }
class CreateReaction(Schema):
reactionName: str | None = None
reactionDescription: str | None = None
smirks: str | None = None
educt: str | None = None
product: str | None = None
rule: str | None = None
@router.post("/package/{uuid:package_uuid}/reaction")
def create_package_reaction(
request,
package_uuid,
r: Form[CreateReaction],
):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if r.smirks is None and (r.educt is None or r.product is None):
raise ValueError("Either SMIRKS or educt/product must be provided")
if r.smirks is not None and (r.educt is not None and r.product is not None):
raise ValueError("SMIRKS and educt/product provided!")
rule = None
if r.rule:
try:
rule = Rule.objects.get(package=p, url=r.rule)
except Rule.DoesNotExist:
raise ValueError(f"Rule with id {r.rule} does not exist!")
if r.educt is not None:
try:
educt_cs = CompoundStructure.objects.get(compound__package=p, url=r.educt)
except CompoundStructure.DoesNotExist:
raise ValueError(f"Compound with id {r.educt} does not exist!")
try:
product_cs = CompoundStructure.objects.get(compound__package=p, url=r.product)
except CompoundStructure.DoesNotExist:
raise ValueError(f"Compound with id {r.product} does not exist!")
new_r = Reaction.create(
p, r.reactionName, r.reactionDescription, [educt_cs], [product_cs], rule
)
else:
educts = r.smirks.split(">>")[0].split("\\.")
products = r.smirks.split(">>")[1].split("\\.")
new_r = Reaction.create(
p, r.reactionName, r.reactionDescription, educts, products, rule
)
return redirect(new_r.url)
except ValueError as e:
return 400, {"message": str(e)}
@router.delete("/package/{uuid:package_uuid}/reaction/{uuid:reaction_uuid}")
def delete_reaction(request, package_uuid, reaction_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
r = Reaction.objects.get(package=p, uuid=reaction_uuid)
r.delete()
return redirect(f"{p.url}/reaction")
else:
raise ValueError("You do not have the rights to delete this Reaction!")
except ValueError:
return 403, {
"message": f"Deleting Reaction with id {reaction_uuid} failed due to insufficient rights!"
}
############ ############
# Scenario # # Scenario #
############ ############
@ -823,7 +1190,7 @@ class ScenarioSchema(Schema):
description: str = Field(None, alias="description") description: str = Field(None, alias="description")
id: str = Field(None, alias="url") id: str = Field(None, alias="url")
identifier: str = "scenario" identifier: str = "scenario"
linkedTo: List[Dict[str, str]] = Field({}, alias="linked_to") linkedTo: List[Dict[str, str]] = Field([], alias="linked_to")
name: str = Field(None, alias="name") name: str = Field(None, alias="name")
pathways: List["SimplePathway"] = Field([], alias="related_pathways") pathways: List["SimplePathway"] = Field([], alias="related_pathways")
relatedScenarios: List[Dict[str, str]] = Field([], alias="related_scenarios") relatedScenarios: List[Dict[str, str]] = Field([], alias="related_scenarios")
@ -874,6 +1241,38 @@ def get_package_scenario(request, package_uuid, scenario_uuid):
} }
@router.delete("/package/{uuid:package_uuid}/scenario")
def delete_scenarios(request, package_uuid, scenario_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
scens = Scenario.objects.filter(package=p)
scens.delete()
return redirect(f"{p.url}/scenario")
else:
raise ValueError("You do not have the rights to delete Scenarios!")
except ValueError:
return 403, {"message": "Deleting Scenarios failed due to insufficient rights!"}
@router.delete("/package/{uuid:package_uuid}/scenario/{uuid:scenario_uuid}")
def delete_scenario(request, package_uuid, scenario_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
scen = Scenario.objects.get(package=p, uuid=scenario_uuid)
scen.delete()
return redirect(f"{p.url}/scenario")
else:
raise ValueError("You do not have the rights to delete this Scenario!")
except ValueError:
return 403, {
"message": f"Deleting Scenario with id {scenario_uuid} failed due to insufficient rights!"
}
########### ###########
# Pathway # # Pathway #
########### ###########
@ -1013,46 +1412,67 @@ def get_package_pathway(request, package_uuid, pathway_uuid):
} }
class CreatePathway(Schema):
smilesinput: str
name: str | None = None
description: str | None = None
rootOnly: str | None = None
selectedSetting: str | None = None
@router.post("/package/{uuid:package_uuid}/pathway") @router.post("/package/{uuid:package_uuid}/pathway")
def create_pathway( def create_pathway(
request, request,
package_uuid, package_uuid,
smilesinput: Form[str], pw: Form[CreatePathway],
name: Optional[str] = Form(None),
description: Optional[str] = Form(None),
rootOnly: Optional[str] = Form(None),
selectedSetting: Optional[str] = Form(None),
): ):
try: try:
p = PackageManager.get_package_by_id(request.user, package_uuid) p = PackageManager.get_package_by_id(request.user, package_uuid)
stand_smiles = FormatConverter.standardize(smilesinput.strip()) stand_smiles = FormatConverter.standardize(pw.smilesinput.strip())
pw = Pathway.create(p, stand_smiles, name=name, description=description) new_pw = Pathway.create(p, stand_smiles, name=pw.name, description=pw.description)
pw_mode = "predict" pw_mode = "predict"
if rootOnly and rootOnly == "true": if pw.rootOnly and pw.rootOnly.strip() == "true":
pw_mode = "build" pw_mode = "build"
pw.kv.update({"mode": pw_mode}) new_pw.kv.update({"mode": pw_mode})
pw.save() new_pw.save()
if pw_mode == "predict": if pw_mode == "predict":
setting = request.user.prediction_settings() setting = request.user.prediction_settings()
if selectedSetting: if pw.selectedSetting:
setting = SettingManager.get_setting_by_url(request.user, selectedSetting) setting = SettingManager.get_setting_by_url(request.user, pw.selectedSetting)
pw.setting = setting new_pw.setting = setting
pw.save() new_pw.save()
from .tasks import predict from .tasks import dispatch, predict
predict.delay(pw.pk, setting.pk, limit=-1) dispatch(request.user, predict, new_pw.pk, setting.pk, limit=-1)
return redirect(pw.url) return redirect(new_pw.url)
except ValueError as e: except ValueError as e:
print(e) return 400, {"message": str(e)}
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
def delete_pathway(request, package_uuid, pathway_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
pw.delete()
return redirect(f"{p.url}/pathway")
else:
raise ValueError("You do not have the rights to delete this pathway!")
except ValueError:
return 403, {
"message": f"Deleting Pathway with id {pathway_uuid} failed due to insufficient rights!"
}
######## ########
@ -1143,6 +1563,52 @@ def get_package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
} }
class CreateNode(Schema):
nodeAsSmiles: str
nodeName: str | None = None
nodeReason: str | None = None
nodeDepth: str | None = None
@router.post(
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
response={200: str | Any, 403: Error},
)
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
node_depth = int(n.nodeDepth)
else:
node_depth = -1
n = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
return redirect(n.url)
except ValueError:
return 403, {"message": "Adding node failed!"}
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node/{uuid:node_uuid}")
def delete_node(request, package_uuid, pathway_uuid, node_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
n = Node.objects.get(pathway=pw, uuid=node_uuid)
n.delete()
return redirect(f"{pw.url}/node")
else:
raise ValueError("You do not have the rights to delete this Node!")
except ValueError:
return 403, {
"message": f"Deleting Node with id {node_uuid} failed due to insufficient rights!"
}
######## ########
# Edge # # Edge #
######## ########
@ -1206,6 +1672,200 @@ def get_package_pathway_edge(request, package_uuid, pathway_uuid, edge_uuid):
} }
class CreateEdge(Schema):
edgeAsSmirks: str | None = None
educts: str | None = None # Node URIs comma sep
products: str | None = None # Node URIs comma sep
multistep: str | None = None
edgeReason: str | None = None
@router.post(
"/package/{uuid:package_uuid}/üathway/{uuid:pathway_uuid}/edge",
response={200: str | Any, 403: Error},
)
def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
if e.edgeAsSmirks is None and (e.educts is None or e.products is None):
raise ValueError("Either SMIRKS or educt/product must be provided")
if e.edgeAsSmirks is not None and (e.educts is not None and e.products is not None):
raise ValueError("SMIRKS and educt/product provided!")
educts = []
products = []
if e.edgeAsSmirks:
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
educts.append(Node.objects.get(pathway=pw, default_node_label__smiles=ed))
for pr in e.edgeAsSmirks.split(">>")[1].split("\\."):
products.append(Node.objects.get(pathway=pw, default_node_label__smiles=pr))
else:
for ed in e.educts.split(","):
educts.append(Node.objects.get(pathway=pw, url=ed.strip()))
for pr in e.products.split(","):
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
new_e = Edge.create(
pathway=pw,
start_nodes=educts,
end_nodes=products,
rule=None,
name=e.name,
description=e.edgeReason,
)
return redirect(new_e.url)
except ValueError:
return 403, {"message": "Adding node failed!"}
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")
def delete_edge(request, package_uuid, pathway_uuid, edge_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
e = Edge.objects.get(pathway=pw, uuid=edge_uuid)
e.delete()
return redirect(f"{pw.url}/edge")
else:
raise ValueError("You do not have the rights to delete this Edge!")
except ValueError:
return 403, {
"message": f"Deleting Edge with id {edge_uuid} failed due to insufficient rights!"
}
#########
# Model #
#########
class ModelWrapper(Schema):
relative_reasoning: List["SimpleModel"] = Field(..., alias="relative-reasoning")
class ModelSchema(Schema):
aliases: List[str] = Field([], alias="aliases")
description: str = Field(None, alias="description")
evalPackages: List["SimplePackage"] = Field([])
id: str = Field(None, alias="url")
identifier: str = "relative-reasoning"
# "info" : {
# "Accuracy (Single-Gen)" : "0.5932962678936605" ,
# "Area under PR-Curve (Single-Gen)" : "0.5654653182134282" ,
# "Area under ROC-Curve (Single-Gen)" : "0.8178302405034772" ,
# "Precision (Single-Gen)" : "0.6978730822873083" ,
# "Probability Threshold" : "0.5" ,
# "Recall/Sensitivity (Single-Gen)" : "0.4484149210261006"
# } ,
name: str = Field(None, alias="name")
pathwayPackages: List["SimplePackage"] = Field([])
reviewStatus: str = Field(None, alias="review_status")
rulePackages: List["SimplePackage"] = Field([])
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
status: str
statusMessage: str
threshold: str
type: str
@router.get("/model", response={200: ModelWrapper, 403: Error})
def get_models(request):
pass
@router.get("/package/{uuid:package_uuid}/model", response={200: ModelWrapper, 403: Error})
def get_package_models(request, package_uuid, model_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return EPModel.objects.filter(package=p)
except ValueError:
return 403, {
"message": f"Getting Reaction with id {model_uuid} failed due to insufficient rights!"
}
class Classify(Schema):
smiles: str | None = None
@router.get(
"/package/{uuid:package_uuid}/model/{uuid:model_uuid}",
response={200: ModelSchema | Any, 403: Error, 400: Error},
)
def get_model(request, package_uuid, model_uuid, c: Query[Classify]):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
mod = EPModel.objects.get(package=p, uuid=model_uuid)
if c.smiles:
if c.smiles == "":
return 400, {"message": "Received empty SMILES"}
try:
stand_smiles = FormatConverter.standardize(c.smiles)
except ValueError:
return 400, {"message": f'"{c.smiles}" is not a valid SMILES'}
from epdb.tasks import dispatch_eager, predict_simple
pred_res = dispatch_eager(request.user, predict_simple, mod.pk, stand_smiles)
result = []
for pr in pred_res:
if len(pr) > 0:
products = []
for prod_set in pr.product_sets:
products.append(tuple([x for x in prod_set]))
res = {
"probability": pr.probability,
"products": list(set(products)),
}
if pr.rule:
res["id"] = pr.rule.url
res["identifier"] = pr.rule.get_rule_identifier()
res["name"] = pr.rule.name
res["reviewStatus"] = (
"reviewed" if pr.rule.package.reviewed else "unreviewed"
)
result.append(res)
return result
return mod
except ValueError:
return 403, {
"message": f"Getting Reaction with id {model_uuid} failed due to insufficient rights!"
}
@router.delete("/package/{uuid:package_uuid}/model/{uuid:model_uuid}")
def delete_model(request, package_uuid, model_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if PackageManager.writable(request.user, p):
m = EPModel.objects.get(package=p, uuid=model_uuid)
m.delete()
return redirect(f"{p.url}/model")
else:
raise ValueError("You do not have the rights to delete this Model!")
except ValueError:
return 403, {
"message": f"Deleting Model with id {model_uuid} failed due to insufficient rights!"
}
########### ###########
# Setting # # Setting #
########### ###########

View File

@ -1,39 +1,40 @@
import re
import logging
import json import json
from typing import Union, List, Optional, Set, Dict, Any import logging
import re
from typing import Any, Dict, List, Optional, Set, Union
from uuid import UUID from uuid import UUID
import nh3 import nh3
from django.conf import settings as s
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import transaction from django.db import transaction
from django.conf import settings as s
from pydantic import ValidationError from pydantic import ValidationError
from epdb.models import ( from epdb.models import (
User,
Package,
UserPackagePermission,
GroupPackagePermission,
Permission,
Group,
Setting,
EPModel,
UserSettingPermission,
Rule,
Pathway,
Node,
Edge,
Compound, Compound,
Reaction,
CompoundStructure, CompoundStructure,
Edge,
EnzymeLink, EnzymeLink,
EPModel,
Group,
GroupPackagePermission,
Node,
Pathway,
Permission,
Reaction,
Rule,
Setting,
User,
UserPackagePermission,
UserSettingPermission,
) )
from utilities.chem import FormatConverter from utilities.chem import FormatConverter
from utilities.misc import PackageImporter, PackageExporter from utilities.misc import PackageExporter, PackageImporter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
Package = s.GET_PACKAGE_MODEL()
class EPDBURLParser: class EPDBURLParser:
UUID_PATTERN = r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}" UUID_PATTERN = r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
@ -578,30 +579,39 @@ class PackageManager(object):
else: else:
_ = perm_cls.objects.update_or_create(defaults={"permission": new_perm}, **data) _ = perm_cls.objects.update_or_create(defaults={"permission": new_perm}, **data)
@staticmethod
def grant_read(caller: User, package: Package, grantee: Union[User, Group]):
PackageManager.update_permissions(caller, package, grantee, Permission.READ[0])
@staticmethod
def grant_write(caller: User, package: Package, grantee: Union[User, Group]):
PackageManager.update_permissions(caller, package, grantee, Permission.WRITE[0])
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def import_legacy_package( def import_legacy_package(
data: dict, owner: User, keep_ids=False, add_import_timestamp=True, trust_reviewed=False data: dict, owner: User, keep_ids=False, add_import_timestamp=True, trust_reviewed=False
): ):
from uuid import UUID, uuid4
from datetime import datetime
from collections import defaultdict from collections import defaultdict
from datetime import datetime
from uuid import UUID, uuid4
from envipy_additional_information import AdditionalInformationConverter
from .models import ( from .models import (
Package,
Compound, Compound,
CompoundStructure, CompoundStructure,
SimpleRule, Edge,
SimpleAmbitRule, Node,
ParallelRule, ParallelRule,
Pathway,
Reaction,
Scenario,
SequentialRule, SequentialRule,
SequentialRuleOrdering, SequentialRuleOrdering,
Reaction, SimpleAmbitRule,
Pathway, SimpleRule,
Node,
Edge,
Scenario,
) )
from envipy_additional_information import AdditionalInformationConverter
pack = Package() pack = Package()
pack.uuid = UUID(data["id"].split("/")[-1]) if keep_ids else uuid4() pack.uuid = UUID(data["id"].split("/")[-1]) if keep_ids else uuid4()

View File

@ -2,7 +2,9 @@ from django.conf import settings as s
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from epdb.models import MLRelativeReasoning, EnviFormer, Package from epdb.models import EnviFormer, MLRelativeReasoning
Package = s.GET_PACKAGE_MODEL()
class Command(BaseCommand): class Command(BaseCommand):
@ -75,11 +77,13 @@ class Command(BaseCommand):
return packages return packages
# Iteratively create models in options["model_names"] # Iteratively create models in options["model_names"]
print(f"Creating models: {options['model_names']}\n" print(
f"Data packages: {options['data_packages']}\n" f"Creating models: {options['model_names']}\n"
f"Rule Packages (only for MLRR): {options['rule_packages']}\n" f"Data packages: {options['data_packages']}\n"
f"Eval Packages: {options['eval_packages']}\n" f"Rule Packages (only for MLRR): {options['rule_packages']}\n"
f"Threshold: {options['threshold']:.2f}") f"Eval Packages: {options['eval_packages']}\n"
f"Threshold: {options['threshold']:.2f}"
)
data_packages = decode_packages(options["data_packages"]) data_packages = decode_packages(options["data_packages"])
eval_packages = decode_packages(options["eval_packages"]) eval_packages = decode_packages(options["eval_packages"])
rule_packages = decode_packages(options["rule_packages"]) rule_packages = decode_packages(options["rule_packages"])
@ -90,10 +94,10 @@ class Command(BaseCommand):
pack, pack,
data_packages=data_packages, data_packages=data_packages,
eval_packages=eval_packages, eval_packages=eval_packages,
threshold=options['threshold'], threshold=options["threshold"],
name=f"EnviFormer - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}", name=f"EnviFormer - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description=f"EnviFormer transformer trained on {options['data_packages']} " description=f"EnviFormer transformer trained on {options['data_packages']} "
f"evaluated on {options['eval_packages']}.", f"evaluated on {options['eval_packages']}.",
) )
elif model_name == "mlrr": elif model_name == "mlrr":
model = MLRelativeReasoning.create( model = MLRelativeReasoning.create(
@ -101,10 +105,10 @@ class Command(BaseCommand):
rule_packages=rule_packages, rule_packages=rule_packages,
data_packages=data_packages, data_packages=data_packages,
eval_packages=eval_packages, eval_packages=eval_packages,
threshold=options['threshold'], threshold=options["threshold"],
name=f"ECC - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}", name=f"ECC - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description=f"ML Relative Reasoning trained on {options['data_packages']} with rules from " description=f"ML Relative Reasoning trained on {options['data_packages']} with rules from "
f"{options['rule_packages']} and evaluated on {options['eval_packages']}.", f"{options['rule_packages']} and evaluated on {options['eval_packages']}.",
) )
else: else:
raise ValueError(f"Cannot create model of type {model_name}, unknown model type") raise ValueError(f"Cannot create model of type {model_name}, unknown model type")

View File

@ -8,7 +8,9 @@ from django.conf import settings as s
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from epdb.models import EnviFormer, Package from epdb.models import EnviFormer
Package = s.GET_PACKAGE_MODEL()
class Command(BaseCommand): class Command(BaseCommand):

View File

@ -1,8 +1,8 @@
from django.apps import apps from django.apps import apps
from django.conf import settings as s
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import F, JSONField, TextField, Value
from django.db.models import F, Value, TextField, JSONField from django.db.models.functions import Cast, Replace
from django.db.models.functions import Replace, Cast
from epdb.models import EnviPathModel from epdb.models import EnviPathModel
@ -23,10 +23,13 @@ class Command(BaseCommand):
) )
def handle(self, *args, **options): def handle(self, *args, **options):
Package = s.GET_PACKAGE_MODEL()
print("Localizing urls for Package")
Package.objects.update(url=Replace(F("url"), Value(options["old"]), Value(options["new"])))
MODELS = [ MODELS = [
"User", "User",
"Group", "Group",
"Package",
"Compound", "Compound",
"CompoundStructure", "CompoundStructure",
"Pathway", "Pathway",

View File

@ -2,40 +2,41 @@ import abc
import hashlib import hashlib
import json import json
import logging import logging
import math
import os import os
import secrets import secrets
from abc import abstractmethod from abc import abstractmethod
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
from typing import Union, List, Optional, Dict, Tuple, Set, Any from typing import Any, Dict, List, Optional, Set, Tuple, Union
from uuid import uuid4 from uuid import uuid4
import math
import joblib import joblib
import nh3 import nh3
import numpy as np import numpy as np
from django.conf import settings as s from django.conf import settings as s
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
from django.contrib.contenttypes.models import ContentType from django.contrib.contenttypes.models import ContentType
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
from django.db import models, transaction from django.db import models, transaction
from django.db.models import JSONField, Count, Q, QuerySet from django.db.models import Count, JSONField, Q, QuerySet
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from envipy_additional_information import EnviPyModel from envipy_additional_information import EnviPyModel
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from sklearn.metrics import precision_score, recall_score, jaccard_score from sklearn.metrics import jaccard_score, precision_score, recall_score
from sklearn.model_selection import ShuffleSplit from sklearn.model_selection import ShuffleSplit
from utilities.chem import FormatConverter, ProductSet, PredictionResult, IndigoUtils from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
from utilities.ml import ( from utilities.ml import (
RuleBasedDataset,
ApplicabilityDomainPCA, ApplicabilityDomainPCA,
EnsembleClassifierChain,
RelativeReasoning,
EnviFormerDataset,
Dataset, Dataset,
EnsembleClassifierChain,
EnviFormerDataset,
RelativeReasoning,
RuleBasedDataset,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -44,8 +45,6 @@ logger = logging.getLogger(__name__)
########################## ##########################
# User/Groups/Permission # # User/Groups/Permission #
########################## ##########################
class User(AbstractUser): class User(AbstractUser):
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
uuid = models.UUIDField( uuid = models.UUIDField(
@ -53,7 +52,10 @@ class User(AbstractUser):
) )
url = models.TextField(blank=False, null=True, verbose_name="URL", unique=True) url = models.TextField(blank=False, null=True, verbose_name="URL", unique=True)
default_package = models.ForeignKey( default_package = models.ForeignKey(
"epdb.Package", verbose_name="Default Package", null=True, on_delete=models.SET_NULL s.EPDB_PACKAGE_MODEL,
verbose_name="Default Package",
null=True,
on_delete=models.SET_NULL,
) )
default_group = models.ForeignKey( default_group = models.ForeignKey(
"Group", "Group",
@ -243,7 +245,7 @@ class UserPackagePermission(Permission):
) )
user = models.ForeignKey("User", verbose_name="Permission to", on_delete=models.CASCADE) user = models.ForeignKey("User", verbose_name="Permission to", on_delete=models.CASCADE)
package = models.ForeignKey( package = models.ForeignKey(
"epdb.Package", verbose_name="Permission on", on_delete=models.CASCADE s.EPDB_PACKAGE_MODEL, verbose_name="Permission on", on_delete=models.CASCADE
) )
class Meta: class Meta:
@ -259,7 +261,7 @@ class GroupPackagePermission(Permission):
) )
group = models.ForeignKey("Group", verbose_name="Permission to", on_delete=models.CASCADE) group = models.ForeignKey("Group", verbose_name="Permission to", on_delete=models.CASCADE)
package = models.ForeignKey( package = models.ForeignKey(
"epdb.Package", verbose_name="Permission on", on_delete=models.CASCADE s.EPDB_PACKAGE_MODEL, verbose_name="Permission on", on_delete=models.CASCADE
) )
class Meta: class Meta:
@ -728,10 +730,13 @@ class Package(EnviPathModel):
rules = sorted(rules, key=lambda x: x.url) rules = sorted(rules, key=lambda x: x.url)
return rules return rules
class Meta:
swappable = "EPDB_PACKAGE_MODEL"
class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin): class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin):
package = models.ForeignKey( package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
) )
default_structure = models.ForeignKey( default_structure = models.ForeignKey(
"CompoundStructure", "CompoundStructure",
@ -781,7 +786,7 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def create( def create(
package: Package, smiles: str, name: str = None, description: str = None, *args, **kwargs package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
) -> "Compound": ) -> "Compound":
if smiles is None or smiles.strip() == "": if smiles is None or smiles.strip() == "":
raise ValueError("SMILES is required") raise ValueError("SMILES is required")
@ -1061,7 +1066,7 @@ class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin): class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
package = models.ForeignKey( package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
) )
# # https://github.com/django-polymorphic/django-polymorphic/issues/229 # # https://github.com/django-polymorphic/django-polymorphic/issues/229
@ -1074,6 +1079,10 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
def apply(self, *args, **kwargs): def apply(self, *args, **kwargs):
pass pass
@abc.abstractmethod
def get_rule_identifier(self) -> str:
pass
@staticmethod @staticmethod
def cls_for_type(rule_type: str): def cls_for_type(rule_type: str):
if rule_type == "SimpleAmbitRule": if rule_type == "SimpleAmbitRule":
@ -1167,7 +1176,7 @@ class SimpleAmbitRule(SimpleRule):
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def create( def create(
package: Package, package: "Package",
name: str = None, name: str = None,
description: str = None, description: str = None,
smirks: str = None, smirks: str = None,
@ -1228,6 +1237,9 @@ class SimpleAmbitRule(SimpleRule):
def _url(self): def _url(self):
return "{}/simple-ambit-rule/{}".format(self.package.url, self.uuid) return "{}/simple-ambit-rule/{}".format(self.package.url, self.uuid)
def get_rule_identifier(self) -> str:
return "simple-rule"
def apply(self, smiles): def apply(self, smiles):
return FormatConverter.apply(smiles, self.smirks) return FormatConverter.apply(smiles, self.smirks)
@ -1241,7 +1253,7 @@ class SimpleAmbitRule(SimpleRule):
@property @property
def related_reactions(self): def related_reactions(self):
qs = Package.objects.filter(reviewed=True) qs = s.GET_PACKAGE_MODEL().objects.filter(reviewed=True)
return self.reaction_rule.filter(package__in=qs).order_by("name") return self.reaction_rule.filter(package__in=qs).order_by("name")
@property @property
@ -1273,6 +1285,9 @@ class ParallelRule(Rule):
def _url(self): def _url(self):
return "{}/parallel-rule/{}".format(self.package.url, self.uuid) return "{}/parallel-rule/{}".format(self.package.url, self.uuid)
def get_rule_identifier(self) -> str:
return "parallel-rule"
@cached_property @cached_property
def srs(self) -> QuerySet: def srs(self) -> QuerySet:
return self.simple_rules.all() return self.simple_rules.all()
@ -1304,6 +1319,57 @@ class ParallelRule(Rule):
return res return res
@staticmethod
@transaction.atomic
def create(
package: "Package",
simple_rules: List["SimpleRule"],
name: str = None,
description: str = None,
reactant_filter_smarts: str = None,
product_filter_smarts: str = None,
):
if len(simple_rules) == 0:
raise ValueError("At least one simple rule is required!")
for sr in simple_rules:
if sr.package != package:
raise ValueError(
f"Simple rule {sr.uuid} does not belong to package {package.uuid}!"
)
r = ParallelRule()
r.package = package
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
r.name = name
if description is not None and description.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
if not FormatConverter.is_valid_smarts(reactant_filter_smarts.strip()):
raise ValueError(f'Reactant Filter SMARTS "{reactant_filter_smarts}" is invalid!')
else:
r.reactant_filter_smarts = reactant_filter_smarts.strip()
if product_filter_smarts is not None and product_filter_smarts.strip() != "":
if not FormatConverter.is_valid_smarts(product_filter_smarts.strip()):
raise ValueError(f'Product Filter SMARTS "{product_filter_smarts}" is invalid!')
else:
r.product_filter_smarts = product_filter_smarts.strip()
r.save()
for sr in simple_rules:
r.simple_rules.add(sr)
return r
class SequentialRule(Rule): class SequentialRule(Rule):
simple_rules = models.ManyToManyField( simple_rules = models.ManyToManyField(
@ -1313,6 +1379,9 @@ class SequentialRule(Rule):
def _url(self): def _url(self):
return "{}/sequential-rule/{}".format(self.compound.url, self.uuid) return "{}/sequential-rule/{}".format(self.compound.url, self.uuid)
def get_rule_identifier(self) -> str:
return "sequential-rule"
@property @property
def srs(self): def srs(self):
return self.simple_rules.all() return self.simple_rules.all()
@ -1333,7 +1402,7 @@ class SequentialRuleOrdering(models.Model):
class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin): class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin):
package = models.ForeignKey( package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
) )
educts = models.ManyToManyField( educts = models.ManyToManyField(
"epdb.CompoundStructure", verbose_name="Educts", related_name="reaction_educts" "epdb.CompoundStructure", verbose_name="Educts", related_name="reaction_educts"
@ -1355,7 +1424,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def create( def create(
package: Package, package: "Package",
name: str = None, name: str = None,
description: str = None, description: str = None,
educts: Union[List[str], List[CompoundStructure]] = None, educts: Union[List[str], List[CompoundStructure]] = None,
@ -1514,7 +1583,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
package = models.ForeignKey( package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
) )
setting = models.ForeignKey( setting = models.ForeignKey(
"epdb.Setting", verbose_name="Setting", on_delete=models.CASCADE, null=True, blank=True "epdb.Setting", verbose_name="Setting", on_delete=models.CASCADE, null=True, blank=True
@ -2076,7 +2145,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
class EPModel(PolymorphicModel, EnviPathModel): class EPModel(PolymorphicModel, EnviPathModel):
package = models.ForeignKey( package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
) )
def _url(self): def _url(self):
@ -2085,17 +2154,17 @@ class EPModel(PolymorphicModel, EnviPathModel):
class PackageBasedModel(EPModel): class PackageBasedModel(EPModel):
rule_packages = models.ManyToManyField( rule_packages = models.ManyToManyField(
"Package", s.EPDB_PACKAGE_MODEL,
verbose_name="Rule Packages", verbose_name="Rule Packages",
related_name="%(app_label)s_%(class)s_rule_packages", related_name="%(app_label)s_%(class)s_rule_packages",
) )
data_packages = models.ManyToManyField( data_packages = models.ManyToManyField(
"Package", s.EPDB_PACKAGE_MODEL,
verbose_name="Data Packages", verbose_name="Data Packages",
related_name="%(app_label)s_%(class)s_data_packages", related_name="%(app_label)s_%(class)s_data_packages",
) )
eval_packages = models.ManyToManyField( eval_packages = models.ManyToManyField(
"Package", s.EPDB_PACKAGE_MODEL,
verbose_name="Evaluation Packages", verbose_name="Evaluation Packages",
related_name="%(app_label)s_%(class)s_eval_packages", related_name="%(app_label)s_%(class)s_eval_packages",
) )
@ -3400,7 +3469,7 @@ class PluginModel(EPModel):
class Scenario(EnviPathModel): class Scenario(EnviPathModel):
package = models.ForeignKey( package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
) )
scenario_date = models.CharField(max_length=256, null=False, blank=False, default="No date") scenario_date = models.CharField(max_length=256, null=False, blank=False, default="No date")
scenario_type = models.CharField( scenario_type = models.CharField(
@ -3555,7 +3624,7 @@ class Setting(EnviPathModel):
) )
rule_packages = models.ManyToManyField( rule_packages = models.ManyToManyField(
"Package", s.EPDB_PACKAGE_MODEL,
verbose_name="Setting Rule Packages", verbose_name="Setting Rule Packages",
related_name="setting_rule_packages", related_name="setting_rule_packages",
blank=True, blank=True,

View File

@ -6,14 +6,17 @@ 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.conf import settings as s
from django.utils import timezone from django.utils import timezone
from epdb.logic import SPathway from epdb.logic import SPathway
from epdb.models import Edge, EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User from epdb.models import Edge, EPModel, JobLog, Node, 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.
Package = s.GET_PACKAGE_MODEL()
def get_ml_model(model_pk: int): def get_ml_model(model_pk: int):
if model_pk not in ML_CACHE: if model_pk not in ML_CACHE:

View File

@ -1,58 +1,60 @@
import json import json
import logging import logging
from typing import List, Dict, Any from typing import Any, Dict, List
import nh3
from django.conf import settings as s from django.conf import settings as s
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, JsonResponse
from django.shortcuts import render, redirect from django.shortcuts import redirect, render
from django.urls import reverse from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from envipy_additional_information import NAME_MAPPING from envipy_additional_information import NAME_MAPPING
from oauth2_provider.decorators import protected_resource from oauth2_provider.decorators import protected_resource
import nh3
from utilities.chem import FormatConverter, IndigoUtils from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required from utilities.decorators import package_permission_required
from utilities.misc import HTMLGenerator from utilities.misc import HTMLGenerator
from .logic import ( from .logic import (
EPDBURLParser,
GroupManager, GroupManager,
PackageManager, PackageManager,
UserManager,
SettingManager,
SearchManager, SearchManager,
EPDBURLParser, SettingManager,
UserManager,
) )
from .models import ( from .models import (
Package, APIToken,
GroupPackagePermission,
Group,
CompoundStructure,
Compound, Compound,
CompoundStructure,
Edge,
EnviFormer,
EnzymeLink,
EPModel,
ExternalDatabase,
ExternalIdentifier,
Group,
GroupPackagePermission,
JobLog,
License,
MLRelativeReasoning,
Node,
Pathway,
Permission,
Reaction, Reaction,
Rule, Rule,
Pathway,
Node,
EPModel,
EnviFormer,
MLRelativeReasoning,
RuleBasedRelativeReasoning, RuleBasedRelativeReasoning,
Scenario, Scenario,
SimpleAmbitRule, SimpleAmbitRule,
APIToken,
UserPackagePermission,
Permission,
License,
User, User,
Edge, UserPackagePermission,
ExternalDatabase,
ExternalIdentifier,
EnzymeLink,
JobLog,
) )
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
Package = s.GET_PACKAGE_MODEL()
def log_post_params(request): def log_post_params(request):
if s.DEBUG: if s.DEBUG:
@ -60,6 +62,26 @@ def log_post_params(request):
logger.debug(f"{k}\t{v}") logger.debug(f"{k}\t{v}")
def get_error_handler_context(request, for_user=None) -> Dict[str, Any]:
current_user = _anonymous_or_real(request)
if for_user:
current_user = for_user
ctx = {
"title": "enviPath",
"meta": {
"site_id": s.MATOMO_SITE_ID,
"version": "0.0.1",
"server_url": s.SERVER_URL,
"user": current_user,
"enabled_features": s.FLAGS,
"debug": s.DEBUG,
},
}
return ctx
def error(request, message: str, detail: str, code: int = 400): def error(request, message: str, detail: str, code: int = 400):
context = get_base_context(request) context = get_base_context(request)
error_context = { error_context = {
@ -74,6 +96,48 @@ def error(request, message: str, detail: str, code: int = 400):
return render(request, "errors/error.html", context, status=code) return render(request, "errors/error.html", context, status=code)
def handler400(request, exception):
"""Custom 400 Bad Request error handler"""
context = get_error_handler_context(request)
context["public_mode"] = True
return render(request, "errors/400_bad_request.html", context, status=400)
def handler403(request, exception):
"""Custom 403 Forbidden error handler"""
context = get_error_handler_context(request)
context["public_mode"] = True
return render(request, "errors/403_access_denied.html", context, status=403)
def handler404(request, exception):
"""Custom 404 Not Found error handler"""
context = get_error_handler_context(request)
context["public_mode"] = True
return render(request, "errors/404_not_found.html", context, status=404)
def handler500(request):
"""Custom 500 Internal Server Error handler"""
context = get_error_handler_context(request)
error_context = {}
error_context["error_message"] = "Internal Server Error"
error_context["error_detail"] = "An unexpected error occurred. Please try again later."
if request.headers.get("Accept") == "application/json":
return JsonResponse(error_context, status=500)
context["public_mode"] = True
context["error_code"] = 500
context["error_description"] = (
"We encountered an unexpected error while processing your request. Our team has been notified and is working to resolve the issue."
)
context.update(**error_context)
return render(request, "errors/error.html", context, status=500)
def login(request): def login(request):
context = get_base_context(request) context = get_base_context(request)
@ -83,8 +147,7 @@ def login(request):
return render(request, "static/login.html", context) return render(request, "static/login.html", context)
elif request.method == "POST": elif request.method == "POST":
from django.contrib.auth import authenticate from django.contrib.auth import authenticate, login
from django.contrib.auth import login
username = request.POST.get("username").strip() username = request.POST.get("username").strip()
if username != request.POST.get("username"): if username != request.POST.get("username"):
@ -191,8 +254,8 @@ def register(request):
def editable(request, user): def editable(request, user):
if user.is_superuser: # if user.is_superuser:
return True # return True
url = request.build_absolute_uri(request.path) url = request.build_absolute_uri(request.path)
if PackageManager.is_package_url(url): if PackageManager.is_package_url(url):
@ -872,7 +935,7 @@ def package_models(request, package_uuid):
request, "Invalid model type.", f'Model type "{model_type}" is not supported."' request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
) )
from .tasks import dispatch, build_model from .tasks import build_model, dispatch
dispatch(current_user, build_model, mod.pk) dispatch(current_user, build_model, mod.pk)
@ -906,9 +969,10 @@ def package_model(request, package_uuid, model_uuid):
if classify: if classify:
from epdb.tasks import dispatch_eager, predict_simple from epdb.tasks import dispatch_eager, predict_simple
res = dispatch_eager(current_user, predict_simple, current_model.pk, stand_smiles) pred_res = dispatch_eager(
current_user, predict_simple, current_model.pk, stand_smiles
)
pred_res = current_model.predict(stand_smiles)
res = [] res = []
for pr in pred_res: for pr in pred_res:
@ -1068,9 +1132,7 @@ def package(request, package_uuid):
return redirect(s.SERVER_URL + "/package") return redirect(s.SERVER_URL + "/package")
elif hidden == "publish-package": elif hidden == "publish-package":
for g in Group.objects.filter(public=True): for g in Group.objects.filter(public=True):
PackageManager.update_permissions( PackageManager.grant_read(current_user, current_package, g)
current_user, current_package, g, Permission.READ[0]
)
return redirect(current_package.url) return redirect(current_package.url)
elif hidden == "copy": elif hidden == "copy":
object_to_copy = request.POST.get("object_to_copy") object_to_copy = request.POST.get("object_to_copy")
@ -2409,9 +2471,9 @@ def package_scenarios(request, package_uuid):
context["unreviewed_objects"] = unreviewed_scenario_qs context["unreviewed_objects"] = unreviewed_scenario_qs
from envipy_additional_information import ( from envipy_additional_information import (
SEDIMENT_ADDITIONAL_INFORMATION,
SLUDGE_ADDITIONAL_INFORMATION, SLUDGE_ADDITIONAL_INFORMATION,
SOIL_ADDITIONAL_INFORMATION, SOIL_ADDITIONAL_INFORMATION,
SEDIMENT_ADDITIONAL_INFORMATION,
) )
context["scenario_types"] = { context["scenario_types"] = {

View File

@ -1,24 +1,21 @@
import gzip
import json import json
import logging import logging
import os.path import os.path
from datetime import datetime
from django.conf import settings as s from django.conf import settings as s
from django.http import HttpResponseNotAllowed from django.http import HttpResponseNotAllowed
from django.shortcuts import render from django.shortcuts import render
from epdb.logic import PackageManager
from epdb.models import Rule, SimpleAmbitRule, Package, CompoundStructure
from epdb.views import get_base_context, _anonymous_or_real
from utilities.chem import FormatConverter
from rdkit import Chem from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize from rdkit.Chem.MolStandardize import rdMolStandardize
from epdb.models import CompoundStructure, Rule, SimpleAmbitRule
from epdb.views import get_base_context
from utilities.chem import FormatConverter
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
Package = s.GET_PACKAGE_MODEL()
def normalize_smiles(smiles): def normalize_smiles(smiles):
m1 = Chem.MolFromSmiles(smiles) m1 = Chem.MolFromSmiles(smiles)
@ -59,9 +56,7 @@ def run_both_engines(SMILES, SMIRKS):
set( set(
[ [
normalize_smiles(str(x)) normalize_smiles(str(x))
for x in FormatConverter.sanitize_smiles( for x in FormatConverter.sanitize_smiles([str(s) for s in all_rdkit_prods])[0]
[str(s) for s in all_rdkit_prods]
)[0]
] ]
) )
) )
@ -85,8 +80,7 @@ def migration(request):
url="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1" url="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1"
) )
ALL_SMILES = [ ALL_SMILES = [
cs.smiles cs.smiles for cs in CompoundStructure.objects.filter(compound__package=BBD)
for cs in CompoundStructure.objects.filter(compound__package=BBD)
] ]
RULES = SimpleAmbitRule.objects.filter(package=BBD) RULES = SimpleAmbitRule.objects.filter(package=BBD)
@ -142,9 +136,7 @@ def migration(request):
) )
for r in migration_status["results"]: for r in migration_status["results"]:
r["detail_url"] = r["detail_url"].replace( r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
"http://localhost:8000", s.SERVER_URL
)
context.update(**migration_status) context.update(**migration_status)
@ -152,8 +144,6 @@ def migration(request):
def migration_detail(request, package_uuid, rule_uuid): def migration_detail(request, package_uuid, rule_uuid):
current_user = _anonymous_or_real(request)
if request.method == "GET": if request.method == "GET":
context = get_base_context(request) context = get_base_context(request)
@ -235,9 +225,7 @@ def compare(request):
context["smirks"] = ( context["smirks"] = (
"[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([#1,#6:7])[H:3].[#6;A:2]=O" "[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([#1,#6:7])[H:3].[#6;A:2]=O"
) )
context["smiles"] = ( context["smiles"] = "C(CC(=O)N[C@@H](CS[Se-])C(=O)NCC(=O)[O-])[C@@H](C(=O)[O-])N"
"C(CC(=O)N[C@@H](CS[Se-])C(=O)NCC(=O)[O-])[C@@H](C(=O)[O-])N"
)
return render(request, "compare.html", context) return render(request, "compare.html", context)
elif request.method == "POST": elif request.method == "POST":

View File

@ -34,7 +34,7 @@ dependencies = [
[tool.uv.sources] [tool.uv.sources]
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" } enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" }
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" } envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7"} envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7" }
envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" } envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }
[project.optional-dependencies] [project.optional-dependencies]
@ -45,6 +45,8 @@ dev = [
"poethepoet>=0.37.0", "poethepoet>=0.37.0",
"pre-commit>=4.3.0", "pre-commit>=4.3.0",
"ruff>=0.13.3", "ruff>=0.13.3",
"pytest-playwright>=0.7.1",
"pytest-django>=4.11.1",
] ]
[tool.ruff] [tool.ruff]
@ -66,47 +68,31 @@ docstring-code-format = true
[tool.poe.tasks] [tool.poe.tasks]
# Main tasks # Main tasks
setup = { sequence = ["db-up", "migrate", "bootstrap"], help = "Complete setup: start database, run migrations, and bootstrap data" } setup = { sequence = [
dev = { shell = """ "db-up",
# Start pnpm CSS watcher in background "migrate",
pnpm run dev & "bootstrap",
PNPM_PID=$! ], help = "Complete setup: start database, run migrations, and bootstrap data" }
echo "Started CSS watcher (PID: $PNPM_PID)" dev = { cmd = "uv run python scripts/dev_server.py", help = "Start the development server with CSS watcher", deps = [
"db-up",
# Cleanup function "js-deps",
cleanup() { ] }
echo "\nShutting down..." build = { sequence = [
if kill -0 $PNPM_PID 2>/dev/null; then "build-frontend",
kill $PNPM_PID "collectstatic",
echo " CSS watcher stopped" ], help = "Build frontend assets and collect static files" }
fi
if [ ! -z "${DJ_PID:-}" ] && kill -0 $DJ_PID 2>/dev/null; then
kill $DJ_PID
echo " Django server stopped"
fi
}
# Set trap for cleanup
trap cleanup EXIT INT TERM
# Start Django dev server in background
uv run python manage.py runserver &
DJ_PID=$!
# Wait for Django to finish
wait $DJ_PID
""", help = "Start the development server with CSS watcher", deps = ["db-up", "js-deps"] }
build = { sequence = ["build-frontend", "collectstatic"], help = "Build frontend assets and collect static files" }
# Database tasks # Database tasks
db-up = { cmd = "docker compose -f docker-compose.dev.yml up -d", help = "Start PostgreSQL database using Docker Compose" } db-up = { cmd = "docker compose -f docker-compose.dev.yml up -d", help = "Start PostgreSQL database using Docker Compose" }
db-down = { cmd = "docker compose -f docker-compose.dev.yml down", help = "Stop PostgreSQL database" } db-down = { cmd = "docker compose -f docker-compose.dev.yml down", help = "Stop PostgreSQL database" }
# Frontend tasks # Frontend tasks
js-deps = { cmd = "pnpm install", help = "Install frontend dependencies" } js-deps = { cmd = "uv run python scripts/pnpm_wrapper.py install", help = "Install frontend dependencies" }
# Full cleanup tasks # Full cleanup tasks
clean = { sequence = ["clean-db"], help = "Remove model files and database volumes (WARNING: destroys all data!)" } clean = { sequence = [
"clean-db",
], help = "Remove model files and database volumes (WARNING: destroys all data!)" }
clean-db = { cmd = "docker compose -f docker-compose.dev.yml down -v", help = "Removes the database container and volume." } clean-db = { cmd = "docker compose -f docker-compose.dev.yml down -v", help = "Removes the database container and volume." }
# Django tasks # Django tasks
@ -121,9 +107,17 @@ echo "Default admin credentials:"
echo " Username: admin" echo " Username: admin"
echo " Email: admin@envipath.com" echo " Email: admin@envipath.com"
echo " Password: SuperSafe" echo " Password: SuperSafe"
""", help = "Bootstrap initial data (anonymous user, packages, models)" } """, help = "Bootstrap initial data (anonymous user, packages, models)" }
shell = { cmd = "uv run python manage.py shell", help = "Open Django shell" } shell = { cmd = "uv run python manage.py shell", help = "Open Django shell" }
# Build tasks
build-frontend = { cmd = "pnpm run build", help = "Build frontend assets using pnpm", deps = ["js-deps"] } build-frontend = { cmd = "uv run python scripts/pnpm_wrapper.py run build", help = "Build frontend assets using pnpm", deps = [
collectstatic = { cmd = "uv run python manage.py collectstatic --noinput", help = "Collect static files for production", deps = ["build-frontend"] } "js-deps",
] } # Build tasks
collectstatic = { cmd = "uv run python manage.py collectstatic --noinput", help = "Collect static files for production", deps = [
"build-frontend",
] }
frontend-test-setup = { cmd = "playwright install", help = "Install the browsers required for frontend testing" }

201
scripts/dev_server.py Executable file
View File

@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
Cross-platform development server script.
Starts pnpm CSS watcher and Django dev server, handling cleanup on exit.
Works on both Windows and Unix systems.
"""
import atexit
import shutil
import signal
import subprocess
import sys
import time
def find_pnpm():
"""
Find pnpm executable on the system.
Returns the path to pnpm or None if not found.
"""
# Try to find pnpm using shutil.which
# On Windows, this will find pnpm.cmd if it's in PATH
pnpm_path = shutil.which("pnpm")
if pnpm_path:
return pnpm_path
# On Windows, also try pnpm.cmd explicitly
if sys.platform == "win32":
pnpm_cmd = shutil.which("pnpm.cmd")
if pnpm_cmd:
return pnpm_cmd
return None
class DevServerManager:
"""Manages background processes for development server."""
def __init__(self):
self.processes = []
self._cleanup_registered = False
def start_process(self, command, description, shell=False):
"""Start a background process and return the process object."""
print(f"Starting {description}...")
try:
if shell:
# Use shell=True for commands that need shell interpretation
process = subprocess.Popen(
command,
shell=True,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
else:
# Split command into list for subprocess
process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
bufsize=1,
)
self.processes.append((process, description))
print(f"✓ Started {description} (PID: {process.pid})")
return process
except Exception as e:
print(f"✗ Failed to start {description}: {e}", file=sys.stderr)
self.cleanup()
sys.exit(1)
def cleanup(self):
"""Terminate all running processes."""
if not self.processes:
return
print("\nShutting down...")
for process, description in self.processes:
if process.poll() is None: # Process is still running
try:
# Try graceful termination first
if sys.platform == "win32":
process.terminate()
else:
process.send_signal(signal.SIGTERM)
# Wait up to 5 seconds for graceful shutdown
try:
process.wait(timeout=5)
except subprocess.TimeoutExpired:
# Force kill if graceful shutdown failed
if sys.platform == "win32":
process.kill()
else:
process.send_signal(signal.SIGKILL)
process.wait()
print(f"{description} stopped")
except Exception as e:
print(f"✗ Error stopping {description}: {e}", file=sys.stderr)
self.processes.clear()
def register_cleanup(self):
"""Register cleanup handlers for various exit scenarios."""
if self._cleanup_registered:
return
self._cleanup_registered = True
# Register atexit handler (works on all platforms)
atexit.register(self.cleanup)
# Register signal handlers (Unix only)
if sys.platform != "win32":
signal.signal(signal.SIGINT, self._signal_handler)
signal.signal(signal.SIGTERM, self._signal_handler)
def _signal_handler(self, signum, frame):
"""Handle Unix signals."""
self.cleanup()
sys.exit(0)
def wait_for_process(self, process, description):
"""Wait for a process to finish and handle its output."""
try:
# Stream output from the process
for line in iter(process.stdout.readline, ""):
if line:
print(f"[{description}] {line.rstrip()}")
process.wait()
return process.returncode
except KeyboardInterrupt:
# Handle Ctrl+C
self.cleanup()
sys.exit(0)
except Exception as e:
print(f"Error waiting for {description}: {e}", file=sys.stderr)
self.cleanup()
sys.exit(1)
def main():
"""Main entry point."""
manager = DevServerManager()
manager.register_cleanup()
# Find pnpm executable
pnpm_path = find_pnpm()
if not pnpm_path:
print("Error: pnpm not found in PATH.", file=sys.stderr)
print("\nPlease install pnpm:", file=sys.stderr)
print(" Windows: https://pnpm.io/installation#on-windows", file=sys.stderr)
print(" Unix: https://pnpm.io/installation#on-posix-systems", file=sys.stderr)
sys.exit(1)
# Determine shell usage based on platform
use_shell = sys.platform == "win32"
# Start pnpm CSS watcher
# Use the found pnpm path to ensure it works on Windows
pnpm_command = f'"{pnpm_path}" run dev' if use_shell else [pnpm_path, "run", "dev"]
manager.start_process(
pnpm_command,
"CSS watcher",
shell=use_shell,
)
# Give pnpm a moment to start
time.sleep(1)
# Start Django dev server
django_process = manager.start_process(
["uv", "run", "python", "manage.py", "runserver"],
"Django server",
shell=False,
)
print("\nDevelopment servers are running. Press Ctrl+C to stop.\n")
try:
# Wait for Django server (main process)
# If Django exits, we should clean up everything
return_code = manager.wait_for_process(django_process, "Django")
# If Django exited unexpectedly, clean up and exit
if return_code != 0:
manager.cleanup()
sys.exit(return_code)
except KeyboardInterrupt:
# Ctrl+C was pressed
manager.cleanup()
sys.exit(0)
if __name__ == "__main__":
main()

59
scripts/pnpm_wrapper.py Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""
Cross-platform pnpm command wrapper.
Finds pnpm correctly on Windows (handles pnpm.cmd) and Unix systems.
"""
import shutil
import subprocess
import sys
def find_pnpm():
"""
Find pnpm executable on the system.
Returns the path to pnpm or None if not found.
"""
# Try to find pnpm using shutil.which
# On Windows, this will find pnpm.cmd if it's in PATH
pnpm_path = shutil.which("pnpm")
if pnpm_path:
return pnpm_path
# On Windows, also try pnpm.cmd explicitly
if sys.platform == "win32":
pnpm_cmd = shutil.which("pnpm.cmd")
if pnpm_cmd:
return pnpm_cmd
return None
def main():
"""Main entry point - execute pnpm with provided arguments."""
pnpm_path = find_pnpm()
if not pnpm_path:
print("Error: pnpm not found in PATH.", file=sys.stderr)
print("\nPlease install pnpm:", file=sys.stderr)
print(" Windows: https://pnpm.io/installation#on-windows", file=sys.stderr)
print(" Unix: https://pnpm.io/installation#on-posix-systems", file=sys.stderr)
sys.exit(1)
# Get all arguments passed to this script
args = sys.argv[1:]
# Execute pnpm with the provided arguments
try:
sys.exit(subprocess.call([pnpm_path] + args))
except KeyboardInterrupt:
# Handle Ctrl+C gracefully
sys.exit(130)
except Exception as e:
print(f"Error executing pnpm: {e}", file=sys.stderr)
sys.exit(1)
if __name__ == "__main__":
main()

View File

@ -34,3 +34,30 @@
} }
@import "./daisyui-theme.css"; @import "./daisyui-theme.css";
/* Loading Spinner - Benzene Ring */
.loading-spinner {
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
}
.loading-spinner svg {
width: 48px;
height: 48px;
animation: spin 2s linear infinite;
}
.loading-spinner .hexagon,
.loading-spinner .double-bonds {
fill: none;
stroke: currentColor;
stroke-width: 2;
}
@keyframes spin {
100% {
transform: rotate(360deg);
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 814 B

265
static/js/alpine/index.js Normal file
View File

@ -0,0 +1,265 @@
/**
* Alpine.js Components for enviPath
*
* This module provides reusable Alpine.js data components for modals,
* form validation, and form submission.
*/
document.addEventListener('alpine:init', () => {
/**
* Modal Form Component
*
* Provides form validation using HTML5 Constraint Validation API,
* loading states for submission, and error message management.
*
* Basic Usage:
* <dialog x-data="modalForm()" @close="reset()">
* <form id="my-form">
* <input name="field" required>
* </form>
* <button @click="submit('my-form')" :disabled="isSubmitting">Submit</button>
* </dialog>
*
* With Custom State:
* <dialog x-data="modalForm({ state: { selectedItem: '', imageUrl: '' } })" @close="reset()">
* <select x-model="selectedItem" @change="updateImagePreview(selectedItem + '?image=svg')">
* <img :src="imageUrl" x-show="imageUrl">
* </dialog>
*
* With AJAX:
* <button @click="submitAsync('my-form', { onSuccess: (data) => console.log(data) })">
*/
Alpine.data('modalForm', (options = {}) => ({
isSubmitting: false,
errors: {},
// Spread custom initial state from options
...(options.state || {}),
/**
* Validate a single field using HTML5 Constraint Validation API
* @param {HTMLElement} field - The input/select/textarea element
*/
validateField(field) {
const name = field.name || field.id;
if (!name) return;
if (!field.validity.valid) {
this.errors[name] = field.validationMessage;
} else {
delete this.errors[name];
}
},
/**
* Clear error for a field (call on input)
* @param {HTMLElement} field - The input element
*/
clearError(field) {
const name = field.name || field.id;
if (name && this.errors[name]) {
delete this.errors[name];
}
},
/**
* Get error message for a field
* @param {string} name - Field name
* @returns {string|undefined} Error message or undefined
*/
getError(name) {
return this.errors[name];
},
/**
* Check if form has any errors
* @returns {boolean} True if there are errors
*/
hasErrors() {
return Object.keys(this.errors).length > 0;
},
/**
* Validate all fields in a form
* @param {string} formId - The form element ID
* @returns {boolean} True if form is valid
*/
validateAll(formId) {
const form = document.getElementById(formId);
if (!form) return false;
this.errors = {};
const fields = form.querySelectorAll('input, select, textarea');
fields.forEach(field => {
if (field.name && !field.validity.valid) {
this.errors[field.name] = field.validationMessage;
}
});
return !this.hasErrors();
},
/**
* Validate that two password fields match
* @param {string} password1Id - ID of first password field
* @param {string} password2Id - ID of second password field
* @returns {boolean} True if passwords match
*/
validatePasswordMatch(password1Id, password2Id) {
const pw1 = document.getElementById(password1Id);
const pw2 = document.getElementById(password2Id);
if (!pw1 || !pw2) return false;
if (pw1.value !== pw2.value) {
this.errors[pw2.name || password2Id] = 'Passwords do not match';
pw2.setCustomValidity('Passwords do not match');
return false;
}
delete this.errors[pw2.name || password2Id];
pw2.setCustomValidity('');
return true;
},
/**
* Submit a form with loading state
* @param {string} formId - The form element ID
*/
submit(formId) {
const form = document.getElementById(formId);
if (!form) return;
// Validate before submit
if (!form.checkValidity()) {
form.reportValidity();
return;
}
// Set action to current URL if empty
if (!form.action || form.action === window.location.href + '#') {
form.action = window.location.href;
}
// Set loading state and submit
this.isSubmitting = true;
form.submit();
},
/**
* Submit form via AJAX (fetch)
* @param {string} formId - The form element ID
* @param {Object} options - Options { onSuccess, onError, closeOnSuccess }
*/
async submitAsync(formId, options = {}) {
const form = document.getElementById(formId);
if (!form) return;
// Validate before submit
if (!form.checkValidity()) {
form.reportValidity();
return;
}
this.isSubmitting = true;
try {
const formData = new FormData(form);
const response = await fetch(form.action || window.location.href, {
method: form.method || 'POST',
body: formData,
headers: {
'X-Requested-With': 'XMLHttpRequest',
},
});
const data = await response.json().catch(() => ({}));
if (response.ok) {
if (options.onSuccess) {
options.onSuccess(data);
}
if (data.redirect || data.success) {
window.location.href = data.redirect || data.success;
} else if (options.closeOnSuccess) {
this.$el.closest('dialog')?.close();
}
} else {
const errorMsg = data.error || data.message || `Error: ${response.status}`;
this.errors['_form'] = errorMsg;
if (options.onError) {
options.onError(errorMsg, data);
}
}
} catch (error) {
this.errors['_form'] = error.message;
if (options.onError) {
options.onError(error.message);
}
} finally {
this.isSubmitting = false;
}
},
/**
* Set form action URL dynamically
* @param {string} formId - The form element ID
* @param {string} url - The URL to set as action
*/
setFormAction(formId, url) {
const form = document.getElementById(formId);
if (form) {
form.action = url;
}
},
/**
* Update image preview
* @param {string} url - Image URL (with query params)
* @param {string} targetId - Target element ID for the image
*/
updateImagePreview(url) {
// Store URL for reactive binding with :src
this.imageUrl = url;
},
/**
* Reset form state (call on modal close)
* Resets to initial state from options
*/
reset() {
this.isSubmitting = false;
this.errors = {};
this.imageUrl = '';
// Reset custom state to initial values
if (options.state) {
Object.keys(options.state).forEach(key => {
this[key] = options.state[key];
});
}
// Call custom reset handler if provided
if (options.onReset) {
options.onReset.call(this);
}
}
}));
/**
* Simple Modal Component (no form)
*
* For modals that don't need form validation.
*
* Usage:
* <dialog x-data="modal()">
* <button @click="$el.closest('dialog').close()">Close</button>
* </dialog>
*/
Alpine.data('modal', () => ({
// Placeholder for simple modals that may need state later
}));
});

View File

@ -0,0 +1,133 @@
/**
* Alpine.js Pagination Component
*
* Provides client-side pagination for large lists.
*/
document.addEventListener('alpine:init', () => {
Alpine.data('paginatedList', (initialItems = [], options = {}) => ({
allItems: initialItems,
filteredItems: [],
currentPage: 1,
perPage: options.perPage || 50,
searchQuery: '',
isReviewed: options.isReviewed || false,
instanceId: options.instanceId || Math.random().toString(36).substring(2, 9),
init() {
this.filteredItems = this.allItems;
},
get totalPages() {
return Math.ceil(this.filteredItems.length / this.perPage);
},
get paginatedItems() {
const start = (this.currentPage - 1) * this.perPage;
const end = start + this.perPage;
return this.filteredItems.slice(start, end);
},
get totalItems() {
return this.filteredItems.length;
},
get showingStart() {
if (this.totalItems === 0) return 0;
return (this.currentPage - 1) * this.perPage + 1;
},
get showingEnd() {
return Math.min(this.currentPage * this.perPage, this.totalItems);
},
search(query) {
this.searchQuery = query.toLowerCase();
if (this.searchQuery === '') {
this.filteredItems = this.allItems;
} else {
this.filteredItems = this.allItems.filter(item =>
item.name.toLowerCase().includes(this.searchQuery)
);
}
this.currentPage = 1;
},
nextPage() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
}
},
prevPage() {
if (this.currentPage > 1) {
this.currentPage--;
}
},
goToPage(page) {
if (page >= 1 && page <= this.totalPages) {
this.currentPage = page;
}
},
get pageNumbers() {
const pages = [];
const total = this.totalPages;
const current = this.currentPage;
// Handle empty case
if (total === 0) {
return pages;
}
if (total <= 7) {
// Show all pages if 7 or fewer
for (let i = 1; i <= total; i++) {
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
}
} else {
// More than 7 pages - show first, last, and sliding window around current
// Always show first page
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
// Determine the start and end of the middle range
let rangeStart, rangeEnd;
if (current <= 4) {
// Near the beginning: show pages 2-5
rangeStart = 2;
rangeEnd = 5;
} else if (current >= total - 3) {
// Near the end: show last 4 pages before the last page
rangeStart = total - 4;
rangeEnd = total - 1;
} else {
// In the middle: show current page and one on each side
rangeStart = current - 1;
rangeEnd = current + 1;
}
// Add ellipsis before range if there's a gap
if (rangeStart > 2) {
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
}
// Add pages in the range
for (let i = rangeStart; i <= rangeEnd; i++) {
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
}
// Add ellipsis after range if there's a gap
if (rangeEnd < total - 1) {
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
}
// Always show last page
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
}
return pages;
}
}));
});

145
static/js/alpine/search.js Normal file
View File

@ -0,0 +1,145 @@
/**
* Search Modal Alpine.js Component
*
* Provides package selection, search mode switching, and results display
* for the search modal.
*/
document.addEventListener('alpine:init', () => {
/**
* Search Modal Component
*
* Usage:
* <dialog x-data="searchModal()" @close="reset()">
* ...
* </dialog>
*/
Alpine.data('searchModal', () => ({
// Package selector state
selectedPackages: [],
// Search state
searchMode: 'text',
searchModeLabel: 'Text',
query: '',
// Results state
results: null,
isSearching: false,
error: null,
// Initialize on modal open
init() {
// Load reviewed packages by default
this.loadInitialSelection();
// Watch for modal open to focus searchbar
this.$watch('$el.open', (open) => {
if (open) {
setTimeout(() => {
this.$refs.searchbar.focus();
}, 320);
}
});
},
loadInitialSelection() {
// Select all reviewed packages by default
const menuItems = this.$refs.packageDropdown.querySelectorAll('li');
for (const item of menuItems) {
// Stop at 'Unreviewed Packages' section
if (item.classList.contains('menu-title') &&
item.textContent.trim() === 'Unreviewed Packages') {
break;
}
const packageOption = item.querySelector('.package-option');
if (packageOption) {
this.selectedPackages.push({
url: packageOption.dataset.packageUrl,
name: packageOption.dataset.packageName
});
}
}
},
togglePackage(url, name) {
const index = this.selectedPackages.findIndex(pkg => pkg.url === url);
if (index !== -1) {
this.selectedPackages.splice(index, 1);
} else {
this.selectedPackages.push({ url, name });
}
},
removePackage(url) {
const index = this.selectedPackages.findIndex(pkg => pkg.url === url);
if (index !== -1) {
this.selectedPackages.splice(index, 1);
}
},
isPackageSelected(url) {
return this.selectedPackages.some(pkg => pkg.url === url);
},
setSearchMode(mode, label) {
this.searchMode = mode;
this.searchModeLabel = label;
this.$refs.modeDropdown.hidePopover();
},
async performSearch(serverBase) {
if (!this.query.trim()) {
return;
}
if (this.selectedPackages.length < 1) {
this.results = { error: 'no_packages' };
return;
}
const params = new URLSearchParams();
this.selectedPackages.forEach(pkg => params.append('packages', pkg.url));
params.append('search', this.query.trim());
params.append('mode', this.searchModeLabel.toLowerCase());
this.isSearching = true;
this.results = null;
this.error = null;
try {
const response = await fetch(`${serverBase}/search?${params.toString()}`, {
method: 'GET',
headers: { 'Accept': 'application/json' }
});
if (!response.ok) {
throw new Error('Search request failed');
}
this.results = await response.json();
} catch (err) {
console.error('Search error:', err);
this.error = 'Search failed. Please try again.';
} finally {
this.isSearching = false;
}
},
hasResults() {
if (!this.results || this.results.error) return false;
const categories = ['Compounds', 'Compound Structures', 'Rules', 'Reactions', 'Pathways'];
return categories.some(cat => this.results[cat] && this.results[cat].length > 0);
},
reset() {
this.query = '';
this.results = null;
this.error = null;
this.isSearching = false;
}
}));
});

View File

@ -63,17 +63,20 @@ class DiscourseAPI {
* @returns {string} Cleaned excerpt * @returns {string} Cleaned excerpt
*/ */
extractExcerpt(excerpt) { extractExcerpt(excerpt) {
if (!excerpt) return 'Click to read more'; if (!excerpt) return 'No preview available yet';
// Remove HTML tags and clean up; collapse whitespace; do not add manual ellipsis // Remove HTML tags and clean up; collapse whitespace; do not add manual ellipsis
return excerpt const cleaned = excerpt
.replace(/<[^>]*>/g, '') // Remove HTML tags .replace(/<[^>]*>/g, '') // Remove HTML tags
.replace(/&nbsp;/g, ' ') // Replace &nbsp; with spaces .replace(/&nbsp;/g, ' ') // Replace &nbsp; with spaces
.replace(/&amp;/g, '&') // Replace &amp; with & .replace(/&amp;/g, '&') // Replace &amp; with &
.replace(/&lt;/g, '<') // Replace &lt; with < .replace(/&lt;/g, '<') // Replace &lt; with <
.replace(/&gt;/g, '>') // Replace &gt; with > .replace(/&gt;/g, '>') // Replace &gt; with >
.replace(/\s+/g, ' ') // Collapse all whitespace/newlines .replace(/\s+/g, ' ') // Collapse all whitespace/newlines
.trim() .trim();
// Check if excerpt is empty after cleaning
return cleaned || 'No preview available yet';
} }
/** /**

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,22 @@
console.log("loaded pw.js") console.log("loaded pw.js")
function predictFromNode(url) { function predictFromNode(url) {
$.post("", {node: url}) fetch("", {
.done(function (data) { method: "POST",
console.log("Success:", data); headers: {
window.location.href = data.success; "Content-Type": "application/x-www-form-urlencoded",
}) "X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
.fail(function (xhr, status, error) { },
console.error("Error:", xhr.status, xhr.responseText); body: new URLSearchParams({node: url})
// show user-friendly message or log error })
}); .then(response => response.json())
.then(data => {
console.log("Success:", data);
window.location.href = data.success;
})
.catch(error => {
console.error("Error:", error);
});
} }
// data = {{ pathway.d3_json | safe }}; // data = {{ pathway.d3_json | safe }};
@ -103,6 +110,9 @@ function draw(pathway, elem) {
} }
function dragstarted(event, d) { function dragstarted(event, d) {
// Prevent zoom pan when dragging nodes
event.sourceEvent.stopPropagation();
if (!event.active) simulation.alphaTarget(0.3).restart(); if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x; d.fx = d.x;
d.fy = d.y; d.fy = d.y;
@ -117,6 +127,9 @@ function draw(pathway, elem) {
} }
function dragged(event, d) { function dragged(event, d) {
// Prevent zoom pan when dragging nodes
event.sourceEvent.stopPropagation();
d.fx = event.x; d.fx = event.x;
d.fy = event.y; d.fy = event.y;
@ -127,6 +140,9 @@ function draw(pathway, elem) {
} }
function dragended(event, d) { function dragended(event, d) {
// Prevent zoom pan when dragging nodes
event.sourceEvent.stopPropagation();
if (!event.active) simulation.alphaTarget(0); if (!event.active) simulation.alphaTarget(0);
// Mark that dragging has ended // Mark that dragging has ended
@ -192,52 +208,153 @@ function draw(pathway, elem) {
d3.select(t).select("circle").classed("highlighted", !d3.select(t).select("circle").classed("highlighted")); d3.select(t).select("circle").classed("highlighted", !d3.select(t).select("circle").classed("highlighted"));
} }
// Wait one second before showing popup // Wait before showing popup (ms)
var popupWaitBeforeShow = 1000; var popupWaitBeforeShow = 1000;
// Keep Popup at least for one second
var popushowAtLeast = 1000;
function pop_show_e(element) { // Custom popover element
var e = element; let popoverTimeout = null;
setTimeout(function () {
if ($(e).is(':hover')) { // if element is still hovered
$(e).popover("show");
// workaround to set fixed positions function createPopover() {
pop = $(e).attr("aria-describedby") const popover = document.createElement('div');
h = $('#' + pop).height(); popover.id = 'custom-popover';
$('#' + pop).attr("style", `position: fixed; top: ${clientY - (h / 2.0)}px; left: ${clientX + 10}px; margin: 0px; max-width: 1000px; display: block;`) popover.className = 'fixed z-50';
setTimeout(function () { popover.style.cssText = `
var close = setInterval(function () { background: #ffffff;
if (!$(".popover:hover").length // mouse outside popover border: 1px solid #d1d5db;
&& !$(e).is(':hover')) { // mouse outside element box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
$(e).popover('hide'); max-width: 320px;
clearInterval(close); padding: 0.75rem;
} border-radius: 0.5rem;
}, 100); opacity: 0;
}, popushowAtLeast); visibility: hidden;
transition: opacity 150ms ease-in-out, visibility 150ms ease-in-out;
pointer-events: auto;
`;
popover.setAttribute('role', 'tooltip');
popover.innerHTML = `
<div class="font-semibold mb-2 popover-title" style="font-weight: 600; margin-bottom: 0.5rem;"></div>
<div class="text-sm popover-content" style="font-size: 0.875rem;"></div>
`;
// Add styles for content images
const style = document.createElement('style');
style.textContent = `
#custom-popover img {
max-width: 100%;
height: auto;
display: block;
margin: 0.5rem 0;
} }
}, popupWaitBeforeShow); #custom-popover a {
color: #2563eb;
text-decoration: none;
}
#custom-popover a:hover {
text-decoration: underline;
}
`;
if (!document.getElementById('popover-styles')) {
style.id = 'popover-styles';
document.head.appendChild(style);
}
// Keep popover open when hovering over it
popover.addEventListener('mouseenter', () => {
if (popoverTimeout) {
clearTimeout(popoverTimeout);
popoverTimeout = null;
}
});
popover.addEventListener('mouseleave', () => {
hidePopover();
});
document.body.appendChild(popover);
return popover;
}
function getPopover() {
return document.getElementById('custom-popover') || createPopover();
}
function showPopover(element, title, content) {
const popover = getPopover();
popover.querySelector('.popover-title').textContent = title;
popover.querySelector('.popover-content').innerHTML = content;
// Make visible to measure
popover.style.visibility = 'hidden';
popover.style.opacity = '0';
popover.style.display = 'block';
// Smart positioning - avoid viewport overflow
const padding = 10;
const popoverRect = popover.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
let left = clientX + 15;
let top = clientY - (popoverRect.height / 2);
// Prevent right overflow
if (left + popoverRect.width > viewportWidth - padding) {
left = clientX - popoverRect.width - 15;
}
// Prevent bottom overflow
if (top + popoverRect.height > viewportHeight - padding) {
top = viewportHeight - popoverRect.height - padding;
}
// Prevent top overflow
if (top < padding) {
top = padding;
}
popover.style.top = `${top}px`;
popover.style.left = `${left}px`;
popover.style.visibility = 'visible';
popover.style.opacity = '1';
currentElement = element;
}
function hidePopover() {
const popover = getPopover();
popover.style.opacity = '0';
popover.style.visibility = 'hidden';
currentElement = null;
} }
function pop_add(objects, title, contentFunction) { function pop_add(objects, title, contentFunction) {
objects.attr("id", "pop") objects.each(function (d) {
.attr("data-container", "body") const element = this;
.attr("data-toggle", "popover")
.attr("data-placement", "right")
.attr("title", title);
objects.each(function (d, i) { element.addEventListener('mouseenter', () => {
options = {trigger: "manual", html: true, animation: false}; if (popoverTimeout) clearTimeout(popoverTimeout);
this_ = this;
var p = $(this).popover(options).on("mouseenter", function () { popoverTimeout = setTimeout(() => {
pop_show_e(this); if (element.matches(':hover')) {
const content = contentFunction(d);
showPopover(element, title, content);
}
}, popupWaitBeforeShow);
}); });
p.on("show.bs.popover", function (e) {
// this is to dynamically ajdust the content and bounds of the popup element.addEventListener('mouseleave', () => {
p.attr('data-content', contentFunction(d)); if (popoverTimeout) {
p.data("bs.popover").setContent(); clearTimeout(popoverTimeout);
p.data("bs.popover").tip().css({"max-width": "1000px"}); popoverTimeout = null;
}
// Delay hide to allow moving to popover
setTimeout(() => {
const popover = getPopover();
if (!popover.matches(':hover') && !element.matches(':hover')) {
hidePopover();
}
}, 100);
}); });
}); });
} }
@ -255,7 +372,7 @@ function draw(pathway, elem) {
} }
} }
popupContent += "<img src='" + n.image + "' width='" + 20 * nodeRadius + "'><br>" popupContent += "<img src='" + n.image + "'><br>"
if (n.scenarios.length > 0) { if (n.scenarios.length > 0) {
popupContent += '<b>Half-lives and related scenarios:</b><br>' popupContent += '<b>Half-lives and related scenarios:</b><br>'
for (var s of n.scenarios) { for (var s of n.scenarios) {
@ -265,7 +382,7 @@ function draw(pathway, elem) {
var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0; var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0;
if (pathway.isIncremental && isLeaf) { if (pathway.isIncremental && isLeaf) {
popupContent += '<br><a class="btn btn-primary" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>'; popupContent += '<br><a class="btn btn-primary btn-sm mt-2" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>';
} }
return popupContent; return popupContent;
@ -285,7 +402,7 @@ function draw(pathway, elem) {
popupContent += adcontent; popupContent += adcontent;
} }
popupContent += "<img src='" + e.image + "' width='" + 20 * nodeRadius + "'><br>" popupContent += "<img src='" + e.image + "'><br>"
if (e.reaction_probability) { if (e.reaction_probability) {
popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>'; popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>';
} }
@ -308,6 +425,23 @@ function draw(pathway, elem) {
}); });
const zoomable = d3.select("#zoomable"); const zoomable = d3.select("#zoomable");
const svg = d3.select("#pwsvg");
const container = d3.select("#vizdiv");
// Set explicit SVG dimensions for proper zoom behavior
svg.attr("width", width)
.attr("height", height);
// Add background rectangle FIRST to enable pan/zoom on empty space
// This must be inserted before zoomable group so it's behind everything
svg.insert("rect", "#zoomable")
.attr("x", 0)
.attr("y", 0)
.attr("width", width)
.attr("height", height)
.attr("fill", "transparent")
.attr("pointer-events", "all")
.style("cursor", "grab");
// Zoom Funktion aktivieren // Zoom Funktion aktivieren
const zoom = d3.zoom() const zoom = d3.zoom()
@ -316,7 +450,12 @@ function draw(pathway, elem) {
zoomable.attr("transform", event.transform); zoomable.attr("transform", event.transform);
}); });
d3.select("svg").call(zoom); // Apply zoom to the SVG element - this enables wheel zoom
svg.call(zoom);
// Also apply zoom to container to catch events that might not reach SVG
// This ensures drag-to-pan works even when clicking on empty space
container.call(zoom);
nodes = pathway['nodes']; nodes = pathway['nodes'];
links = pathway['links']; links = pathway['links'];

View File

@ -1,6 +1,9 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_compound_modal"> <a
role="button"
onclick="document.getElementById('new_compound_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Compound</a <span class="glyphicon glyphicon-plus"></span> New Compound</a
> >
</li> </li>

View File

@ -2,8 +2,7 @@
<li> <li>
<a <a
role="button" role="button"
data-toggle="modal" onclick="document.getElementById('new_compound_structure_modal').showModal(); return false;"
data-target="#new_compound_structure_modal"
> >
<span class="glyphicon glyphicon-plus"></span> New Compound Structure</a <span class="glyphicon glyphicon-plus"></span> New Compound Structure</a
> >

View File

@ -1,6 +1,9 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_edge_modal"> <a
role="button"
onclick="document.getElementById('new_edge_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Edge</a <span class="glyphicon glyphicon-plus"></span> New Edge</a
> >
</li> </li>

View File

@ -1,5 +1,8 @@
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_group_modal"> <a
role="button"
onclick="document.getElementById('new_group_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Group</a <span class="glyphicon glyphicon-plus"></span> New Group</a
> >
</li> </li>

View File

@ -1,6 +1,9 @@
{% if meta.can_edit and meta.enabled_features.MODEL_BUILDING %} {% if meta.can_edit and meta.enabled_features.MODEL_BUILDING %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_model_modal"> <a
role="button"
onclick="document.getElementById('new_model_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Model</a <span class="glyphicon glyphicon-plus"></span> New Model</a
> >
</li> </li>

View File

@ -1,6 +1,9 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_node_modal"> <a
role="button"
onclick="document.getElementById('new_node_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Node</a <span class="glyphicon glyphicon-plus"></span> New Node</a
> >
</li> </li>

View File

@ -1,18 +1,23 @@
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_package_modal"> <a
role="button"
onclick="document.getElementById('new_package_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Package</a <span class="glyphicon glyphicon-plus"></span> New Package</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#import_package_modal"> <a
role="button"
onclick="document.getElementById('import_package_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-import"></span> Import Package from JSON</a <span class="glyphicon glyphicon-import"></span> Import Package from JSON</a
> >
</li> </li>
<li> <li>
<a <a
role="button" role="button"
data-toggle="modal" onclick="document.getElementById('import_legacy_package_modal').showModal(); return false;"
data-target="#import_legacy_package_modal"
> >
<span class="glyphicon glyphicon-import"></span> Import Package from legacy <span class="glyphicon glyphicon-import"></span> Import Package from legacy
JSON</a JSON</a

View File

@ -1,6 +1,9 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_reaction_modal"> <a
role="button"
onclick="document.getElementById('new_reaction_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Reaction</a <span class="glyphicon glyphicon-plus"></span> New Reaction</a
> >
</li> </li>

View File

@ -1,6 +1,9 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_rule_modal"> <a
role="button"
onclick="document.getElementById('new_rule_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Rule</a <span class="glyphicon glyphicon-plus"></span> New Rule</a
> >
</li> </li>

View File

@ -1,6 +1,9 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_scenario_modal"> <a
role="button"
onclick="document.getElementById('new_scenario_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Scenario</a <span class="glyphicon glyphicon-plus"></span> New Scenario</a
> >
</li> </li>

View File

@ -1,6 +1,9 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_setting_modal"> <a
role="button"
onclick="document.getElementById('new_setting_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span>New Setting</a <span class="glyphicon glyphicon-plus"></span>New Setting</a
> >
</li> </li>

View File

@ -1,42 +1,59 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_compound_modal"> <a
role="button"
onclick="document.getElementById('edit_compound_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-edit"></i> Edit Compound</a <i class="glyphicon glyphicon-edit"></i> Edit Compound</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal"> <a
role="button"
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a <i class="glyphicon glyphicon-plus"></i> Set Aliases</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#add_structure_modal"> <a
role="button"
onclick="document.getElementById('add_structure_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Add Structure</a <i class="glyphicon glyphicon-plus"></i> Add Structure</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a
role="button"
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
> >
</li> </li>
<li> <li>
<a <a
role="button" role="button"
data-toggle="modal" onclick="document.getElementById('generic_set_external_reference_modal').showModal(); return false;"
data-target="#generic_set_external_reference_modal"
> >
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a <i class="glyphicon glyphicon-plus"></i> Set External Reference</a
> >
</li> </li>
{% endif %} {% endif %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal"> <a
role="button"
onclick="document.getElementById('generic_copy_object_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-duplicate"></i> Copy</a <i class="glyphicon glyphicon-duplicate"></i> Copy</a
> >
</li> </li>
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a <i class="glyphicon glyphicon-trash"></i> Delete Compound</a
> >
</li> </li>

View File

@ -2,33 +2,40 @@
<li> <li>
<a <a
role="button" role="button"
data-toggle="modal" onclick="document.getElementById('edit_compound_structure_modal').showModal(); return false;"
data-target="#edit_compound_structure_modal"
> >
<i class="glyphicon glyphicon-edit"></i> Edit Compound Structure</a <i class="glyphicon glyphicon-edit"></i> Edit Compound Structure</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal"> <a
role="button"
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a <i class="glyphicon glyphicon-plus"></i> Set Aliases</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a
role="button"
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
> >
</li> </li>
<li> <li>
<a <a
role="button" role="button"
data-toggle="modal" onclick="document.getElementById('generic_set_external_reference_modal').showModal(); return false;"
data-target="#generic_set_external_reference_modal"
> >
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a <i class="glyphicon glyphicon-plus"></i> Set External Reference</a
> >
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Compound Structure</a <i class="glyphicon glyphicon-trash"></i> Delete Compound Structure</a
> >
</li> </li>

View File

@ -1,16 +1,25 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal"> <a
role="button"
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a <i class="glyphicon glyphicon-plus"></i> Set Aliases</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a
role="button"
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
> >
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Edge</a <i class="glyphicon glyphicon-trash"></i> Delete Edge</a
> >
</li> </li>

View File

@ -1,11 +1,17 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_group_member_modal"> <a
role="button"
onclick="document.getElementById('edit_group_member_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Add/Remove Member</a <i class="glyphicon glyphicon-trash"></i> Add/Remove Member</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Group</a <i class="glyphicon glyphicon-trash"></i> Delete Group</a
> >
</li> </li>

View File

@ -1,21 +1,33 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_model_modal"> <a
role="button"
onclick="document.getElementById('edit_model_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-edit"></i> Edit Model</a <i class="glyphicon glyphicon-edit"></i> Edit Model</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#evaluate_model_modal"> <a
role="button"
onclick="document.getElementById('evaluate_model_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-ok"></i> Evaluate Model</a <i class="glyphicon glyphicon-ok"></i> Evaluate Model</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#retrain_model_modal"> <a
role="button"
onclick="document.getElementById('retrain_model_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-repeat"></i> Retrain Model</a <i class="glyphicon glyphicon-repeat"></i> Retrain Model</a
> >
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Model</a <i class="glyphicon glyphicon-trash"></i> Delete Model</a
> >
</li> </li>

View File

@ -1,21 +1,33 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_node_modal"> <a
role="button"
onclick="document.getElementById('edit_node_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-edit"></i> Edit Node</a <i class="glyphicon glyphicon-edit"></i> Edit Node</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal"> <a
role="button"
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a <i class="glyphicon glyphicon-plus"></i> Set Aliases</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a
role="button"
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
> >
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Node</a <i class="glyphicon glyphicon-trash"></i> Delete Node</a
> >
</li> </li>

View File

@ -1,35 +1,49 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_package_modal"> <a
role="button"
onclick="document.getElementById('edit_package_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-edit"></i> Edit Package</a <i class="glyphicon glyphicon-edit"></i> Edit Package</a
> >
</li> </li>
<li> <li>
<a <a
role="button" role="button"
data-toggle="modal" onclick="document.getElementById('edit_package_permissions_modal').showModal(); return false;"
data-target="#edit_package_permissions_modal"
> >
<i class="glyphicon glyphicon-user"></i> Edit Permissions</a <i class="glyphicon glyphicon-user"></i> Edit Permissions</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#publish_package_modal"> <a
role="button"
onclick="document.getElementById('publish_package_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-bullhorn"></i> Publish Package</a <i class="glyphicon glyphicon-bullhorn"></i> Publish Package</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#export_package_modal"> <a
role="button"
onclick="document.getElementById('export_package_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-bullhorn"></i> Export Package as JSON</a <i class="glyphicon glyphicon-bullhorn"></i> Export Package as JSON</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_license_modal"> <a
role="button"
onclick="document.getElementById('set_license_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-duplicate"></i> License</a <i class="glyphicon glyphicon-duplicate"></i> License</a
> >
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Package</a <i class="glyphicon glyphicon-trash"></i> Delete Package</a
> >
</li> </li>

View File

@ -1,26 +1,34 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a class="button" data-toggle="modal" data-target="#add_pathway_node_modal"> <a
class="button"
onclick="document.getElementById('add_pathway_node_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Add Compound</a <i class="glyphicon glyphicon-plus"></i> Add Compound</a
> >
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal"> <a
class="button"
onclick="document.getElementById('add_pathway_edge_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a <i class="glyphicon glyphicon-plus"></i> Add Reaction</a
> >
</li> </li>
<li role="separator" class="divider"></li> <li role="separator" class="divider"></li>
{% endif %} {% endif %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal"> <a
role="button"
onclick="document.getElementById('generic_copy_object_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-duplicate"></i> Copy</a <i class="glyphicon glyphicon-duplicate"></i> Copy</a
> >
</li> </li>
<li> <li>
<a <a
class="button" class="button"
data-toggle="modal" onclick="document.getElementById('download_pathway_csv_modal').showModal(); return false;"
data-target="#download_pathway_csv_modal"
> >
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as CSV</a <i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as CSV</a
> >
@ -28,8 +36,7 @@
<li> <li>
<a <a
class="button" class="button"
data-toggle="modal" onclick="document.getElementById('download_pathway_image_modal').showModal(); return false;"
data-target="#download_pathway_image_modal"
> >
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as Image</a <i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as Image</a
> >
@ -38,8 +45,7 @@
<li> <li>
<a <a
class="button" class="button"
data-toggle="modal" onclick="document.getElementById('identify_missing_rules_modal').showModal(); return false;"
data-target="#identify_missing_rules_modal"
> >
<i class="glyphicon glyphicon-question-sign"></i> Identify Missing <i class="glyphicon glyphicon-question-sign"></i> Identify Missing
Rules</a Rules</a
@ -47,30 +53,34 @@
</li> </li>
<li role="separator" class="divider"></li> <li role="separator" class="divider"></li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#edit_pathway_modal"> <a
class="button"
onclick="document.getElementById('edit_pathway_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-edit"></i> Edit Pathway</a <i class="glyphicon glyphicon-edit"></i> Edit Pathway</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a
role="button"
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal"> <a
role="button"
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a <i class="glyphicon glyphicon-plus"></i> Set Aliases</a
> >
</li> </li>
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal">#}
{# <i class="glyphicon glyphicon-plus"></i> Calculate Compound Properties</a>#}
{# </li>#}
<li role="separator" class="divider"></li> <li role="separator" class="divider"></li>
<li> <li>
<a <a
class="button" class="button"
data-toggle="modal" onclick="document.getElementById('delete_pathway_node_modal').showModal(); return false;"
data-target="#delete_pathway_node_modal"
> >
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a <i class="glyphicon glyphicon-trash"></i> Delete Compound</a
> >
@ -78,14 +88,16 @@
<li> <li>
<a <a
class="button" class="button"
data-toggle="modal" onclick="document.getElementById('delete_pathway_edge_modal').showModal(); return false;"
data-target="#delete_pathway_edge_modal"
> >
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a <i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
> >
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Pathway</a <i class="glyphicon glyphicon-trash"></i> Delete Pathway</a
> >
</li> </li>

View File

@ -1,37 +1,51 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_reaction_modal"> <a
role="button"
onclick="document.getElementById('edit_reaction_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-edit"></i> Edit Reaction</a <i class="glyphicon glyphicon-edit"></i> Edit Reaction</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal"> <a
role="button"
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a <i class="glyphicon glyphicon-plus"></i> Set Aliases</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a
role="button"
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
> >
</li> </li>
<li> <li>
<a <a
role="button" role="button"
data-toggle="modal" onclick="document.getElementById('generic_set_external_reference_modal').showModal(); return false;"
data-target="#generic_set_external_reference_modal"
> >
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a <i class="glyphicon glyphicon-plus"></i> Set External Reference</a
> >
</li> </li>
{% endif %} {% endif %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal"> <a
role="button"
onclick="document.getElementById('generic_copy_object_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-duplicate"></i> Copy</a <i class="glyphicon glyphicon-duplicate"></i> Copy</a
> >
</li> </li>
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a <i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
> >
</li> </li>

View File

@ -1,28 +1,43 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_rule_modal"> <a
role="button"
onclick="document.getElementById('edit_rule_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-edit"></i> Edit Rule</a <i class="glyphicon glyphicon-edit"></i> Edit Rule</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal"> <a
role="button"
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a <i class="glyphicon glyphicon-plus"></i> Set Aliases</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a
role="button"
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
> >
</li> </li>
{% endif %} {% endif %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal"> <a
role="button"
onclick="document.getElementById('generic_copy_object_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-duplicate"></i> Copy</a <i class="glyphicon glyphicon-duplicate"></i> Copy</a
> >
</li> </li>
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Rule</a <i class="glyphicon glyphicon-trash"></i> Delete Rule</a
> >
</li> </li>

View File

@ -2,8 +2,7 @@
<li> <li>
<a <a
class="button" class="button"
data-toggle="modal" onclick="document.getElementById('add_additional_information_modal').showModal(); return false;"
data-target="#add_additional_information_modal"
> >
<i class="glyphicon glyphicon-trash"></i> Add Additional Information</a <i class="glyphicon glyphicon-trash"></i> Add Additional Information</a
> >
@ -11,14 +10,16 @@
<li> <li>
<a <a
class="button" class="button"
data-toggle="modal" onclick="document.getElementById('update_scenario_additional_information_modal').showModal(); return false;"
data-target="#update_scenario_additional_information_modal"
> >
<i class="glyphicon glyphicon-trash"></i> Set Additional Information</a <i class="glyphicon glyphicon-trash"></i> Set Additional Information</a
> >
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Scenario</a <i class="glyphicon glyphicon-trash"></i> Delete Scenario</a
> >
</li> </li>

View File

@ -1,19 +1,24 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_user_modal"> <a
role="button"
onclick="document.getElementById('edit_user_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-edit"></i> Update</a <i class="glyphicon glyphicon-edit"></i> Update</a
> >
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_password_modal"> <a
role="button"
onclick="document.getElementById('edit_password_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-lock"></i> Update Password</a <i class="glyphicon glyphicon-lock"></i> Update Password</a
> >
</li> </li>
<li> <li>
<a <a
role="button" role="button"
data-toggle="modal" onclick="document.getElementById('new_prediction_setting_modal').showModal(); return false;"
data-target="#new_prediction_setting_modal"
> >
<i class="glyphicon glyphicon-plus"></i> New Prediction Setting</a <i class="glyphicon glyphicon-plus"></i> New Prediction Setting</a
> >
@ -23,7 +28,10 @@
{# <i class="glyphicon glyphicon-console"></i> Manage API Token</a>#} {# <i class="glyphicon glyphicon-console"></i> Manage API Token</a>#}
{# </li>#} {# </li>#}
<li> <li>
<a role="button" data-toggle="modal" data-target="#generic_delete_modal"> <a
class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Account</a <i class="glyphicon glyphicon-trash"></i> Delete Account</a
> >
</li> </li>

View File

@ -1,56 +1,52 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% load envipytags %} {% load envipytags %}
{% block content %} {% block content %}
<div class="panel-group" id="reviewListAccordion"> <div class="space-y-2 p-4">
<div class="panel panel-default"> <!-- Header Section -->
<div <div class="card bg-base-100">
class="panel-heading" <div class="card-body">
id="headingPanel" <h2 class="card-title text-2xl">User Prediction Jobs</h2>
style="font-size:2rem;height: 46px" <p class="mt-2">Job Logs Desc</p>
>
Jobs
</div>
<div class="panel-body">
<p>Job Logs Desc</p>
</div> </div>
</div>
<div <!-- Jobs -->
class="panel panel-default panel-heading list-group-item" <div class="collapse-arrow bg-base-200 collapse">
style="background-color:silver" <input type="checkbox" checked />
> <div class="collapse-title text-xl font-medium">Recent Jobs</div>
<h4 class="panel-title"> <div class="collapse-content" id="job-content">
<a <div class="overflow-x-auto">
id="job-accordion-link" <table class="table-zebra table">
data-toggle="collapse" <thead>
data-parent="#job-accordion" <tr>
href="#jobs" <th>ID</th>
> <th>Name</th>
Jobs <th>Status</th>
</a> <th>Queued</th>
</h4> <th>Done</th>
</div> <th>Result</th>
<div id="jobs" class="panel-collapse in collapse"> </tr>
<div class="panel-body list-group-item" id="job-content"> </thead>
<table class="table-bordered table-hover table">
<tr style="background-color: rgba(0, 0, 0, 0.08);">
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">Queued</th>
<th scope="col">Done</th>
<th scope="col">Result</th>
</tr>
<tbody> <tbody>
{% for job in jobs %} {% for job in jobs %}
<tr> <tr>
{% if meta.user.is_superuser %}
<td>
<a href="{{ job.user.url }}">{{ job.user.username }}</a>
</td>
{% endif %}
<td>{{ job.task_id }}</td> <td>{{ job.task_id }}</td>
<td>{{ job.job_name }}</td> <td>{{ job.job_name }}</td>
<td>{{ job.status }}</td> <td>{{ job.status }}</td>
<td>{{ job.created }}</td> <td>{{ job.created }}</td>
<td>{{ job.done_at }}</td> <td>{{ job.done_at }}</td>
{% if job.task_result and job.task_result|is_url == True %} {% if job.task_result and job.task_result|is_url == True %}
<td><a href="{{ job.task_result }}">Result</a></td> <td>
<a href="{{ job.task_result }}" class="link link-primary"
>Result</a
>
</td>
{% elif job.task_result %} {% elif job.task_result %}
<td>{{ job.task_result|slice:"40" }}...</td> <td>{{ job.task_result|slice:"40" }}...</td>
{% else %} {% else %}
@ -62,19 +58,31 @@
</table> </table>
</div> </div>
</div> </div>
<!-- Unreviewable objects such as User / Group / Setting -->
<ul class="list-group">
{% for obj in objects %}
{% if object_type == 'user' %}
<a class="list-group-item" href="{{ obj.url }}"
>{{ obj.username }}</a
>
{% else %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a>
{% endif %}
{% endfor %}
</ul>
</div> </div>
{% if objects %}
<!-- Unreviewable objects such as User / Group / Setting -->
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<ul class="menu bg-base-200 rounded-box">
{% for obj in objects %}
{% if object_type == 'user' %}
<li>
<a href="{{ obj.url }}" class="hover:bg-base-300"
>{{ obj.username }}</a
>
</li>
{% else %}
<li>
<a href="{{ obj.url }}" class="hover:bg-base-300"
>{{ obj.name }}</a
>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -1,28 +1,32 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
{% if object_type != 'package' %} {# Serialize objects data for Alpine pagination #}
<div> {# prettier-ignore-start #}
<div id="load-all-error" style="display: none;"> {# FIXME: This is a hack to get the objects data into the JavaScript code. #}
<div class="alert alert-danger" role="alert"> <script>
<span window.reviewedObjects = [
class="glyphicon glyphicon-exclamation-sign" {% for obj in reviewed_objects %}
aria-hidden="true" { "name": "{{ obj.name|escapejs }}", "url": "{{ obj.url }}" }{% if not forloop.last %},{% endif %}
></span> {% endfor %}
<span class="sr-only">Error:</span> ];
Getting objects failed! window.unreviewedObjects = [
</div> {% for obj in unreviewed_objects %}
</div> { "name": "{{ obj.name|escapejs }}", "url": "{{ obj.url }}" }{% if not forloop.last %},{% endif %}
{% endfor %}
];
</script>
{# prettier-ignore-end #}
{% if object_type != 'package' %}
<div class="px-8 py-4">
<input <input
type="text" type="text"
id="object-search" id="object-search"
class="form-control" class="input input-bordered hidden w-full max-w-xs"
placeholder="Search by name" placeholder="Search by name"
style="display: none;"
/> />
<p></p>
</div> </div>
{% endif %} {% endif %}
@ -56,423 +60,474 @@
{% endif %} {% endif %}
{% endblock action_modals %} {% endblock action_modals %}
<div class="panel-group" id="reviewListAccordion"> <div class="px-8 py-4">
<div class="panel panel-default"> <!-- Header Section -->
<div <div class="card bg-base-100">
class="panel-heading" <div class="card-body px-0 py-4">
id="headingPanel" <div class="flex items-center justify-between">
style="font-size:2rem;height: 46px" <h2 class="card-title text-2xl">
> {% if object_type == 'package' %}
{% if object_type == 'package' %} Packages
Packages {% elif object_type == 'compound' %}
{% elif object_type == 'compound' %} Compounds
Compounds {% elif object_type == 'structure' %}
{% elif object_type == 'structure' %} Compound structures
Compound structures {% elif object_type == 'rule' %}
{% elif object_type == 'rule' %} Rules
Rules {% elif object_type == 'reaction' %}
{% elif object_type == 'reaction' %} Reactions
Reactions {% elif object_type == 'pathway' %}
{% elif object_type == 'pathway' %} Pathways
Pathways {% elif object_type == 'node' %}
{% elif object_type == 'node' %} Nodes
Nodes {% elif object_type == 'edge' %}
{% elif object_type == 'edge' %} Edges
Edges {% elif object_type == 'scenario' %}
{% elif object_type == 'scenario' %} Scenarios
Scenarios {% elif object_type == 'model' %}
{% elif object_type == 'model' %} Model
Model {% elif object_type == 'setting' %}
{% elif object_type == 'setting' %} Settings
Settings {% elif object_type == 'user' %}
{% elif object_type == 'user' %} Users
Users {% elif object_type == 'group' %}
{% elif object_type == 'group' %} Groups
Groups {% endif %}
{% endif %} </h2>
<div <div id="actionsButton" class="dropdown dropdown-end hidden">
id="actionsButton" <div tabindex="0" role="button" class="btn btn-ghost btn-sm">
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" <svg
class="dropdown" xmlns="http://www.w3.org/2000/svg"
> width="16"
<a height="16"
href="#" viewBox="0 0 24 24"
class="dropdown-toggle" fill="none"
data-toggle="dropdown" stroke="currentColor"
role="button" stroke-width="2"
aria-haspopup="true" stroke-linecap="round"
aria-expanded="false" stroke-linejoin="round"
><span class="glyphicon glyphicon-wrench"></span> Actions class="lucide lucide-wrench"
<span class="caret"></span><span style="padding-right:1em"></span >
></a> <path
<ul id="actionsList" class="dropdown-menu"> d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
{% block actions %} />
{% if object_type == 'package' %} </svg>
{% include "actions/collections/package.html" %} Actions
{% elif object_type == 'compound' %} </div>
{% include "actions/collections/compound.html" %} <ul
{% elif object_type == 'structure' %} tabindex="-1"
{% include "actions/collections/compound_structure.html" %} class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
{% elif object_type == 'rule' %} >
{% include "actions/collections/rule.html" %} {% block actions %}
{% elif object_type == 'reaction' %} {% if object_type == 'package' %}
{% include "actions/collections/reaction.html" %} {% include "actions/collections/package.html" %}
{% elif object_type == 'setting' %} {% elif object_type == 'compound' %}
{% include "actions/collections/setting.html" %} {% include "actions/collections/compound.html" %}
{% elif object_type == 'scenario' %} {% elif object_type == 'structure' %}
{% include "actions/collections/scenario.html" %} {% include "actions/collections/compound_structure.html" %}
{% elif object_type == 'model' %} {% elif object_type == 'rule' %}
{% include "actions/collections/model.html" %} {% include "actions/collections/rule.html" %}
{% elif object_type == 'pathway' %} {% elif object_type == 'reaction' %}
{% include "actions/collections/pathway.html" %} {% include "actions/collections/reaction.html" %}
{% elif object_type == 'node' %} {% elif object_type == 'setting' %}
{% include "actions/collections/node.html" %} {% include "actions/collections/setting.html" %}
{% elif object_type == 'edge' %} {% elif object_type == 'scenario' %}
{% include "actions/collections/edge.html" %} {% include "actions/collections/scenario.html" %}
{% elif object_type == 'group' %} {% elif object_type == 'model' %}
{% include "actions/collections/group.html" %} {% include "actions/collections/model.html" %}
{% endif %} {% elif object_type == 'pathway' %}
{% endblock %} {% include "actions/collections/pathway.html" %}
</ul> {% elif object_type == 'node' %}
{% include "actions/collections/node.html" %}
{% elif object_type == 'edge' %}
{% include "actions/collections/edge.html" %}
{% elif object_type == 'group' %}
{% include "actions/collections/group.html" %}
{% endif %}
{% endblock %}
</ul>
</div>
</div> </div>
</div> <div class="mt-2">
<div class="panel-body"> <!-- Set Text above links -->
<!-- Set Text above links --> {% if object_type == 'package' %}
{% if object_type == 'package' %}
<p>
A package contains pathways, rules, etc. and can reflect specific
experimental conditions.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/packages"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'compound' %}
<p>
A compound stores the structure of a molecule and can include
meta-information.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/compounds"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'structure' %}
<p>
The structures stored in this compound
<a
target="_blank"
href="https://wiki.envipath.org/index.php/compounds"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'rule' %}
<p>
A rule describes a biotransformation reaction template that is
defined as SMIRKS.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/Rules"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'reaction' %}
<p>
A reaction is a specific biotransformation from educt compounds to
product compounds.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/reactions"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'pathway' %}
<p>
A pathway displays the (predicted) biodegradation of a compound as
graph.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/pathways"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'node' %}
<p>
Nodes represent the (predicted) compounds in a graph.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/nodes"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'edge' %}
<p>
Edges represent the links between Nodes in a graph
<a
target="_blank"
href="https://wiki.envipath.org/index.php/edges"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'scenario' %}
<p>
A scenario contains meta-information that can be attached to other
data (compounds, rules, ..).
<a
target="_blank"
href="https://wiki.envipath.org/index.php/scenarios"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'model' %}
<p>
A model applies machine learning to limit the combinatorial
explosion.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/relative_reasoning"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'setting' %}
<p>
A setting includes configuration parameters for pathway predictions.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/settings"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'user' %}
<p>
Register now to create own packages and to submit and manage your
data.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/users"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'group' %}
<p>
Users can team up in groups to share packages.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/groups"
role="button"
>Learn more &gt;&gt;</a
>
</p>
{% endif %}
<!-- If theres nothing to show extend the text above -->
{% if reviewed_objects and unreviewed_objects %}
{% if reviewed_objects|length == 0 and unreviewed_objects|length == 0 %}
<p> <p>
Nothing found. There are two possible reasons: <br /><br />1. A package contains pathways, rules, etc. and can reflect specific
There is no content yet.<br />2. You have no reading experimental conditions.
permissions.<br /><br />Please be sure you have at least reading <a
permissions. target="_blank"
href="https://wiki.envipath.org/index.php/packages"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'compound' %}
<p>
A compound stores the structure of a molecule and can include
meta-information.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/compounds"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'structure' %}
<p>
The structures stored in this compound
<a
target="_blank"
href="https://wiki.envipath.org/index.php/compounds"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'rule' %}
<p>
A rule describes a biotransformation reaction template that is
defined as SMIRKS.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/Rules"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'reaction' %}
<p>
A reaction is a specific biotransformation from educt compounds to
product compounds.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/reactions"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'pathway' %}
<p>
A pathway displays the (predicted) biodegradation of a compound as
graph.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/pathways"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'node' %}
<p>
Nodes represent the (predicted) compounds in a graph.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/nodes"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'edge' %}
<p>
Edges represent the links between Nodes in a graph
<a
target="_blank"
href="https://wiki.envipath.org/index.php/edges"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'scenario' %}
<p>
A scenario contains meta-information that can be attached to other
data (compounds, rules, ..).
<a
target="_blank"
href="https://wiki.envipath.org/index.php/scenarios"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'model' %}
<p>
A model applies machine learning to limit the combinatorial
explosion.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/relative_reasoning"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'setting' %}
<p>
A setting includes configuration parameters for pathway
predictions.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/settings"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'user' %}
<p>
Register now to create own packages and to submit and manage your
data.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/users"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'group' %}
<p>
Users can team up in groups to share packages.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/groups"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p> </p>
{% endif %} {% endif %}
{% endif %}
<!-- If theres nothing to show extend the text above -->
{% if reviewed_objects and unreviewed_objects %}
{% if reviewed_objects|length == 0 and unreviewed_objects|length == 0 %}
<p class="mt-4">
Nothing found. There are two possible reasons: <br /><br />1.
There is no content yet.<br />2. You have no reading
permissions.<br /><br />Please be sure you have at least reading
permissions.
</p>
{% endif %}
{% endif %}
</div>
</div> </div>
</div>
<!-- Lists Container - Full Width with Reviewed on Right -->
<div class="w-full">
{% if reviewed_objects %} {% if reviewed_objects %}
{% if reviewed_objects|length > 0 %} {% if reviewed_objects|length > 0 %}
<!-- Reviewed -->
<div <div
class="panel panel-default panel-heading list-group-item" class="collapse-arrow bg-base-200 collapse order-2 w-full"
style="background-color:silver" x-data="paginatedList(window.reviewedObjects || [], { isReviewed: true, instanceId: 'reviewed' })"
> >
<h4 class="panel-title"> <input type="checkbox" checked />
<a <div class="collapse-title text-xl font-medium">
id="ReviewedLink" Reviewed
data-toggle="collapse" <span
data-parent="#reviewListAccordion" class="badge badge-sm badge-neutral ml-2"
href="#Reviewed" x-text="totalItems"
>Reviewed</a ></span>
</div>
<div class="collapse-content w-full">
<ul class="menu bg-base-100 rounded-box w-full">
<template x-for="obj in paginatedItems" :key="obj.url">
<li>
<a :href="obj.url" class="hover:bg-base-200">
<span x-text="obj.name"></span>
<span
class="tooltip tooltip-left ml-auto"
data-tip="Reviewed"
>
<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-star"
>
<polygon
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
/>
</svg>
</span>
</a>
</li>
</template>
</ul>
<!-- Pagination Controls -->
<div
x-show="totalPages > 1"
class="mt-4 flex items-center justify-between px-2"
> >
</h4> <span class="text-base-content/70 text-sm">
</div> Showing <span x-text="showingStart"></span>-<span
<div id="Reviewed" class="panel-collapse in collapse"> x-text="showingEnd"
<div class="panel-body list-group-item" id="ReviewedContent"> ></span>
{% if object_type == 'package' %} of <span x-text="totalItems"></span>
{% for obj in reviewed_objects %} </span>
<a class="list-group-item" href="{{ obj.url }}" <div class="join">
>{{ obj.name|safe }} <button
<span class="join-item btn btn-sm"
class="glyphicon glyphicon-star" :disabled="currentPage === 1"
aria-hidden="true" @click="prevPage()"
style="float:right" >
data-toggle="tooltip" «
data-placement="top" </button>
title="" <template x-for="item in pageNumbers" :key="item.key">
data-original-title="Reviewed" <button
> class="join-item btn btn-sm"
</span> :class="{ 'btn-active': item.page === currentPage }"
</a> :disabled="item.isEllipsis"
{% endfor %} @click="!item.isEllipsis && goToPage(item.page)"
{% else %} x-text="item.page"
{% for obj in reviewed_objects|slice:":50" %} ></button>
<a class="list-group-item" href="{{ obj.url }}" </template>
>{{ obj.name|safe }}{# <i>({{ obj.package.name }})</i> #} <button
<span class="join-item btn btn-sm"
class="glyphicon glyphicon-star" :disabled="currentPage === totalPages"
aria-hidden="true" @click="nextPage()"
style="float:right" >
data-toggle="tooltip" »
data-placement="top" </button>
title="" </div>
data-original-title="Reviewed" </div>
>
</span>
</a>
{% endfor %}
{% endif %}
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% endif %} {% endif %}
{% if unreviewed_objects %} {% if unreviewed_objects %}
<!-- Unreviewed -->
<div <div
class="panel panel-default panel-heading list-group-item" class="collapse-arrow bg-base-200 collapse order-1 w-full"
style="background-color:silver" x-data="paginatedList(window.unreviewedObjects || [], { isReviewed: false, instanceId: 'unreviewed' })"
> >
<h4 class="panel-title"> <input
<a type="checkbox"
id="UnreviewedLink" {% if reviewed_objects|length == 0 or object_type == 'package' %}checked{% endif %}
data-toggle="collapse" />
data-parent="#unReviewListAccordion" <div class="collapse-title text-xl font-medium">
href="#Unreviewed" Unreviewed
>Unreviewed</a <span
class="badge badge-sm badge-neutral ml-2"
x-text="totalItems"
></span>
</div>
<div class="collapse-content w-full">
<ul class="menu bg-base-100 rounded-box w-full">
<template x-for="obj in paginatedItems" :key="obj.url">
<li>
<a
:href="obj.url"
class="hover:bg-base-200"
x-text="obj.name"
></a>
</li>
</template>
</ul>
<!-- Pagination Controls -->
<div
x-show="totalPages > 1"
class="mt-4 flex items-center justify-between px-2"
> >
</h4> <span class="text-base-content/70 text-sm">
</div> Showing <span x-text="showingStart"></span>-<span
<div x-text="showingEnd"
id="Unreviewed" ></span>
class="panel-collapse {% if reviewed_objects|length == 0 or object_type == 'package' %}in{% endif %} collapse" of <span x-text="totalItems"></span>
> </span>
<div class="panel-body list-group-item" id="UnreviewedContent"> <div class="join">
{% if object_type == 'package' %} <button
{% for obj in unreviewed_objects %} class="join-item btn btn-sm"
<a class="list-group-item" href="{{ obj.url }}" :disabled="currentPage === 1"
>{{ obj.name|safe }}</a @click="prevPage()"
> >
{% endfor %} «
{% else %} </button>
{% for obj in unreviewed_objects|slice:":50" %} <template x-for="item in pageNumbers" :key="item.key">
<a class="list-group-item" href="{{ obj.url }}" <button
>{{ obj.name|safe }}</a class="join-item btn btn-sm"
:class="{ 'btn-active': item.page === currentPage }"
:disabled="item.isEllipsis"
@click="!item.isEllipsis && goToPage(item.page)"
x-text="item.page"
></button>
</template>
<button
class="join-item btn btn-sm"
:disabled="currentPage === totalPages"
@click="nextPage()"
> >
{% endfor %} »
{% endif %} </button>
</div>
</div>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if objects %}
<!-- Unreviewable objects such as User / Group / Setting -->
<ul class="list-group">
{% for obj in objects %}
{% if object_type == 'user' %}
<a class="list-group-item" href="{{ obj.url }}"
>{{ obj.username|safe }}</a
>
{% else %}
<a class="list-group-item" href="{{ obj.url }}"
>{{ obj.name|safe }}</a
>
{% endif %}
{% endfor %}
</ul>
{% endif %}
</div> </div>
<style>
.spinner-widget {
position: fixed; /* stays in place on scroll */
bottom: 20px; /* distance from bottom */
right: 20px; /* distance from right */
z-index: 9999; /* above most elements */
width: 60px; /* adjust to gif size */
height: 60px;
}
.spinner-widget img { {% if objects %}
width: 100%; <!-- Unreviewable objects such as User / Group / Setting -->
height: auto; <div class="card bg-base-100">
} <div class="card-body">
</style> <ul class="menu bg-base-200 rounded-box">
{% for obj in objects %}
<div id="load-all-loading" class="spinner-widget" style="display: none"> {% if object_type == 'user' %}
<img <li>
id="loading-gif" <a href="{{ obj.url }}" class="hover:bg-base-300"
src="{% static '/images/wait.gif' %}" >{{ obj.username }}</a
alt="Loading..." >
/> </li>
</div> {% else %}
<li>
<a href="{{ obj.url }}" class="hover:bg-base-300"
>{{ obj.name }}</a
>
</li>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</div> </div>
{# prettier-ignore-start #}
<script>
$(function () {
$('#object-search').show(); <script>
document.addEventListener("DOMContentLoaded", function () {
// Show actions button if there are actions
const actionsButton = document.getElementById("actionsButton");
const actionsList = actionsButton?.querySelector("ul");
if (actionsList && actionsList.children.length > 0) {
actionsButton?.classList.remove("hidden");
}
{% if object_type != 'package' and object_type != 'user' and object_type != 'group' %} // Show search input and connect to Alpine pagination
{% if reviewed_objects|length > 50 or unreviewed_objects|length > 50 %} const objectSearch = document.getElementById("object-search");
$('#load-all-loading').show() if (objectSearch) {
objectSearch.classList.remove("hidden");
setTimeout(function () { objectSearch.addEventListener("input", function () {
$('#load-all-error').hide(); const query = this.value;
// Dispatch search to all paginatedList components
$.getJSON('?all=true', function (resp) { document
$('#ReviewedContent').empty(); .querySelectorAll('[x-data*="paginatedList"]')
$('#UnreviewedContent').empty(); .forEach((el) => {
if (el._x_dataStack && el._x_dataStack[0]) {
for (o in resp.objects) { el._x_dataStack[0].search(query);
obj = resp.objects[o]; }
if (obj.reviewed) {
$('#ReviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + ' <span class="glyphicon glyphicon-star" aria-hidden="true" style="float:right" data-toggle="tooltip" data-placement="top" title="" data-original-title="Reviewed"></span></a>');
} else {
$('#UnreviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + '</a>');
}
}
$('#load-all-loading').hide();
$('#load-remaining').hide();
}).fail(function (resp) {
$('#load-all-loading').hide();
$('#load-all-error').show();
});
}, 2500);
{% endif %}
{% endif %}
$('#modal-form-delete-submit').on('click', function (e) {
e.preventDefault();
$('#modal-form-delete').submit();
}); });
$('#object-search').on('keyup', function () {
let query = $(this).val().toLowerCase();
$('a.list-group-item').each(function () {
let text = $(this).text().toLowerCase();
$(this).toggle(text.indexOf(query) !== -1);
});
});
}); });
</script> }
{# prettier-ignore-end #}
// Delete form submit handler
const deleteSubmit = document.getElementById("modal-form-delete-submit");
const deleteForm = document.getElementById("modal-form-delete");
if (deleteSubmit && deleteForm) {
deleteSubmit.addEventListener("click", function (e) {
e.preventDefault();
deleteForm.submit();
});
}
});
</script>
{% endblock content %} {% endblock content %}

View File

@ -1,18 +1,77 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="alert alert-error" role="alert"> <div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<h4 class="alert-heading">Bad Request!</h4> <div class="w-full max-w-2xl">
<p>Lorem</p> <div class="alert alert-error mb-6 shadow-lg">
<hr /> <svg
<p class="mb-0"> xmlns="http://www.w3.org/2000/svg"
You can find out more about permissions in our class="h-8 w-8 shrink-0 stroke-current"
<a fill="none"
target="_blank" viewBox="0 0 24 24"
href="https://wiki.envipath.org/index.php/packages" >
role="button" <path
>Wiki &gt;&gt;</a stroke-linecap="round"
> stroke-linejoin="round"
</p> 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>
<div class="flex flex-col">
<h3 class="text-lg font-bold">Bad Request</h3>
<p class="text-sm">The request you sent was invalid or malformed.</p>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4 text-2xl">What happened?</h2>
<p class="text-base-content/70 mb-4">
The server couldn't process your request because it contains invalid
data or parameters.
</p>
<div class="card-actions mt-6 justify-end">
<a href="/" class="btn btn-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Go Home
</a>
<a
href="https://wiki.envipath.org/index.php/packages"
target="_blank"
class="btn btn-outline"
>
Learn More
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -1,18 +1,80 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="alert alert-error" role="alert"> <div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<h4 class="alert-heading">Access Denied!</h4> <div class="w-full max-w-2xl">
<p>Access to X denied.</p> <div class="alert alert-warning mb-6 shadow-lg">
<hr /> <svg
<p class="mb-0"> xmlns="http://www.w3.org/2000/svg"
You can find out more about permissions in our class="h-8 w-8 shrink-0 stroke-current"
<a fill="none"
target="_blank" viewBox="0 0 24 24"
href="https://wiki.envipath.org/index.php/packages" >
role="button" <path
>Wiki &gt;&gt;</a stroke-linecap="round"
> stroke-linejoin="round"
</p> 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>
<div class="flex flex-col">
<h3 class="text-lg font-bold">Access Denied</h3>
<p class="text-sm">
You don't have permission to access this resource.
</p>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4 text-2xl">Permission Required</h2>
<p class="text-base-content/70 mb-4">
You need the appropriate permissions to access this content. If you
believe this is an error, please contact the package owner or
administrator.
</p>
<div class="card-actions mt-6 justify-end">
<a href="/" class="btn btn-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Go Home
</a>
<a
href="https://wiki.envipath.org/index.php/packages"
target="_blank"
class="btn btn-outline"
>
Learn More
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -1,18 +1,77 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="alert alert-error" role="alert"> <div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<h4 class="alert-heading">Not Found!</h4> <div class="w-full max-w-2xl">
<p>Does not exist</p> <div class="alert alert-info mb-6 shadow-lg">
<hr /> <svg
<p class="mb-0"> xmlns="http://www.w3.org/2000/svg"
You can find out more about permissions in our class="h-8 w-8 shrink-0 stroke-current"
<a fill="none"
target="_blank" viewBox="0 0 24 24"
href="https://wiki.envipath.org/index.php/packages" >
role="button" <path
>Wiki &gt;&gt;</a stroke-linecap="round"
> stroke-linejoin="round"
</p> stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div class="flex flex-col">
<h3 class="text-lg font-bold">Page Not Found</h3>
<p class="text-sm">The page you're looking for doesn't exist.</p>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4 text-2xl">404 Error</h2>
<p class="text-base-content/70 mb-4">
The page or resource you requested could not be found. It may have
been moved, deleted, or the URL might be incorrect.
</p>
<div class="card-actions mt-6 justify-end">
<a href="/" class="btn btn-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Go Home
</a>
<a
href="https://wiki.envipath.org/index.php/packages"
target="_blank"
class="btn btn-outline"
>
Learn More
<svg
xmlns="http://www.w3.org/2000/svg"
class="ml-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
/>
</svg>
</a>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -1,9 +1,76 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="alert alert-danger" role="alert"> <div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<h4 class="alert-heading">{{ error_message }}</h4> <div class="w-full max-w-2xl">
<hr /> <div class="alert alert-error mb-6 shadow-lg">
<p class="mb-0">{{ error_detail }}</p> <svg
xmlns="http://www.w3.org/2000/svg"
class="h-8 w-8 shrink-0 stroke-current"
fill="none"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z"
/>
</svg>
<div class="flex flex-col">
<h3 class="text-lg font-bold">
{{ error_message|default:"An Error Occurred" }}
</h3>
<p class="text-sm">
{{ error_detail|default:"Something went wrong. Please try again later." }}
</p>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4 text-2xl">Oops! Something went wrong</h2>
<p class="text-base-content/70 mb-4">
{{ error_description|default:"We encountered an unexpected error while processing your request. Our team has been notified and is working to resolve the issue." }}
</p>
<div class="card-actions mt-6 justify-end">
<a href="/" class="btn btn-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Go Home
</a>
<button onclick="window.history.back()" class="btn btn-outline">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
Go Back
</button>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -1,11 +1,81 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="alert alert-danger" role="alert"> <div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<h4 class="alert-heading">Your account has not been activated yet!</h4> <div class="w-full max-w-2xl">
<p> <div class="alert alert-warning mb-6 shadow-lg">
Your account has not been activated yet. If you have questions <svg
<a href="mailto:admin@envipath.org">contact us.</a> xmlns="http://www.w3.org/2000/svg"
</p> class="h-8 w-8 shrink-0 stroke-current"
fill="none"
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>
<div class="flex flex-col">
<h3 class="text-lg font-bold">Account Not Activated</h3>
<p class="text-sm">Your account is pending activation.</p>
</div>
</div>
<div class="card bg-base-100 shadow-xl">
<div class="card-body">
<h2 class="card-title mb-4 text-2xl">Account Activation Required</h2>
<p class="text-base-content/70 mb-4">
Your account has not been activated yet. An administrator needs to
approve your account before you can access all features. This
process typically takes 24-48 hours.
</p>
<div class="divider"></div>
<p class="text-base-content/70 mb-4">
If you have questions or believe this is an error, please
<a href="mailto:admin@envipath.org" class="link link-primary"
>contact us</a
>.
</p>
<div class="card-actions mt-6 justify-end">
<a href="/" class="btn btn-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
/>
</svg>
Go Home
</a>
<a href="mailto:admin@envipath.org" class="btn btn-outline">
<svg
xmlns="http://www.w3.org/2000/svg"
class="mr-2 h-5 w-5"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
Contact Admin
</a>
</div>
</div>
</div>
</div>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -151,11 +151,6 @@
>Package</a >Package</a
> >
</li> </li>
<li>
<a href="{{ meta.server_url }}/search" id="searchLink"
>Search</a
>
</li>
<li> <li>
<a href="{{ meta.server_url }}/model" id="modelLink" <a href="{{ meta.server_url }}/model" id="modelLink"
>Modelling</a >Modelling</a
@ -233,15 +228,7 @@
>Documentation Wiki</a >Documentation Wiki</a
> >
</li> </li>
<li>
<a
href="#"
id="citeButton"
data-toggle="modal"
data-target="#citemodal"
>How to cite enviPath</a
>
</li>
<li class="divider"></li> <li class="divider"></li>
<li><a>Version: {{ meta.version }}</a></li> <li><a>Version: {{ meta.version }}</a></li>
</ul> </ul>
@ -413,10 +400,5 @@
} }
}); });
</script> </script>
{% block modals %}
{% include "modals/cite_modal.html" %}
{% include "modals/predict_modal.html" %}
{% include "modals/batch_predict_modal.html" %}
{% endblock %}
</body> </body>
</html> </html>

View File

@ -21,8 +21,14 @@
type="text/css" type="text/css"
/> />
{# jQuery - Keep for compatibility with existing JS #} {# Alpine.js - For reactive components #}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> <script
defer
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"
></script>
<script src="{% static 'js/alpine/index.js' %}"></script>
<script src="{% static 'js/alpine/search.js' %}"></script>
<script src="{% static 'js/alpine/pagination.js' %}"></script>
{# Font Awesome #} {# Font Awesome #}
<link <link
@ -35,21 +41,10 @@
<script> <script>
const csrftoken = document.querySelector("[name=csrf-token]").content; const csrftoken = document.querySelector("[name=csrf-token]").content;
// Setup CSRF header for all jQuery AJAX requests
$.ajaxSetup({
beforeSend: function (xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
},
});
</script> </script>
{# 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 #}
<script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script>
{% if not debug %} {% if not debug %}
<!-- Matomo --> <!-- Matomo -->
@ -171,10 +166,11 @@
{% endblock %} {% endblock %}
<script> <script>
$(function () { document.addEventListener("DOMContentLoaded", function () {
// Hide actionsbutton if there's no action defined // Show actions button if there are actions defined
if ($("#actionsButton ul").children().length > 0) { const actionsButtonUl = document.querySelector("#actionsButton ul");
$("#actionsButton").show(); if (actionsButtonUl && actionsButtonUl.children.length > 0) {
document.getElementById("actionsButton").style.display = "";
} }
}); });

View File

@ -1,140 +1,267 @@
{% load static %} {% load static %}
{# Modern DaisyUI Navbar #} {# Modern DaisyUI Navbar with Mobile Drawer Menu #}
<div class="navbar x-50 bg-neutral-50 text-neutral-950 shadow-lg"> <div class="drawer drawer-mobile">
<div class="navbar-start"> <input id="drawer-toggle" type="checkbox" class="drawer-toggle" />
<a href="{{ meta.server_url }}" class="btn btn-ghost text-xl normal-case"> <div class="drawer-content flex flex-col">
<svg class="fill-base-content h-8" viewBox="0 0 104 26" role="img"> {# Navbar #}
<use href="{% static "/images/logo-name.svg" %}#ep-logo-name" /> <div class="navbar x-50 bg-neutral-50 text-neutral-950 shadow-lg">
</svg> <div class="navbar-start">
</a> {# Hamburger menu button - visible on mobile, hidden on desktop #}
</div> {% if not public_mode %}
<label
{% if not public_mode %} for="drawer-toggle"
<div class="navbar-center hidden lg:flex"> class="btn btn-square btn-ghost drawer-button lg:hidden"
<a >
href="{{ meta.server_url }}/predict" <svg
role="button" xmlns="http://www.w3.org/2000/svg"
class="btn btn-ghost" fill="none"
id="predictLink" viewBox="0 0 24 24"
>Predict</a class="inline-block h-5 w-5 stroke-current"
> >
<!-- <li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li> --> <path
<!--<li><a href="{{ meta.server_url }}/browse" id="browseLink">Browse</a></li>--> stroke-linecap="round"
<div class="dropdown dropdown-center"> stroke-linejoin="round"
<div tabindex="0" role="button" class="btn btn-ghost">Browse</div> stroke-width="2"
<ul d="M4 6h16M4 12h16M4 18h16"
tabindex="-1" ></path>
class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm" </svg>
</label>
{% endif %}
<a
href="{{ meta.server_url }}"
class="btn btn-ghost text-xl normal-case"
> >
<li> <svg class="fill-base-content h-8" viewBox="0 0 104 26" role="img">
<a href="{{ meta.server_url }}/package" id="packageLink">Package</a> <use href="{% static "/images/logo-name.svg" %}#ep-logo-name" />
</li> </svg>
<li> </a>
<a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a> </div>
</li>
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li> {% if not public_mode %}
<li> {# Desktop menu - hidden on mobile, visible on desktop #}
<a href="{{ meta.server_url }}/compound" id="compoundLink" <div class="navbar-center hidden lg:flex">
>Compound</a <a
href="{{ meta.server_url }}/predict"
role="button"
class="btn btn-ghost"
id="predictLink"
>Predict</a
>
<div class="dropdown dropdown-center">
<div tabindex="0" role="button" class="btn btn-ghost">Browse</div>
<ul
tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm"
> >
</li> <li>
<li> <a href="{{ meta.server_url }}/package" id="packageLink"
<a href="{{ meta.server_url }}/reaction" id="reactionLink" >Package</a
>Reaction</a >
</li>
<li>
<a href="{{ meta.server_url }}/pathway" id="pathwayLink"
>Pathway</a
>
</li>
<li>
<a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a>
</li>
<li>
<a href="{{ meta.server_url }}/compound" id="compoundLink"
>Compound</a
>
</li>
<li>
<a href="{{ meta.server_url }}/reaction" id="reactionLink"
>Reaction</a
>
</li>
<li>
<a
href="{{ meta.server_url }}/model"
id="relative-reasoningLink"
>Model</a
>
</li>
<li>
<a href="{{ meta.server_url }}/scenario" id="scenarioLink"
>Scenario</a
>
</li>
</ul>
</div>
</div>
{% endif %}
<div class="navbar-end">
{% if not public_mode %}
<a id="search-trigger" role="button" class="cursor-pointer">
<div
class="badge badge-dash bg-base-200 text-base-content/50 m-1 flex items-center space-x-1 p-2"
> >
</li> <svg
<li> xmlns="http://www.w3.org/2000/svg"
<a href="{{ meta.server_url }}/model" id="relative-reasoningLink" width="16"
>Model</a height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-search-icon lucide-search"
>
<path d="m21 21-4.34-4.34" />
<circle cx="11" cy="11" r="8" />
</svg>
<span id="search-shortcut">⌘K</span>
</div>
</a>
{% endif %}
{% if meta.user.username == 'anonymous' or public_mode %}
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a>
{% else %}
<div class="dropdown dropdown-end">
<div
tabindex="0"
role="button"
class="btn btn-ghost btn-circle m-1"
id="loggedInButton"
> >
</li> <svg
<li> xmlns="http://www.w3.org/2000/svg"
<a href="{{ meta.server_url }}/scenario" id="scenarioLink" width="24"
>Scenario</a 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> <li>
</ul> <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> </div>
{% endif %} </div>
{# Mobile drawer menu - slides in from the left #}
<div class="navbar-end"> <div class="drawer-side">
{% if not public_mode %} <label for="drawer-toggle" class="drawer-overlay"></label>
<a id="search-trigger" role="button" class="cursor-pointer"> <ul class="menu min-h-full w-80 bg-base-200 p-4 text-base-content">
<div {# Drawer header with close button #}
class="badge badge-dash bg-base-200 text-base-content/50 m-1 flex items-center space-x-1 p-2" <li class="mb-4">
> <div class="flex items-center justify-between">
<svg <span class="font-bold text-lg">Menu</span>
xmlns="http://www.w3.org/2000/svg" <label
width="16" for="drawer-toggle"
height="16" class="btn btn-sm btn-circle btn-ghost"
viewBox="0 0 24 24" aria-label="Close menu"
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" /> <svg
<circle cx="11" cy="11" r="8" /> xmlns="http://www.w3.org/2000/svg"
</svg> class="h-6 w-6"
<span id="search-shortcut">⌘K</span> fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</label>
</div> </div>
</a> </li>
{% endif %} {% if not public_mode %}
{% if meta.user.username == 'anonymous' or public_mode %} {# Predict link #}
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a> <li>
{% else %} <a
<div class="dropdown dropdown-end"> href="{{ meta.server_url }}/predict"
<div class="text-lg"
tabindex="0" id="predictLinkMobile"
role="button" >Predict</a
class="btn btn-ghost btn-circle m-1"
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" /> </li>
<circle cx="12" cy="10" r="3" /> {# Browse menu with submenu #}
<path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662" /> <li>
</svg> <details>
</div> <summary class="text-lg">Browse</summary>
<ul <ul>
tabindex="-1" <li>
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-sm" <a href="{{ meta.server_url }}/package" id="packageLinkMobile"
> >Package</a
<li><a href="{{ meta.user.url }}" id="accountbutton">Settings</a></li> >
<li> </li>
<form <li>
id="logoutForm" <a href="{{ meta.server_url }}/pathway" id="pathwayLinkMobile"
action="{% url 'logout' %}" >Pathway</a
method="post" >
style="display: none;" </li>
> <li>
{% csrf_token %} <a href="{{ meta.server_url }}/rule" id="ruleLinkMobile"
<input type="hidden" name="logout" value="true" /> >Rule</a
</form> >
<a </li>
href="#" <li>
id="logoutButton" <a href="{{ meta.server_url }}/compound" id="compoundLinkMobile"
onclick="event.preventDefault(); document.getElementById('logoutForm').submit();" >Compound</a
>Logout</a >
> </li>
</li> <li>
</ul> <a href="{{ meta.server_url }}/reaction" id="reactionLinkMobile"
</div> >Reaction</a
{% endif %} >
</li>
<li>
<a
href="{{ meta.server_url }}/model"
id="relative-reasoningLinkMobile"
>Model</a
>
</li>
<li>
<a href="{{ meta.server_url }}/scenario" id="scenarioLinkMobile"
>Scenario</a
>
</li>
</ul>
</details>
</li>
{% endif %}
</ul>
</div> </div>
</div> </div>

View File

@ -9,7 +9,7 @@
> >
<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 z-10 -translate-x-8"> <div class="absolute bottom-40 left-1/8 -translate-x-8">
<h2 class="text-base-100 text-left text-3xl text-shadow-lg"> <h2 class="text-base-100 text-left text-3xl text-shadow-lg">
Predict Your Pathway Predict Your Pathway
</h2> </h2>
@ -20,16 +20,68 @@
<div class="bg-base-200 mx-auto max-w-5xl shadow-md"> <div class="bg-base-200 mx-auto max-w-5xl shadow-md">
<!-- Predict Pathway Section --> <!-- Predict Pathway Section -->
<div <div
class="relative z-20 mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse" class="relative mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
> >
<div <div
class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out" class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out"
x-data="{
drawMode: false,
smiles: '',
loadExample(smilesStr, linkEl) {
if (this.drawMode && window.indexKetcher && window.indexKetcher.setMolecule) {
window.indexKetcher.setMolecule(smilesStr);
} else {
this.smiles = smilesStr;
}
const original = linkEl.textContent;
linkEl.textContent = 'loaded!';
setTimeout(() => linkEl.textContent = original, 1000);
},
syncFromKetcher() {
const ketcher = getKetcherInstance('index-ketcher');
if (ketcher && ketcher.getSmiles) {
try {
const s = ketcher.getSmiles();
if (s && s.trim()) this.smiles = s;
} catch (err) {
console.error('Failed to sync from Ketcher:', err);
}
}
},
submitForm() {
let finalSmiles = '';
if (this.drawMode) {
const ketcher = getKetcherInstance('index-ketcher');
if (ketcher && ketcher.getSmiles) {
try {
finalSmiles = ketcher.getSmiles().trim();
} catch (err) {
console.error('Failed to get SMILES from Ketcher:', err);
alert('Unable to extract structure. Please try again or switch to SMILES input.');
return;
}
} else {
alert('The drawing editor is still loading. Please wait and try again.');
return;
}
} else {
finalSmiles = this.smiles.trim();
}
if (!finalSmiles) {
alert('Please enter a SMILES string or draw a structure.');
return;
}
document.getElementById('index-form-smiles').value = finalSmiles;
document.getElementById('index-form').submit();
}
}"
x-init="$watch('drawMode', value => { if (!value) syncFromKetcher(); })"
> >
<div class="card-body"> <div class="card-body">
<div class="my-4 ml-8 flex h-fit flex-row items-center justify-start"> <div class="my-4 ml-8 flex h-fit flex-row items-center justify-start">
<div class="flex items-center gap-1"> <div class="flex items-center gap-1">
<label class="swap btn btn-ghost btn-sm p-1" title="Input Mode"> <label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
<input type="checkbox" /> <input type="checkbox" x-model="drawMode" />
<span class="swap-on flex items-center gap-1"> <span class="swap-on flex items-center gap-1">
<div <div
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1" class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
@ -82,16 +134,24 @@
<fieldset <fieldset
class="fieldset overflow-hidden transition-all duration-300 ease-in-out" class="fieldset overflow-hidden transition-all duration-300 ease-in-out"
:class="drawMode ? 'p-4' : 'p-8'"
> >
<form <form
id="index-form" id="index-form"
action="{{ meta.current_package.url }}/pathway" action="{{ meta.current_package.url }}/pathway"
method="POST" method="POST"
@submit.prevent="submitForm()"
> >
{% csrf_token %} {% csrf_token %}
<div <div
id="text-input-container" id="text-input-container"
class="scale-100 transform opacity-100 transition-all duration-300 ease-in-out" x-show="!drawMode"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
> >
<div class="join mx-auto w-full"> <div class="join mx-auto w-full">
<input <input
@ -99,6 +159,7 @@
id="index-form-text-input" id="index-form-text-input"
placeholder="canonical SMILES string" placeholder="canonical SMILES string"
class="input input-md join-item grow" class="input input-md join-item grow"
x-model="smiles"
/> />
<button class="btn btn-neutral join-item">Predict!</button> <button class="btn btn-neutral join-item">Predict!</button>
</div> </div>
@ -107,26 +168,35 @@
<a <a
href="#" href="#"
class="example-link hover:text-primary cursor-pointer" class="example-link hover:text-primary cursor-pointer"
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
title="load example" title="load example"
@click.prevent="loadExample('CN1C=NC2=C1C(=O)N(C(=O)N2C)C', $el)"
>Caffeine</a >Caffeine</a
> >
<a <a
href="#" href="#"
class="example-link hover:text-primary cursor-pointer" class="example-link hover:text-primary cursor-pointer"
data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
title="load example" title="load example"
@click.prevent="loadExample('CC(C)CC1=CC=C(C=C1)C(C)C(=O)O', $el)"
>Ibuprofen</a >Ibuprofen</a
> >
</div> </div>
<a class="absolute top-0 left-[calc(100%-5.4rem)]" href="#" <a
class="absolute top-0 left-[calc(100%-5.4rem)]"
href="/predict"
>Advanced</a >Advanced</a
> >
</div> </div>
</div> </div>
<div <div
id="ketcher-container" id="ketcher-container"
class="hidden w-full scale-95 transform opacity-0 transition-all duration-300 ease-in-out" x-show="drawMode"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="w-full"
> >
<iframe <iframe
id="index-ketcher" id="index-ketcher"
@ -256,6 +326,31 @@
<script language="javascript"> <script language="javascript">
var currentPackage = "{{ meta.current_package.url }}"; var currentPackage = "{{ meta.current_package.url }}";
// Helper function to safely get Ketcher instance from iframe
function getKetcherInstance(iframeId) {
const ketcherFrame = document.getElementById(iframeId);
if (!ketcherFrame) {
console.error("Ketcher iframe not found:", iframeId);
return null;
}
try {
if (
"contentWindow" in ketcherFrame &&
ketcherFrame.contentWindow.ketcher
) {
return ketcherFrame.contentWindow.ketcher;
}
} catch (err) {
console.error(
"Cannot access Ketcher iframe - possible CORS issue:",
err,
);
}
return null;
}
// Discourse API integration is now handled by discourse-api.js // Discourse API integration is now handled by discourse-api.js
// Function to render Discourse topics into cards // Function to render Discourse topics into cards
@ -278,16 +373,13 @@
const date = new Date(topic.created_at).toLocaleDateString(); const date = new Date(topic.created_at).toLocaleDateString();
return ` return `
<div class="card bg-white shadow-xs hover:shadow-lg transition-shadow duration-300 h-64 w-75 flex-shrink-0"> <div class="card bg-white shadow-sm hover:shadow-lg transition-shadow duration-300 h-52 w-75 flex-shrink-0">
<div class="card-body flex flex-col h-full"> <div class="card-body flex flex-col h-full justify-between">
<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 mb-2 line-clamp-5 overflow-hidden">
<a href="${topic.url}" target="_blank" class="hover:text-primary"> <a href="${topic.url}" target="_blank" class="hover:text-primary">
${topic.title} ${topic.title}
</a> </a>
</h3> </h3>
<div class="text-sm line-clamp-4 break-words" >
${topic.excerpt}
</div>
<div class="flex flex-row items-center justify-between"> <div class="flex flex-row items-center justify-between">
<div class="flex items-center gap-2"> <div class="flex items-center gap-2">
@ -313,141 +405,20 @@
// Make render function globally available // Make render function globally available
window.renderDiscourseTopics = renderDiscourseTopics; window.renderDiscourseTopics = renderDiscourseTopics;
// Toggle functionality with smooth animations // Ketcher iframe load handler - set up change event to sync SMILES
function toggleInputMode() { document.addEventListener("DOMContentLoaded", function () {
const toggle = $('input[type="checkbox"]'); const indexKetcher = document.getElementById("index-ketcher");
const textContainer = $("#text-input-container"); indexKetcher.addEventListener("load", function () {
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 checkKetcherReady = () => {
const win = this.contentWindow; const win = this.contentWindow;
if (win.ketcher && "editor" in win.ketcher) { if (win.ketcher && "editor" in win.ketcher) {
window.indexKetcher = win.ketcher; window.indexKetcher = win.ketcher;
win.ketcher.editor.event.change.handlers.push({
once: false,
priority: 0,
f: indexKetcherToTextInput,
ketcher: win.ketcher,
});
} else { } else {
setTimeout(checkKetcherReady, 100); setTimeout(checkKetcherReady, 100);
} }
}; };
checkKetcherReady(); 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> </script>
{% endblock main_content %} {% endblock main_content %}

View File

@ -1,48 +0,0 @@
<div
class="modal fade bs-modal-lg"
id="citemodal"
tabindex="-1"
role="dialog"
aria-labelledby="myLargeModalLabel"
aria-hidden="true"
>
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h3>How to cite enviPath</h3>
</div>
<div class="modal-body">
<ol class="list-group list-group-numbered">
<li class="list-group-item">
Hafner, J., Lorsbach, T., Schmidt, S. <em>et al.</em>
<cite
>Advancements in biotransformation pathway prediction:
enhancements, datasets, and novel functionalities in
enviPath.</cite
>
<a href="https://doi.org/10.1186/s13321-024-00881-6" target="_blank"
>J Cheminform 16, 93 (2024)</a
>
</li>
<li class="list-group-item">
Wicker, J., Lorsbach, T., Gütlein, M., Schmid, E., Latino, D.,
Kramer, S., Fenner, K.
<cite
>enviPath - The environmental contaminant biotransformation
pathway resource</cite
>
<a href="https://doi.org/10.1093/nar/gkv1229" target="_blank">
Nucleic Acids Research, Volume 44, Issue D1, 4 January 2016, Pages
D502-D508
</a>
</li>
</ol>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
Close
</button>
</div>
</div>
</div>
</div>

View File

@ -1,70 +1,85 @@
<div <dialog
class="modal fade"
tabindex="-1"
id="import_legacy_package_modal" id="import_legacy_package_modal"
role="dialog" class="modal"
aria-labelledby="import_legacy_package_modal" x-data="modalForm()"
aria-hidden="true" @close="reset()"
> >
<div class="modal-dialog"> <div class="modal-box">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="text-lg font-bold">Import Package from Legacy System</h3>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
<span class="sr-only">Close</span> <form method="dialog">
</button> <button
<h4 class="modal-title">Import Package from legacy System</h4> class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
</div> :disabled="isSubmitting"
<div class="modal-body"> >
<p>Create a Package based on the JSON Export of the legacy system.</p>
<form </button>
id="import-legacy-package-modal-form" </form>
accept-charset="UTF-8"
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
enctype="multipart/form-data" <p class="mb-4">
> Create a Package based on the JSON Export of the legacy system.
{% csrf_token %} </p>
<p> <form
<label class="btn btn-primary" for="legacyJsonFile"> id="import-legacy-package-modal-form"
<input accept-charset="UTF-8"
id="legacyJsonFile" method="post"
name="file" enctype="multipart/form-data"
type="file" >
style="display:none;" {% csrf_token %}
onchange="$('#upload-legacy-file-info').html(this.files[0].name)" <div class="form-control">
/> <label class="label">
Choose JSON File <span class="label-text">Legacy JSON File</span>
</label> </label>
<span class="label label-info" id="upload-legacy-file-info"></span> <input
<input type="file"
type="hidden" id="legacyJsonFile"
value="import-legacy-package-json" name="file"
name="hidden" class="file-input file-input-bordered w-full"
readonly="" accept=".json"
/> required
</p> />
</form> </div>
</div> <input
<div class="modal-footer"> type="hidden"
<a value="import-legacy-package-json"
id="import-legacy-package-modal-form-submit" name="hidden"
class="btn btn-primary" readonly
href="#" />
>Submit</a </form>
> </div>
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel <!-- Footer -->
</button> <div class="modal-action">
</div> <button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('import-legacy-package-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Importing...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#import-legacy-package-modal-form-submit").on("click", function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#import-legacy-package-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,70 +1,83 @@
<div <dialog
class="modal fade"
tabindex="-1"
id="import_package_modal" id="import_package_modal"
role="dialog" class="modal"
aria-labelledby="import_package_modal" x-data="modalForm()"
aria-hidden="true" @close="reset()"
> >
<div class="modal-dialog"> <div class="modal-box">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="text-lg font-bold">Import Package</h3>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
<span class="sr-only">Close</span> <form method="dialog">
</button> <button
<h4 class="modal-title">Import Package</h4> class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
</div> :disabled="isSubmitting"
<div class="modal-body"> >
<p>Create a Package based on a JSON Export.</p>
<form </button>
id="import-package-modal-form" </form>
accept-charset="UTF-8"
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
enctype="multipart/form-data" <p class="mb-4">Create a Package based on a JSON Export.</p>
> <form
{% csrf_token %} id="import-package-modal-form"
<p> accept-charset="UTF-8"
<label class="btn btn-primary" for="jsonFile"> method="post"
<input enctype="multipart/form-data"
id="jsonFile" >
name="file" {% csrf_token %}
type="file" <div class="form-control">
style="display:none;" <label class="label">
onchange="$('#upload-file-info').html(this.files[0].name)" <span class="label-text">JSON File</span>
/> </label>
Choose JSON File <input
</label> type="file"
<span class="label label-info" id="upload-file-info"></span> id="jsonFile"
<input name="file"
type="hidden" class="file-input file-input-bordered w-full"
value="import-package-json" accept=".json"
name="hidden" required
readonly="" />
/> </div>
</p> <input
</form> type="hidden"
</div> value="import-package-json"
<div class="modal-footer"> name="hidden"
<a readonly
id="import-package-modal-form-submit" />
class="btn btn-primary" </form>
href="#" </div>
>Submit</a
> <!-- Footer -->
<button type="button" class="btn btn-default" data-dismiss="modal"> <div class="modal-action">
Cancel <button
</button> type="button"
</div> class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('import-package-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Importing...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#import-package-modal-form-submit").on("click", function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#import-package-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,119 +1,137 @@
{% load static %} {% load static %}
<div
class="modal fade bs-modal-lg" <dialog
id="new_compound_modal" id="new_compound_modal"
tabindex="-1" class="modal"
aria-labelledby="new_compound_modal" x-data="modalForm()"
aria-modal="true" @close="reset()"
role="dialog"
> >
<div class="modal-dialog modal-lg"> <div class="modal-box max-w-3xl">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="text-lg font-bold">Create a new Compound</h3>
<button
type="button" <!-- Close button (X) -->
class="close" <form method="dialog">
data-dismiss="modal" <button
aria-label="Close" class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
> :disabled="isSubmitting"
<span aria-hidden="true">×</span> >
</button>
<h4 class="modal-title">Create a new Compound</h4> </button>
</div> </form>
<div class="modal-body">
<form <!-- Body -->
id="new_compound_modal_form" <div class="py-4">
accept-charset="UTF-8" <form
action="{% url 'package compound list' meta.current_package.uuid %}" id="new-compound-modal-form"
data-remote="true" accept-charset="UTF-8"
method="post" action="{% url 'package compound list' meta.current_package.uuid %}"
> method="post"
{% csrf_token %} >
<label for="compound-name">Name</label> {% csrf_token %}
<div class="form-control mb-3">
<label class="label" for="compound-name">
<span class="label-text">Name</span>
</label>
<input <input
id="compound-name" id="compound-name"
class="form-control" class="input input-bordered w-full"
name="compound-name" name="compound-name"
placeholder="Name" placeholder="Name"
required
/> />
<label for="compound-description">Description</label> </div>
<div class="form-control mb-3">
<label class="label" for="compound-description">
<span class="label-text">Description</span>
</label>
<input <input
id="compound-description" id="compound-description"
class="form-control" class="input input-bordered w-full"
name="compound-description" name="compound-description"
placeholder="Description" placeholder="Description"
/> />
<label for="compound-smiles">SMILES</label> </div>
<div class="form-control mb-3">
<label class="label" for="compound-smiles">
<span class="label-text">SMILES</span>
</label>
<input <input
type="text" type="text"
class="form-control" class="input input-bordered w-full"
name="compound-smiles" name="compound-smiles"
placeholder="SMILES" placeholder="SMILES"
id="compound-smiles" id="compound-smiles"
/> />
<p></p> </div>
<div>
<iframe <div class="mb-3">
id="new_compound_ketcher" <iframe
src="{% static '/js/ketcher2/ketcher.html' %}" id="new_compound_ketcher"
width="100%" src="{% static '/js/ketcher2/ketcher.html' %}"
height="510" width="100%"
></iframe> height="510"
</div> ></iframe>
<p></p> </div>
</form> </form>
</div> </div>
<div class="modal-footer">
<button <!-- Footer -->
type="button" <div class="modal-action">
class="btn btn-secondary pull-left" <button
data-dismiss="modal" type="button"
> class="btn"
Close onclick="this.closest('dialog').close()"
</button> :disabled="isSubmitting"
<button >
type="button" Close
class="btn btn-primary" </button>
id="new_compound_modal_form_submit" <button
> type="button"
Submit class="btn btn-primary"
</button> @click="submit('new-compound-modal-form')"
</div> :disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script>
function newCompoundModalketcherToNewCompoundModalTextInput() {
$("#compound-smiles").val(this.ketcher.getSmiles());
}
$(function () { <!-- Backdrop -->
$("#new_compound_ketcher").on("load", function () { <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>
<script>
document
.getElementById("new_compound_ketcher")
.addEventListener("load", function () {
const iframe = this;
const checkKetcherReady = () => { const checkKetcherReady = () => {
win = this.contentWindow; const win = iframe.contentWindow;
if (win.ketcher && "editor" in win.ketcher) { if (win.ketcher && "editor" in win.ketcher) {
win.ketcher.editor.event.change.handlers.push({ win.ketcher.editor.event.change.handlers.push({
once: false, once: false,
priority: 0, priority: 0,
f: newCompoundModalketcherToNewCompoundModalTextInput, f: function () {
document.getElementById("compound-smiles").value =
this.ketcher.getSmiles();
},
ketcher: win.ketcher, ketcher: win.ketcher,
}); });
} else { } else {
setTimeout(checkKetcherReady, 100); setTimeout(checkKetcherReady, 100);
} }
}; };
checkKetcherReady(); checkKetcherReady();
}); });
$(function () {
$("#new_compound_modal_form_submit").on("click", function (e) {
e.preventDefault();
$(this).prop("disabled", true);
// submit form
$("#new_compound_modal_form").submit();
});
});
});
</script> </script>

View File

@ -1,70 +1,96 @@
<div {% load static %}
class="modal fade"
tabindex="-1" <dialog
id="new_group_modal" id="new_group_modal"
role="dialog" class="modal"
aria-labelledby="new_group_modal" x-data="modalForm()"
aria-hidden="true" @close="reset()"
> >
<div class="modal-dialog"> <div class="modal-box">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="font-bold text-lg">New Group</h3>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
<span class="sr-only">Close</span> <form method="dialog">
</button> <button
<h4 class="modal-title">New Group</h4> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
</div> :disabled="isSubmitting"
<div class="modal-body"> >
<p>
Create new Group. You can assign users to the group once it is </button>
created. Description can be changed after creation. </form>
</p>
<form <!-- Body -->
id="new_group_modal_form" <div class="py-4">
accept-charset="UTF-8" <p class="mb-4">
action="{{ SERVER_BASE }}/group" Create new Group. You can assign users to the group once it is created.
data-remote="true" Description can be changed after creation.
method="post" </p>
>
{% csrf_token %} <form
<p> id="new-group-modal-form"
<label for="name">Name</label> accept-charset="UTF-8"
<input action="{{ SERVER_BASE }}/group"
id="name" method="post"
type="text" >
name="group-name" {% csrf_token %}
class="form-control"
placeholder="Name" <div class="form-control mb-3">
/> <label class="label" for="group-name">
</p> <span class="label-text">Name</span>
<p> </label>
<label for="description">Description</label> <input
<input id="group-name"
id="description" class="input input-bordered w-full"
type="text" name="group-name"
class="form-control" placeholder="Name"
placeholder="Description..." required
name="group-description" />
/> </div>
</p>
</form> <div class="form-control mb-3">
</div> <label class="label" for="group-description">
<div class="modal-footer"> <span class="label-text">Description</span>
<a id="new_group_modal_form_submit" class="btn btn-primary" href="#" </label>
>Submit</a <input
> id="group-description"
<button type="button" class="btn btn-default" data-dismiss="modal"> type="text"
Cancel class="input input-bordered w-full"
</button> placeholder="Description..."
</div> name="group-description"
/>
</div>
</form>
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('new-group-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#new_group_modal_form_submit").on("click", function () { <button :disabled="isSubmitting">close</button>
$("#new_group_modal_form").submit(); </form>
}); </dialog>
});
</script>

View File

@ -1,30 +1,66 @@
<div <dialog
class="modal fade"
tabindex="-1"
id="new_model_modal" id="new_model_modal"
role="dialog" class="modal"
aria-labelledby="new_model_modal" x-data="{
aria-hidden="true" isSubmitting: false,
modelType: '',
buildAppDomain: false,
reset() {
this.isSubmitting = false;
this.modelType = '';
this.buildAppDomain = false;
},
get showMlrr() {
return this.modelType === 'mlrr';
},
get showRbrr() {
return this.modelType === 'rbrr';
},
get showEnviformer() {
return this.modelType === 'enviformer';
},
submit(formId) {
const form = document.getElementById(formId);
if (form && form.checkValidity()) {
this.isSubmitting = true;
form.submit();
} else if (form) {
form.reportValidity();
}
}
}"
@close="reset()"
> >
<div class="modal-dialog modal-lg"> <div class="modal-box max-w-3xl">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="text-lg font-bold">New Model</h3>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
<span class="sr-only">Close</span> <form method="dialog">
</button> <button
<h4 class="modal-title">New Model</h4> class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
</div> :disabled="isSubmitting"
<div class="modal-body"> >
<form
id="new_model_form" </button>
accept-charset="UTF-8" </form>
action="{{ meta.current_package.url }}/model"
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <form
{% csrf_token %} id="new_model_form"
<div class="jumbotron"> accept-charset="UTF-8"
action="{{ meta.current_package.url }}/model"
method="post"
>
{% csrf_token %}
<div class="alert alert-info mb-4">
<span>
Create a new Model to limit the number of degradation products in Create a new Model to limit the number of degradation products in
the prediction. You just need to set a name and the packages you the prediction. You just need to set a name and the packages you
want the object to be based on. There are multiple types of models want the object to be based on. There are multiple types of models
@ -32,239 +68,270 @@
<a <a
target="_blank" target="_blank"
href="https://wiki.envipath.org/index.php/relative-reasoning" href="https://wiki.envipath.org/index.php/relative-reasoning"
role="button" class="link"
>wiki &gt;&gt;</a >wiki &gt;&gt;</a
> >
</div> </span>
<!-- Name --> </div>
<label for="model-name">Name</label>
<!-- Name -->
<div class="form-control mb-3">
<label class="label" for="model-name">
<span class="label-text">Name</span>
</label>
<input <input
id="model-name" id="model-name"
name="model-name" name="model-name"
class="form-control" class="input input-bordered w-full"
placeholder="Name" placeholder="Name"
required
/> />
</div>
<!-- Description --> <!-- Description -->
<label for="model-description">Description</label> <div class="form-control mb-3">
<label class="label" for="model-description">
<span class="label-text">Description</span>
</label>
<input <input
id="model-description" id="model-description"
name="model-description" name="model-description"
class="form-control" class="input input-bordered w-full"
placeholder="Description" placeholder="Description"
/> />
</div>
<!-- Model Type --> <!-- Model Type -->
<label for="model-type">Model Type</label> <div class="form-control mb-3">
<label class="label" for="model-type">
<span class="label-text">Model Type</span>
</label>
<select <select
id="model-type" id="model-type"
name="model-type" name="model-type"
class="form-control" class="select select-bordered w-full"
data-width="100%" x-model="modelType"
required
> >
<option disabled selected>Select Model Type</option> <option value="" disabled selected>Select Model Type</option>
{% for k, v in model_types.items %} {% for k, v in model_types.items %}
<option value="{{ v }}">{{ k }}</option> <option value="{{ v }}">{{ k }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div>
<!-- Rule Packages --> <!-- Rule Packages (MLRR, RBRR) -->
<div id="rule-packages" class="ep-model-param mlrr rbrr"> <div class="form-control mb-3" x-show="showMlrr || showRbrr" x-cloak>
<label for="model-rule-packages">Rule Packages</label> <label class="label" for="model-rule-packages">
<select <span class="label-text">Rule Packages</span>
id="model-rule-packages" </label>
name="model-rule-packages" <select
data-actions-box="true" id="model-rule-packages"
class="form-control" name="model-rule-packages"
multiple class="select select-bordered w-full h-32"
data-width="100%" multiple
> >
<option disabled>Reviewed Packages</option> <optgroup label="Reviewed Packages">
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</optgroup>
<option disabled>Unreviewed Packages</option> <optgroup label="Unreviewed Packages">
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </optgroup>
</div> </select>
<label class="label">
<span class="label-text-alt">Hold Ctrl/Cmd to select multiple</span>
</label>
</div>
<!-- Data Packages --> <!-- Data Packages (MLRR, RBRR, Enviformer) -->
<div id="data-packages" class="ep-model-param mlrr rbrr enviformer"> <div
<label for="model-data-packages">Data Packages</label> class="form-control mb-3"
<select x-show="showMlrr || showRbrr || showEnviformer"
id="model-data-packages" x-cloak
name="model-data-packages" >
data-actions-box="true" <label class="label" for="model-data-packages">
class="form-control" <span class="label-text">Data Packages</span>
multiple </label>
data-width="100%" <select
> id="model-data-packages"
<option disabled>Reviewed Packages</option> name="model-data-packages"
class="select select-bordered w-full h-32"
multiple
>
<optgroup label="Reviewed Packages">
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</optgroup>
<option disabled>Unreviewed Packages</option> <optgroup label="Unreviewed Packages">
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </optgroup>
</div> </select>
<label class="label">
<span class="label-text-alt">Hold Ctrl/Cmd to select multiple</span>
</label>
</div>
<!-- Fingerprinter --> <!-- Fingerprinter (MLRR) -->
<div id="fingerprinter" class="ep-model-param mlrr"> <div class="form-control mb-3" x-show="showMlrr" x-cloak>
<label for="model-fingerprinter">Fingerprinter</label> <label class="label" for="model-fingerprinter">
<select <span class="label-text">Fingerprinter</span>
id="model-fingerprinter" </label>
name="model-fingerprinter" <select
data-actions-box="true" id="model-fingerprinter"
class="form-control" name="model-fingerprinter"
multiple class="select select-bordered w-full h-32"
data-width="100%" multiple
> >
<option value="MACCS" selected>MACCS Fingerprinter</option> <option value="MACCS" selected>MACCS Fingerprinter</option>
{% if meta.enabled_features.PLUGINS and additional_descriptors %} {% if meta.enabled_features.PLUGINS and additional_descriptors %}
<option disabled selected> <optgroup label="Additional Fingerprinter / Descriptor">
Select Additional Fingerprinter / Descriptor
</option>
{% for k, v in additional_descriptors.items %} {% for k, v in additional_descriptors.items %}
<option value="{{ v }}">{{ k }}</option> <option value="{{ v }}">{{ k }}</option>
{% endfor %} {% endfor %}
{% endif %} </optgroup>
</select> {% endif %}
</div> </select>
<label class="label">
<span class="label-text-alt">Hold Ctrl/Cmd to select multiple</span>
</label>
</div>
<!-- Threshold --> <!-- Threshold (MLRR, Enviformer) -->
<div id="threshold" class="ep-model-param mlrr enviformer"> <div
<label for="model-threshold">Threshold</label> class="form-control mb-3"
<input x-show="showMlrr || showEnviformer"
type="number" x-cloak
min="0" >
max="1" <label class="label" for="model-threshold">
step="0.05" <span class="label-text">Threshold</span>
value="0.5" </label>
id="model-threshold" <input
name="model-threshold" type="number"
class="form-control" min="0"
/> max="1"
</div> step="0.05"
value="0.5"
id="model-threshold"
name="model-threshold"
class="input input-bordered w-full"
/>
</div>
<div id="appdomain" class="ep-model-param mlrr"> <!-- Applicability Domain (MLRR) -->
{% if meta.enabled_features.APPLICABILITY_DOMAIN %} {% if meta.enabled_features.APPLICABILITY_DOMAIN %}
<!-- Build AD? --> <div x-show="showMlrr" x-cloak>
<div class="checkbox"> <div class="form-control mb-3">
<label> <label class="label cursor-pointer justify-start gap-3">
<input <input
type="checkbox" type="checkbox"
id="build-app-domain" id="build-app-domain"
name="build-app-domain" name="build-app-domain"
/>Also build an Applicability Domain? class="checkbox"
x-model="buildAppDomain"
/>
<span class="label-text"
>Also build an Applicability Domain?</span
>
</label>
</div>
<div x-show="buildAppDomain" x-cloak class="ml-4 space-y-3">
<div class="form-control">
<label class="label" for="num-neighbors">
<span class="label-text">Number of Neighbors</span>
</label> </label>
</div>
<div id="ad-params" style="display:none">
<!-- Num Neighbors -->
<label for="num-neighbors">Number of Neighbors</label>
<input <input
id="num-neighbors" id="num-neighbors"
name="num-neighbors" name="num-neighbors"
type="number" type="number"
class="form-control" class="input input-bordered w-full"
value="5" value="5"
step="1" step="1"
min="0" min="0"
max="10" max="10"
/> />
<!-- Local Compatibility --> </div>
<label for="local-compatibility-threshold"
>Local Compatibility Threshold</label <div class="form-control">
> <label class="label" for="local-compatibility-threshold">
<span class="label-text">Local Compatibility Threshold</span>
</label>
<input <input
id="local-compatibility-threshold" id="local-compatibility-threshold"
name="local-compatibility-threshold" name="local-compatibility-threshold"
type="number" type="number"
class="form-control" class="input input-bordered w-full"
value="0.5"
step="0.01"
min="0"
max="1"
/>
<!-- Reliability -->
<label for="reliability-threshold">Reliability Threshold</label>
<input
id="reliability-threshold"
name="reliability-threshold"
type="number"
class="form-control"
value="0.5" value="0.5"
step="0.01" step="0.01"
min="0" min="0"
max="1" max="1"
/> />
</div> </div>
{% endif %}
<div class="form-control">
<label class="label" for="reliability-threshold">
<span class="label-text">Reliability Threshold</span>
</label>
<input
id="reliability-threshold"
name="reliability-threshold"
type="number"
class="input input-bordered w-full"
value="0.5"
step="0.01"
min="0"
max="1"
/>
</div>
</div>
</div> </div>
</form> {% endif %}
</div> </form>
<div class="modal-footer"> </div>
<a id="new_model_modal_form_submit" class="btn btn-primary" href="#"
>Submit</a <!-- Footer -->
> <div class="modal-action">
<button type="button" class="btn btn-default" data-dismiss="modal"> <button
Cancel type="button"
</button> class="btn"
</div> onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('new_model_form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
// Built in Model Types <button :disabled="isSubmitting">close</button>
var nativeModelTypes = ["mlrr", "rbrr", "enviformer"]; </form>
</dialog>
// Initially hide all "specific" forms
$(".ep-model-param").each(function () {
$(this).hide();
});
$("#model-type").selectpicker();
$("#model-fingerprinter").selectpicker();
$("#model-rule-packages").selectpicker();
$("#model-data-packages").selectpicker();
$("#build-app-domain").change(function () {
if ($(this).is(":checked")) {
$("#ad-params").show();
} else {
$("#ad-params").hide();
}
});
// On change hide all and show only selected
$("#model-type").change(function () {
$(".ep-model-param").hide();
var modelType = $("#model-type").val();
if (nativeModelTypes.indexOf(modelType) !== -1) {
$("." + modelType).show();
} else {
// do nothing
}
});
$("#new_model_modal_form_submit").on("click", function (e) {
e.preventDefault();
$("#new_model_form").submit();
});
});
</script>

View File

@ -1,68 +1,93 @@
<div {% load static %}
class="modal fade"
tabindex="-1" <dialog
id="new_package_modal" id="new_package_modal"
role="dialog" class="modal"
aria-labelledby="new_package_modal" x-data="modalForm()"
aria-hidden="true" @close="reset()"
> >
<div class="modal-dialog"> <div class="modal-box">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="font-bold text-lg">New Package</h3>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
<span class="sr-only">Close</span> <form method="dialog">
</button> <button
<h4 class="modal-title">New Package</h4> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
</div> :disabled="isSubmitting"
<div class="modal-body"> >
<p>Create new package. Description can be changed later.</p>
<form </button>
id="new_package_modal_form" </form>
accept-charset="UTF-8"
action="" <!-- Body -->
data-remote="true" <div class="py-4">
method="post" <p class="mb-4">Create new package. Description can be changed later.</p>
>
{% csrf_token %} <form
<p> id="new-package-modal-form"
<label for="name">Name</label> accept-charset="UTF-8"
<input action=""
id="name" method="post"
class="form-control" >
name="package-name" {% csrf_token %}
placeholder="Name"
/> <div class="form-control mb-3">
</p> <label class="label" for="package-name">
<p> <span class="label-text">Name</span>
<label for="description">Description</label> </label>
<input <input
id="description" id="package-name"
type="text" class="input input-bordered w-full"
rows="3" name="package-name"
class="form-control" placeholder="Name"
placeholder="Description..." required
name="package-description" />
/> </div>
</p>
</form> <div class="form-control mb-3">
</div> <label class="label" for="package-description">
<div class="modal-footer"> <span class="label-text">Description</span>
<a id="new_package_modal_form_submit" class="btn btn-primary" href="#" </label>
>Submit</a <input
> id="package-description"
<button type="button" class="btn btn-default" data-dismiss="modal"> type="text"
Cancel class="input input-bordered w-full"
</button> placeholder="Description..."
</div> name="package-description"
/>
</div>
</form>
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('new-package-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#new_package_modal_form_submit").on("click", function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#new_package_modal_form").submit(); </dialog>
});
});
</script>

View File

@ -1,376 +0,0 @@
{% load static %}
<div
class="modal fade"
tabindex="-1"
id="new_pathway_modal"
role="dialog"
aria-labelledby="new_pathway_modal"
aria-hidden="true"
style="overflow-y: auto;"
>
<!-- FIXME: make width dynamic-->
<div class="modal-dialog" id="new_pathway_modal_dialog" style="width:900px">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="js-title-step"></h4>
</div>
<div class="modal-body hide" data-step="1" data-title="New Pathway">
<div class="jumbotron">
Create a new pathway by entering the root compound and a name. Then
select if you want to use the prediction engine to generate a
predicted pathway or create an empty pathway that you fill in by
yourself. If you choose to predict a pathway, you can modify the
settings for the prediction, or use the default settings and just
click Submit.
</div>
<div class="modal-body">
{% if current_user.name == 'anonymous' %}
<div class="alert alert-warning">
You are currently logged in as Anonymous. Please note: Pathways
entered or predicted as anonymous user will be deleted after 30
days. Please log in to save your results.
</div>
{% endif %}
</div>
<div class="row">
<div class="col-md-6">
<label for="name">Name</label>
<input
id="name"
class="form-control"
name="name"
placeholder="Name"
/>
<label for="description">Description</label>
<input
id="description"
class="form-control"
name="description"
placeholder="no description"
/>
</div>
<div class="col-md-6">
<label for="predict">Predict pathway or build yourself?</label>
<div class="radio" id="predict">
<p>
<label>
<input
type="radio"
name="predict"
id="radioPredict"
value="predict"
checked
/>Predict pathway
</label>
</p>
<p>
<label>
<input
type="radio"
name="predict"
id="radioIncremental"
value="incremental"
/>Incremental prediction
</label>
</p>
<p>
<label>
<input
type="radio"
name="predict"
id="radioBuild"
value="build"
/>Build pathway
</label>
</p>
</div>
</div>
</div>
<label for="smilesinput">SMILES</label>
<table style="width: 100%">
<colgroup>
<col span="1" style="width: 90%;" />
<col span="1" style="width: 10%;" />
</colgroup>
<tr>
<td>
<input
id="smilesinput"
class="form-control"
name="smilesinput"
placeholder="C1CCCCC1"
autocapitalize="none"
/>
</td>
<td>
<button type="button" class="btn btn-default" id="render-button">
Render
</button>
</td>
</tr>
</table>
<p id="ketcher_container"></p>
<div>
<iframe
id="ifKetcher"
src="{% static '/js/ketcher/ketcher.html' %}"
width="850"
height="510"
></iframe>
</div>
</div>
<div
class="modal-body hide"
data-step="2"
data-title="New Pathway - Advanced Settings"
>
<div class="jumbotron">
Choose if you want to use an existing setting, or create a new one for
this pathway prediction. Then click Submit to use the specified
setting, or click next to set the parameters.
</div>
<div id="settings">
<div class="radio" id="settingRadio">
<p>
<label>
<input
type="radio"
name="existing"
id="radioDefault"
value="exisiting"
checked
/>
Use Default
</label>
</p>
<p>
<label>
<input
type="radio"
name="existing"
id="radioExists"
value="exisiting"
/>
Select Existing
</label>
</p>
<p>
<label>
<input
type="radio"
name="existing"
id="radioNew"
value="temporary"
/>
Create New
</label>
</p>
</div>
<select id="settingSelect" name="settingSelect" class="form-control">
{% for setting in available_settings %}
<option value="{{ setting.id }}">{{ setting.name|safe }}</option>
{% endfor %}
</select>
<p></p>
</div>
</div>
{% with step_offset=1 %}
{% include "templates/modals/collections/new_setting_modal_body.html" %}
{% endwith %}
<div class="modal-footer">
<button
type="button"
class="btn btn-default js-btn-step pull-left"
data-orientation="cancel"
onclick="reset()"
data-dismiss="modal"
></button>
<button
type="button"
class="btn btn-default js-btn-step"
data-orientation="previous"
id="backbutton"
></button>
<button
type="button"
class="btn btn-default js-btn-step"
data-orientation="next"
id="nextbutton"
></button>
<a id="modal-form-submit" class="btn btn-primary" href="#">Submit</a>
</div>
</div>
</div>
</div>
<script>
s = new Setting(
"settingName",
"package_multi_select",
"modelSelect",
"cutoff",
"evalType",
"availableTS",
"forms",
"truncatorTable",
"summaryTable",
);
$(function () {
// hide all forms
$("#forms").children().hide();
$("#render-button").on("click", function () {
syncKetcherAndTextInput("text", "ifKetcher", "smilesinput");
});
// If theres a change in the in '#smilesinput' sync the value to ketcher
$("#smilesinput").on("input", function () {
syncKetcherAndTextInput("text", "ifKetcher", "smilesinput");
});
// If theres an update in ketcher sync it to textinput
setInterval(function () {
syncKetcherAndTextInput("ketcher", "ifKetcher", "smilesinput");
}, 250);
$("#smilesinput").on("blur", function () {
syncKetcherAndTextInput("text", "ifKetcher", "smilesinput");
});
$("#smilesinput").on("keypress", function (event) {
if (event.keyCode == 13) {
syncKetcherAndTextInput("text", "ifKetcher", "smilesinput");
}
});
// Show forms depending on the selected TS
$("#availableTS").on("change", function (e) {
e.preventDefault();
var type = $(this).val();
// hide current content
$("#forms").children().hide();
if (type === "") {
return;
}
$("#" + type + "_form").show();
});
$("#modelSelect").on("change", function () {
setCutoff = function (thresh) {
$("#cutoff").val(thresh);
};
var modelUri = $("#modelSelect :selected").val();
fillPRCurve(modelUri, setCutoff);
});
// Add a TS to the setting
$("#add-ts-button").on("click", function (e) {
e.preventDefault();
s.addTruncator();
});
$("input[type=radio][name=predict]").change(function () {
if (this.id == "radioBuild") {
$("#nextbutton").prop("disabled", true);
} else {
$("#nextbutton").prop("disabled", false);
}
});
$("input[type=radio][name=existing]").change(function () {
if (this.id == "radioDefault" || this.id == "radioExists") {
if (this.id == "radioDefault") {
$("#settingSelect").prop("disabled", true);
} else {
$("#settingSelect").prop("disabled", false);
}
$("#nextbutton").prop("disabled", true);
} else {
// build...
$("#settingSelect").prop("disabled", true);
$("#nextbutton").prop("disabled", false);
}
});
var pwStep1 = function () {
console.log("pw step 1");
// Make "Next" to "Advanced"
$("#nextbutton").val("Advanced");
};
var pwStep2 = function () {
console.log("pw step 2");
// Make "Advanced" to "Next"
$("#nextbutton").val("Next");
// As "Use default is preselected" disable "Next" button
$("#nextbutton").prop("disabled", true);
// Disable setting dropdown as long as the correspndonding radio isnt checked
$("#settingSelect").prop("disabled", true);
// Show submit button
$("#modal-form-submit").show();
};
var settingStep1 = function () {
// First step sets name and packages
s.extractName();
s.extractSelectedPackages();
};
var settingStep2 = function () {
// Seconds step gathers relative reasoning params
s.extractRelativeReasoning();
s.extractCutoff();
s.extractEvaluationType();
};
var settingStep3 = function () {
s.updateTable();
s.updateSummaryTable();
// hide duplicate submit...
$("#nextbutton").hide();
};
var postPathway = function () {
console.log("Complete!");
console.log(s.tsParams);
console.log("Getting SMILES");
};
function dummy() {
console.log("dummy");
}
$("#new_pathway_modal").modalSteps({
btnCancelHtml: "Cancel",
btnPreviousHtml: "Back",
btnNextHtml: "Next",
btnLastStepHtml: "Submit",
disableNextButton: false,
completeCallback: postPathway,
callbacks: {
1: pwStep1,
2: pwStep2,
3: dummy,
4: settingStep1,
5: settingStep2,
6: settingStep3,
},
});
$("#modal-form-submit").on("click", function () {
e.preventDefault();
postPathway();
});
});
</script>

View File

@ -1,185 +1,260 @@
{% load static %} {% load static %}
<div id="new_prediction_setting_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="new_prediction_setting_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="{
<h5 class="modal-title">Create a Prediction Setting</h5> isSubmitting: false,
<button tpMethod: '',
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
To create a Prediction Setting fill the form below and click "Create"
</p>
<form
id="new-prediction-setting-modal-form"
accept-charset="UTF-8"
action=""
data-remote="true"
method="post"
>
{% csrf_token %}
<label for="prediction-setting-name">Name</label> reset() {
this.isSubmitting = false;
this.tpMethod = '';
},
async submit() {
const form = document.getElementById('new-prediction-setting-modal-form');
if (!form.checkValidity()) {
form.reportValidity();
return;
}
this.isSubmitting = true;
const formData = new FormData(form);
try {
const response = await fetch('/setting', {
method: 'POST',
body: new URLSearchParams(formData)
});
if (response.ok) {
location.reload();
}
} catch (error) {
console.error('Error creating setting:', error);
} finally {
this.isSubmitting = false;
}
}
}"
@close="reset()"
>
<div class="modal-box max-w-2xl">
<!-- Header -->
<h3 class="text-lg font-bold">Create a Prediction Setting</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<p class="mb-4">
To create a Prediction Setting fill the form below and click "Create"
</p>
<form
id="new-prediction-setting-modal-form"
accept-charset="UTF-8"
action=""
method="post"
>
{% csrf_token %}
<div class="form-control mb-3">
<label class="label" for="prediction-setting-name">
<span class="label-text">Name</span>
</label>
<input <input
id="prediction-setting-name" id="prediction-setting-name"
name="prediction-setting-name" name="prediction-setting-name"
class="form-control" class="input input-bordered w-full"
placeholder="Name" placeholder="Name"
required
/> />
<label for="prediction-setting-description">Description</label> </div>
<div class="form-control mb-3">
<label class="label" for="prediction-setting-description">
<span class="label-text">Description</span>
</label>
<input <input
id="prediction-setting-description" id="prediction-setting-description"
name="prediction-setting-description" name="prediction-setting-description"
class="form-control" class="input input-bordered w-full"
placeholder="Description" placeholder="Description"
/> />
</div>
<label for="prediction-setting-max-nodes">Max #Nodes</label> <div class="form-control mb-3">
<label class="label" for="prediction-setting-max-nodes">
<span class="label-text">Max #Nodes</span>
</label>
<input <input
id="prediction-setting-max-nodes" id="prediction-setting-max-nodes"
type="number" type="number"
class="form-control" class="input input-bordered w-full"
name="prediction-setting-max-nodes" name="prediction-setting-max-nodes"
value="30" value="30"
min="1" min="1"
max="50" max="50"
step="1" step="1"
/> />
<label for="prediction-setting-max-depth">Max Depth</label> </div>
<div class="form-control mb-3">
<label class="label" for="prediction-setting-max-depth">
<span class="label-text">Max Depth</span>
</label>
<input <input
id="prediction-setting-max-depth" id="prediction-setting-max-depth"
type="number" type="number"
class="form-control" class="input input-bordered w-full"
name="prediction-setting-max-depth" name="prediction-setting-max-depth"
value="5" value="5"
min="1" min="1"
max="8" max="8"
step="1" step="1"
/> />
</div>
<label for="tp-generation-method">TP Generation Method</label> <div class="form-control mb-3">
<label class="label" for="tp-generation-method">
<span class="label-text">TP Generation Method</span>
</label>
<select <select
id="tp-generation-method" id="tp-generation-method"
name="tp-generation-method" name="tp-generation-method"
class="form-control" class="select select-bordered w-full"
data-width="100%" x-model="tpMethod"
required
> >
<option disabled selected>Select how TPs are generated</option> <option value="" disabled selected>
Select how TPs are generated
</option>
<option value="rule-based-prediction-setting">Rule Based</option> <option value="rule-based-prediction-setting">Rule Based</option>
<option value="model-based-prediction-setting">Model Based</option> <option value="model-based-prediction-setting">Model Based</option>
</select> </select>
<div id="rule-based-prediction-setting-specific-form"> </div>
<!-- Rule Packages -->
<label>Rule Packages</label><br /> <!-- Rule Based Settings -->
<div x-show="tpMethod === 'rule-based-prediction-setting'" x-cloak>
<div class="form-control mb-3">
<label class="label">
<span class="label-text">Rule Packages</span>
</label>
<select <select
id="rule-based-prediction-setting-packages" id="rule-based-prediction-setting-packages"
name="rule-based-prediction-setting-packages" name="rule-based-prediction-setting-packages"
data-actions-box="true" class="select select-bordered w-full h-32"
class="form-control"
multiple multiple
data-width="100%"
> >
<option disabled>Reviewed Packages</option> <optgroup label="Reviewed Packages">
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</optgroup>
<option disabled>Unreviewed Packages</option> <optgroup label="Unreviewed Packages">
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</optgroup>
</select> </select>
<label class="label">
<span class="label-text-alt"
>Hold Ctrl/Cmd to select multiple</span
>
</label>
</div> </div>
<div id="model-based-prediction-setting-specific-form"> </div>
<label>Select Model</label><br />
<!-- Model Based Settings -->
<div x-show="tpMethod === 'model-based-prediction-setting'" x-cloak>
<div class="form-control mb-3">
<label class="label" for="model-based-prediction-setting-model">
<span class="label-text">Select Model</span>
</label>
<select <select
id="model-based-prediction-setting-model" id="model-based-prediction-setting-model"
name="model-based-prediction-setting-model" name="model-based-prediction-setting-model"
class="form-control" class="select select-bordered w-full"
data-width="100%"
> >
<option disabled selected>Select the model</option> <option value="" disabled selected>Select the model</option>
{% for m in models %} {% for m in models %}
<option value="{{ m.url }}">{{ m.name|safe }}</option> <option value="{{ m.url }}">{{ m.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
<label for="model-based-prediction-setting-threshold" </div>
>Threshold</label
> <div class="form-control mb-3">
<label class="label" for="model-based-prediction-setting-threshold">
<span class="label-text">Threshold</span>
</label>
<input <input
id="model-based-prediction-setting-threshold" id="model-based-prediction-setting-threshold"
name="model-based-prediction-setting-threshold" name="model-based-prediction-setting-threshold"
class="form-control" class="input input-bordered w-full"
placeholder="0.25" placeholder="0.25"
type="number" type="number"
min="0"
max="1"
step="0.05"
/> />
</div> </div>
</div>
<input <div class="form-control">
class="form-check-input" <label class="label cursor-pointer justify-start gap-3">
type="checkbox" <input
value="on" type="checkbox"
id="prediction-setting-new-default" class="checkbox"
name="prediction-setting-new-default" value="on"
/> id="prediction-setting-new-default"
<label class="form-check-label" for="prediction-setting-new-default" name="prediction-setting-new-default"
>Set this setting as new default</label />
> <span class="label-text">Set this setting as new default</span>
</form> </label>
</div> </div>
<div class="modal-footer"> </form>
<button type="button" class="btn btn-secondary" data-dismiss="modal"> </div>
Close
</button> <!-- Footer -->
<button <div class="modal-action">
type="button" <button
class="btn btn-primary" type="button"
id="new-prediction-setting-modal-submit" class="btn"
> onclick="this.closest('dialog').close()"
Create :disabled="isSubmitting"
</button> >
</div> Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit()"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Create</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script>
$(function () {
// Initially hide all "specific" forms
$("div[id$='-specific-form']").each(function () {
$(this).hide();
});
$("#rule-based-prediction-setting-packages").selectpicker(); <!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
// On change hide all and show only selected <button :disabled="isSubmitting">close</button>
$("#tp-generation-method").change(function () { </form>
$("div[id$='-specific-form']").each(function () { </dialog>
$(this).hide();
});
val = $("option:selected", this).val();
$("#" + val + "-specific-form").show();
});
$("#new-prediction-setting-modal-submit").click(function (e) {
e.preventDefault();
// $('#new-prediction-setting-modal-form').submit();
const formData = $("#new-prediction-setting-modal-form").serialize();
$.post("/setting", formData, function (response) {
location.reload();
});
});
});
</script>

View File

@ -1,91 +1,105 @@
{% load static %} {% load static %}
<div
class="modal fade bs-modal-lg" <dialog
id="new_reaction_modal" id="new_reaction_modal"
tabindex="-1" class="modal"
aria-labelledby="new_reaction_modal" x-data="modalForm()"
aria-modal="true" @close="reset()"
role="dialog"
> >
<div class="modal-dialog modal-lg"> <div class="modal-box max-w-3xl">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="font-bold text-lg">Create a new Reaction</h3>
<button
type="button" <!-- Close button (X) -->
class="close" <form method="dialog">
data-dismiss="modal" <button
aria-label="Close" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
> :disabled="isSubmitting"
<span aria-hidden="true">×</span> >
</button>
<h4 class="modal-title">Create a new Reaction</h4> </button>
</div> </form>
<div class="modal-body">
<form <!-- Body -->
id="new_reaction_modal_form" <div class="py-4">
accept-charset="UTF-8" <form
action="{% url 'package reaction list' meta.current_package.uuid %}" id="new-reaction-modal-form"
data-remote="true" accept-charset="UTF-8"
method="post" action="{% url 'package reaction list' meta.current_package.uuid %}"
> method="post"
{% csrf_token %} >
<label for="reaction-name">Name</label> {% csrf_token %}
<div class="form-control mb-3">
<label class="label" for="reaction-name">
<span class="label-text">Name</span>
</label>
<input <input
id="reaction-name" id="reaction-name"
class="form-control" class="input input-bordered w-full"
name="reaction-name" name="reaction-name"
placeholder="Name" placeholder="Name"
required
/> />
<label for="reaction-description">Description</label> </div>
<div class="form-control mb-3">
<label class="label" for="reaction-description">
<span class="label-text">Description</span>
</label>
<input <input
id="reaction-description" id="reaction-description"
class="form-control" class="input input-bordered w-full"
name="reaction-description" name="reaction-description"
placeholder="Description" placeholder="Description"
/> />
<p></p> </div>
<div>
<iframe <div class="mb-3">
id="new_reaction_ketcher" <iframe
src="{% static '/js/ketcher2/ketcher.html' %}" id="new_reaction_ketcher"
width="100%" src="{% static '/js/ketcher2/ketcher.html' %}"
height="510" width="100%"
></iframe> height="510"
</div> ></iframe>
<input type="hidden" name="reaction-smirks" id="reaction-smirks" /> </div>
<p></p>
</form> <input type="hidden" name="reaction-smirks" id="reaction-smirks" />
</div> </form>
<div class="modal-footer"> </div>
<button
type="button" <!-- Footer -->
class="btn btn-secondary pull-left" <div class="modal-action">
data-dismiss="modal" <button
> type="button"
Close class="btn"
</button> onclick="this.closest('dialog').close()"
<button :disabled="isSubmitting"
type="button" >
class="btn btn-primary" Close
id="new_reaction_modal_form_submit" </button>
> <button
Submit type="button"
</button> class="btn btn-primary"
</div> @click="
const k = getKetcher('new_reaction_ketcher');
document.getElementById('reaction-smirks').value = k.getSmiles();
submit('new-reaction-modal-form');
"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script>
$(function () {
$("#new_reaction_modal_form_submit").on("click", function (e) {
e.preventDefault();
$(this).prop("disabled", true);
k = getKetcher("new_reaction_ketcher"); <!-- Backdrop -->
$("#reaction-smirks").val(k.getSmiles()); <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
// submit form </form>
$("#new_reaction_modal_form").submit(); </dialog>
});
});
</script>

View File

@ -1,120 +1,140 @@
{% load static %} {% load static %}
<div
class="modal fade bs-modal-lg" <dialog
id="new_rule_modal" id="new_rule_modal"
tabindex="-1" class="modal"
aria-labelledby="new_rule_modal" x-data="{
aria-modal="true" ...modalForm(),
role="dialog" smirksVizHtml: '',
updateSmirksViz() {
const smirks = document.getElementById('rule-smirks').value;
if (!smirks) {
this.smirksVizHtml = '';
return;
}
const img = new Image();
img.src = '{% url 'depict' %}?is_query_smirks=true&smirks=' + encodeURIComponent(smirks);
img.style.width = '100%';
img.style.height = '100%';
img.style.objectFit = 'cover';
img.onload = () => {
this.smirksVizHtml = img.outerHTML;
};
img.onerror = () => {
this.smirksVizHtml = `
<div class='alert alert-error' role='alert'>
<h4 class='alert-heading'>Could not render SMIRKS!</h4>
<p>Could not render SMIRKS - Have you entered a valid SMIRKS?</p>
</div>`;
};
}
}"
@close="reset(); smirksVizHtml = ''"
> >
<div class="modal-dialog modal-lg"> <div class="modal-box max-w-3xl">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="font-bold text-lg">Create a new Rule</h3>
<button
type="button" <!-- Close button (X) -->
class="close" <form method="dialog">
data-dismiss="modal" <button
aria-label="Close" class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
> :disabled="isSubmitting"
<span aria-hidden="true">×</span> >
</button>
<h4 class="modal-title">Create a new Rule</h4> </button>
</div> </form>
<div class="modal-body">
<form <!-- Body -->
id="new_rule_modal_form" <div class="py-4">
accept-charset="UTF-8" <form
action="{% url 'package rule list' meta.current_package.uuid %}" id="new-rule-modal-form"
data-remote="true" accept-charset="UTF-8"
method="post" action="{% url 'package rule list' meta.current_package.uuid %}"
> method="post"
{% csrf_token %} >
<label for="rule-name">Name</label> {% csrf_token %}
<div class="form-control mb-3">
<label class="label" for="rule-name">
<span class="label-text">Name</span>
</label>
<input <input
id="rule-name" id="rule-name"
class="form-control" class="input input-bordered w-full"
name="rule-name" name="rule-name"
placeholder="Name" placeholder="Name"
required
/> />
<label for="rule-description">Description</label> </div>
<div class="form-control mb-3">
<label class="label" for="rule-description">
<span class="label-text">Description</span>
</label>
<input <input
id="rule-description" id="rule-description"
class="form-control" class="input input-bordered w-full"
name="rule-description" name="rule-description"
placeholder="Description" placeholder="Description"
/> />
<label for="rule-smirks">SMIRKS</label> </div>
<div class="form-control mb-3">
<label class="label" for="rule-smirks">
<span class="label-text">SMIRKS</span>
</label>
<input <input
id="rule-smirks" id="rule-smirks"
class="form-control" class="input input-bordered w-full"
name="rule-smirks" name="rule-smirks"
placeholder="SMIRKS" placeholder="SMIRKS"
@input="updateSmirksViz()"
/> />
<p></p> </div>
<div id="rule-smirks-viz"></div>
<input <div id="rule-smirks-viz" class="mb-3" x-html="smirksVizHtml"></div>
type="hidden"
name="rule-type" <input
id="rule-type" type="hidden"
value="SimpleAmbitRule" name="rule-type"
/> id="rule-type"
<p></p> value="SimpleAmbitRule"
</form> />
</div> </form>
<div class="modal-footer"> </div>
<button
type="button" <!-- Footer -->
class="btn btn-secondary pull-left" <div class="modal-action">
data-dismiss="modal" <button
> type="button"
Close class="btn"
</button> onclick="this.closest('dialog').close()"
<button :disabled="isSubmitting"
type="button" >
class="btn btn-primary" Close
id="new_rule_modal_form_submit" </button>
> <button
Submit type="button"
</button> class="btn btn-primary"
</div> @click="submit('new-rule-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script>
$(function () {
$("#rule-smirks").on("input", function (e) {
$("#rule-smirks-viz").empty();
smirks = $("#rule-smirks").val(); <!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
const img = new Image(); <button :disabled="isSubmitting">close</button>
img.src = </form>
"{% url 'depict' %}?is_query_smirks=true&smirks=" + </dialog>
encodeURIComponent(smirks);
img.style.width = "100%";
img.style.height = "100%";
img.style.objectFit = "cover";
img.onload = function () {
$("#rule-smirks-viz").append(img);
};
img.onerror = function () {
error_tpl = `
<div class="alert alert-error" role="alert">
<h4 class="alert-heading">Could not render SMIRKS!</h4>
<p>Could not render SMIRKS - Have you entered a valid SMIRKS?</a>
</p>
</div>`;
$("#rule-smirks-viz").append(error_tpl);
};
});
$("#new_rule_modal_form_submit").on("click", function (e) {
e.preventDefault();
$(this).prop("disabled", true);
// submit form
$("#new_rule_modal_form").submit();
});
});
</script>

View File

@ -1,30 +1,45 @@
<div {% load static %}
class="modal fade"
tabindex="-1" <dialog
id="new_scenario_modal" id="new_scenario_modal"
role="dialog" class="modal"
aria-labelledby="new_scenario_modal" x-data="{
aria-hidden="true" ...modalForm(),
scenarioType: 'empty',
validateYear(el) {
if (el.value && el.value.length < 4) {
el.value = new Date().getFullYear();
}
}
}"
@close="reset()"
> >
<div class="modal-dialog modal-lg"> <div class="modal-box max-w-3xl">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="font-bold text-lg">New Scenario</h3>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
<span class="sr-only">Close</span> <form method="dialog">
</button> <button
<h4 class="modal-title">New Scenario</h4> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
</div> :disabled="isSubmitting"
<div class="modal-body"> >
<form
id="new_scenario_form" </button>
accept-charset="UTF-8" </form>
action="{{ meta.current_package.url }}/scenario"
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <form
{% csrf_token %} id="new-scenario-modal-form"
<div class="jumbotron"> accept-charset="UTF-8"
action="{{ meta.current_package.url }}/scenario"
method="post"
>
{% csrf_token %}
<div class="alert alert-info mb-4">
<span>
Please enter name, description, and date of scenario. Date should be Please enter name, description, and date of scenario. Date should be
associated to the data, not the current date. For example, this associated to the data, not the current date. For example, this
could reflect the publishing date of a study. You can leave all could reflect the publishing date of a study. You can leave all
@ -32,122 +47,131 @@
<a <a
target="_blank" target="_blank"
href="https://wiki.envipath.org/index.php/scenario" href="https://wiki.envipath.org/index.php/scenario"
role="button" class="link"
>wiki &gt;&gt;</a >wiki &gt;&gt;</a
> >
</div> </span>
<label for="scenario-name">Name</label> </div>
<div class="form-control mb-3">
<label class="label" for="scenario-name">
<span class="label-text">Name</span>
</label>
<input <input
id="scenario-name" id="scenario-name"
name="scenario-name" name="scenario-name"
class="form-control" class="input input-bordered w-full"
placeholder="Name" placeholder="Name"
required
/> />
<label for="scenario-description">Description</label> </div>
<div class="form-control mb-3">
<label class="label" for="scenario-description">
<span class="label-text">Description</span>
</label>
<input <input
id="scenario-description" id="scenario-description"
name="scenario-description" name="scenario-description"
class="form-control" class="input input-bordered w-full"
placeholder="Description" placeholder="Description"
/> />
<label id="dateField" for="dateYear">Date</label> </div>
<table>
<tr> <div class="form-control mb-3">
<th> <label class="label">
<input <span class="label-text">Date</span>
type="number" </label>
id="dateYear" <div class="flex gap-2">
name="scenario-date-year" <input
class="form-control" type="number"
placeholder="YYYY" id="dateYear"
max="{% now "Y" %}" name="scenario-date-year"
/> class="input input-bordered w-24"
</th> placeholder="YYYY"
<th> max="{% now 'Y' %}"
<input @blur="validateYear($el)"
type="number" />
id="dateMonth" <input
name="scenario-date-month" type="number"
min="1" id="dateMonth"
max="12" name="scenario-date-month"
class="form-control" min="1"
placeholder="MM" max="12"
/> class="input input-bordered w-20"
</th> placeholder="MM"
<th> />
<input <input
type="number" type="number"
id="dateDay" id="dateDay"
name="scenario-date-day" name="scenario-date-day"
min="1" min="1"
max="31" max="31"
class="form-control" class="input input-bordered w-20"
placeholder="DD" placeholder="DD"
/> />
</th> </div>
</tr> </div>
</table>
<label for="scenario-type">Scenario Type</label> <div class="form-control mb-3">
<label class="label" for="scenario-type">
<span class="label-text">Scenario Type</span>
</label>
<select <select
id="scenario-type" id="scenario-type"
name="scenario-type" name="scenario-type"
class="form-control" class="select select-bordered w-full"
data-width="100%" x-model="scenarioType"
> >
<option value="empty" selected>Empty Scenario</option> <option value="empty" selected>Empty Scenario</option>
{% for k, v in scenario_types.items %} {% for k, v in scenario_types.items %}
<option value="{{ v.name }}">{{ k }}</option> <option value="{{ v.name }}">{{ k }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div>
{% for type in scenario_types.values %} {% for type in scenario_types.values %}
<div id="{{ type.name }}-specific-inputs"> <div
{% for widget in type.widgets %} id="{{ type.name }}-specific-inputs"
{{ widget|safe }} x-show="scenarioType === '{{ type.name }}'"
{% endfor %} x-cloak
</div> >
{% endfor %} {% for widget in type.widgets %}
</form> {{ widget|safe }}
</div> {% endfor %}
<div class="modal-footer"> </div>
<a id="new_scenario_modal_form_submit" class="btn btn-primary" href="#" {% endfor %}
>Submit</a </form>
> </div>
<button type="button" class="btn btn-default" data-dismiss="modal">
Cancel <!-- Footer -->
</button> <div class="modal-action">
</div> <button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('new-scenario-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
// Initially hide all "specific" forms <button :disabled="isSubmitting">close</button>
$("div[id$='-specific-inputs']").each(function () { </form>
$(this).hide(); </dialog>
});
// On change hide all and show only selected
$("#scenario-type").change(function () {
$("div[id$='-specific-inputs']").each(function () {
$(this).hide();
});
val = $("option:selected", this).val();
$("#" + val + "-specific-inputs").show();
});
$("#new_scenario_modal_form_submit").on("click", function (e) {
e.preventDefault();
$("#new_scenario_form").submit();
});
var dateYear = document.getElementById("dateYear");
dateYear.addEventListener("change", () => {
console.log("Final value after editing:", dateYear.value);
if (dateYear.value.length < 4) {
dateYear.value = new Date().getFullYear();
}
});
});
</script>

View File

@ -1,92 +1,117 @@
{% load static %} {% load static %}
<!-- Add Additional Information--> <!-- Add Additional Information -->
<div id="add_additional_information_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="add_additional_information_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="{
<button isSubmitting: false,
type="button" selectedType: '',
class="close"
data-dismiss="modal" reset() {
aria-label="Close" this.isSubmitting = false;
> this.selectedType = '';
<span aria-hidden="true">&times;</span> },
</button>
<h3 class="modal-title">Add Additional Information</h3> submit() {
</div> if (!this.selectedType) return;
<div class="modal-body">
const form = document.getElementById('add_' + this.selectedType + '_add-additional-information-modal-form');
if (form && form.checkValidity()) {
this.isSubmitting = true;
form.submit();
} else if (form) {
form.reportValidity();
}
}
}"
@close="reset()"
>
<div class="modal-box">
<!-- Header -->
<h3 class="text-lg font-bold">Add Additional Information</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<div class="form-control">
<label class="label" for="select-additional-information-type">
<span class="label-text">Select the type to add</span>
</label>
<select <select
id="select-additional-information-type" id="select-additional-information-type"
data-actions-box="true" class="select select-bordered w-full"
class="form-control" x-model="selectedType"
data-width="100%"
> >
<option selected disabled>Select the type to add</option> <option value="" selected disabled>Select the type to add</option>
{% for add_inf in available_additional_information %} {% for add_inf in available_additional_information %}
<option value="{{ add_inf.name }}"> <option value="{{ add_inf.name }}">
{{ add_inf.display_name }} {{ add_inf.display_name }}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
{% for add_inf in available_additional_information %}
<div class="aiform {{ add_inf.name }}" style="display: none;">
<form
id="add_{{ add_inf.name }}_add-additional-information-modal-form"
accept-charset="UTF-8"
action=""
data-remote="true"
method="post"
>
{% csrf_token %}
{{ add_inf.widget|safe }}
<input
type="hidden"
name="hidden"
value="add-additional-information"
/>
</form>
</div>
{% endfor %}
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"> {% for add_inf in available_additional_information %}
Close <div
</button> class="mt-4"
<button x-show="selectedType === '{{ add_inf.name }}'"
type="button" x-cloak
class="btn btn-primary"
id="add-additional-information-modal-submit"
> >
Add <form
</button> id="add_{{ add_inf.name }}_add-additional-information-modal-form"
</div> accept-charset="UTF-8"
action=""
method="post"
>
{% csrf_token %}
{{ add_inf.widget|safe }}
<input
type="hidden"
name="hidden"
value="add-additional-information"
/>
</form>
</div>
{% endfor %}
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit()"
:disabled="isSubmitting || !selectedType"
>
<span x-show="!isSubmitting">Add</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Adding...</span>
</button>
</div> </div>
</div> </div>
</div>
<script>
$(function () {
$("#select-additional-information-type").change(function (e) {
var selectedType = $(
"#select-additional-information-type :selected",
).val();
$(".aiform").hide();
$("." + selectedType).show();
});
$("#add-additional-information-modal-submit").click(function (e) { <!-- Backdrop -->
e.preventDefault(); <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
var selectedType = $( </form>
"#select-additional-information-type :selected", </dialog>
).val();
console.log(selectedType);
if (
selectedType !== null &&
selectedType !== undefined &&
selectedType !== ""
) {
$("." + selectedType + " >form").submit();
}
});
});
</script>

View File

@ -1,67 +1,107 @@
{% load static %} {% load static %}
<div <dialog
class="modal fade bs-modal-lg"
id="add_pathway_edge_modal" id="add_pathway_edge_modal"
tabindex="-1" class="modal"
aria-labelledby="add_pathway_edge_modal" x-data="{
aria-modal="true" isSubmitting: false,
role="dialog" reactionImageUrl: '',
reset() {
this.isSubmitting = false;
this.reactionImageUrl = '';
},
updateReactionImage() {
const substratesSelect = document.getElementById('add_pathway_edge_substrates');
const productsSelect = document.getElementById('add_pathway_edge_products');
const substrates = [];
for (const option of substratesSelect.selectedOptions) {
substrates.push(option.dataset.smiles);
}
const products = [];
for (const option of productsSelect.selectedOptions) {
products.push(option.dataset.smiles);
}
if (substrates.length > 0 && products.length > 0) {
const reaction = substrates.join('.') + '>>' + products.join('.');
this.reactionImageUrl = '{% url "depict" %}?smirks=' + encodeURIComponent(reaction);
} else {
this.reactionImageUrl = '';
}
},
submit() {
this.isSubmitting = true;
document.getElementById('add_pathway_edge_modal_form').submit();
}
}"
@close="reset()"
> >
<div class="modal-dialog modal-lg"> <div class="modal-box max-w-4xl">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="text-lg font-bold">Add a Reaction</h3>
<button
type="button" <!-- Close button (X) -->
class="close" <form method="dialog">
data-dismiss="modal" <button
aria-label="Close" class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
> :disabled="isSubmitting"
<span aria-hidden="true">&times;</span> >
</button>
<h4 class="modal-title">Add a Reaction</h4> </button>
</div> </form>
<div class="modal-body">
<form <!-- Body -->
id="add_pathway_edge_modal_form" <div class="py-4">
accept-charset="UTF-8" <form
action="{% url 'package pathway edge list' meta.current_package.uuid pathway.uuid %}" id="add_pathway_edge_modal_form"
data-remote="true" accept-charset="UTF-8"
method="post" action="{% url 'package pathway edge list' meta.current_package.uuid pathway.uuid %}"
> data-remote="true"
{% csrf_token %} method="post"
<label for="edge-name">Name</label> >
{% csrf_token %}
<div class="form-control mb-3">
<label class="label" for="edge-name">
<span class="label-text">Name</span>
</label>
<input <input
id="edge-name" id="edge-name"
class="form-control" type="text"
class="input input-bordered w-full"
name="edge-name" name="edge-name"
placeholder="Name" placeholder="Name"
/> />
<label for="edge-description">Description</label> </div>
<div class="form-control mb-3">
<label class="label" for="edge-description">
<span class="label-text">Description</span>
</label>
<input <input
id="edge-description" id="edge-description"
class="form-control" type="text"
class="input input-bordered w-full"
name="edge-description" name="edge-description"
placeholder="Description" placeholder="Description"
/> />
<p></p> </div>
<div class="row">
<div class="col-xs-5"> <div class="mb-3 grid grid-cols-11 gap-2">
<legend>Substrate(s)</legend> <div class="col-span-5">
</div> <div class="form-control">
<div class="col-xs-2"></div> <label class="label">
<div class="col-xs-5"> <span class="label-text font-semibold">Substrate(s)</span>
<legend>Product(s)</legend> </label>
</div>
</div>
<div class="row">
<div class="col-xs-5">
<select <select
id="add_pathway_edge_substrates" id="add_pathway_edge_substrates"
name="edge-substrates" name="edge-substrates"
data-actions-box="true" class="select select-bordered h-32 w-full"
class="form-control"
multiple multiple
data-width="100%" @change="updateReactionImage()"
> >
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option <option
@ -73,20 +113,21 @@
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
<div </div>
class="col-xs-2" <div class="col-span-1 flex items-center justify-center">
style="display: flex; justify-content: center; align-items: center;" <span class="text-2xl"></span>
> </div>
<i class="glyphicon glyphicon-arrow-right"></i> <div class="col-span-5">
</div> <div class="form-control">
<div class="col-xs-5"> <label class="label">
<span class="label-text font-semibold">Product(s)</span>
</label>
<select <select
id="add_pathway_edge_products" id="add_pathway_edge_products"
name="edge-products" name="edge-products"
data-actions-box="true" class="select select-bordered h-32 w-full"
class="form-control"
multiple multiple
data-width="100%" @change="updateReactionImage()"
> >
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option <option
@ -99,76 +140,42 @@
</select> </select>
</div> </div>
</div> </div>
<div class="row"> </div>
<p></p>
<div class="col-xs-12" id="reaction_image"></div> <div class="mb-3" x-show="reactionImageUrl" x-cloak>
</div> <img :src="reactionImageUrl" class="w-full" alt="Reaction preview" />
</form> </div>
</div> </form>
<div class="modal-footer"> </div>
<button
type="button" <!-- Footer -->
class="btn btn-secondary pull-left" <div class="modal-action">
data-dismiss="modal" <button
> type="button"
Close class="btn"
</button> onclick="this.closest('dialog').close()"
<button :disabled="isSubmitting"
type="button" >
class="btn btn-primary" Close
id="add_pathway_edge_modal_form_submit" </button>
> <button
Submit type="button"
</button> class="btn btn-primary"
</div> @click="submit()"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Submitting...</span>
</button>
</div> </div>
</div> </div>
</div>
<script>
function reactionImage() {
var substrates = [];
$("#add_pathway_edge_substrates option:selected").each(function () {
var smiles = $(this).data("smiles"); // read data-smiles attribute
substrates.push(smiles);
});
var products = []; <!-- Backdrop -->
$("#add_pathway_edge_products option:selected").each(function () { <form method="dialog" class="modal-backdrop">
var smiles = $(this).data("smiles"); // read data-smiles attribute <button :disabled="isSubmitting">close</button>
products.push(smiles); </form>
}); </dialog>
if (substrates.length > 0 && products.length > 0) {
reaction = substrates.join(".") + ">>" + products.join(".");
$("#reaction_image").empty();
$("#reaction_image").append(
"<img width='100%' src='{% url 'depict' %}?smirks=" +
encodeURIComponent(reaction) +
"'>",
);
}
}
$(function () {
$("#add_pathway_edge_substrates").selectpicker();
$("#add_pathway_edge_products").selectpicker();
$("#add_pathway_edge_substrates").on("change", function (e) {
reactionImage();
});
$("#add_pathway_edge_products").on("change", function (e) {
reactionImage();
});
$(function () {
$("#add_pathway_edge_modal_form_submit").on("click", function (e) {
e.preventDefault();
$(this).prop("disabled", true);
// submit form
$("#add_pathway_edge_modal_form").submit();
});
});
});
</script>

View File

@ -1,119 +1,137 @@
{% load static %} {% load static %}
<div <dialog
class="modal fade bs-modal-lg"
id="add_pathway_node_modal" id="add_pathway_node_modal"
tabindex="-1" class="modal"
aria-labelledby="add_pathway_node_modal" x-data="modalForm()"
aria-modal="true" @close="reset()"
role="dialog"
> >
<div class="modal-dialog modal-lg"> <div class="modal-box max-w-4xl">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="text-lg font-bold">Add a Node</h3>
<button
type="button" <!-- Close button (X) -->
class="close" <form method="dialog">
data-dismiss="modal" <button
aria-label="Close" class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
> :disabled="isSubmitting"
<span aria-hidden="true">&times;</span> >
</button>
<h4 class="modal-title">Add a Node</h4> </button>
</div> </form>
<div class="modal-body">
<form <!-- Body -->
id="add_pathway_node_modal_form" <div class="py-4">
accept-charset="UTF-8" <form
action="{% url 'package pathway node list' meta.current_package.uuid pathway.uuid %}" id="add_pathway_node_modal_form"
data-remote="true" accept-charset="UTF-8"
method="post" action="{% url 'package pathway node list' meta.current_package.uuid pathway.uuid %}"
> data-remote="true"
{% csrf_token %} method="post"
<label for="node-name">Name</label> >
{% csrf_token %}
<div class="form-control mb-3">
<label class="label" for="node-name">
<span class="label-text">Name</span>
</label>
<input <input
id="node-name" id="node-name"
class="form-control" type="text"
class="input input-bordered w-full"
name="node-name" name="node-name"
placeholder="Name" placeholder="Name"
/> />
<label for="node-description">Description</label> </div>
<div class="form-control mb-3">
<label class="label" for="node-description">
<span class="label-text">Description</span>
</label>
<input <input
id="node-description" id="node-description"
class="form-control" type="text"
class="input input-bordered w-full"
name="node-description" name="node-description"
placeholder="Description" placeholder="Description"
/> />
<label for="node-smiles">SMILES</label> </div>
<div class="form-control mb-3">
<label class="label" for="node-smiles">
<span class="label-text">SMILES</span>
</label>
<input <input
type="text" type="text"
class="form-control" class="input input-bordered w-full"
name="node-smiles" name="node-smiles"
placeholder="SMILES" placeholder="SMILES"
id="node-smiles" id="node-smiles"
/> />
<p></p> </div>
<div>
<iframe <div class="mb-3">
id="add_node_ketcher" <iframe
src="{% static '/js/ketcher2/ketcher.html' %}" id="add_node_ketcher"
width="100%" src="{% static '/js/ketcher2/ketcher.html' %}"
height="510" width="100%"
></iframe> height="510"
</div> ></iframe>
<p></p> </div>
</form> </form>
</div> </div>
<div class="modal-footer">
<button <!-- Footer -->
type="button" <div class="modal-action">
class="btn btn-secondary pull-left" <button
data-dismiss="modal" type="button"
> class="btn"
Close onclick="this.closest('dialog').close()"
</button> :disabled="isSubmitting"
<button >
type="button" Close
class="btn btn-primary" </button>
id="add_pathway_node_modal_form_submit" <button
> type="button"
Submit class="btn btn-primary"
</button> @click="submit('add_pathway_node_modal_form')"
</div> :disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Submitting...</span>
</button>
</div> </div>
</div> </div>
</div>
<script>
function newStructureModalketcherToNewStructureModalTextInput() {
$("#node-smiles").val(this.ketcher.getSmiles());
}
$(function () { <!-- Backdrop -->
$("#add_node_ketcher").on("load", function () { <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>
<script>
document
.getElementById("add_node_ketcher")
.addEventListener("load", function () {
const iframe = this;
const checkKetcherReady = () => { const checkKetcherReady = () => {
win = this.contentWindow; const win = iframe.contentWindow;
if (win.ketcher && "editor" in win.ketcher) { if (win.ketcher && "editor" in win.ketcher) {
win.ketcher.editor.event.change.handlers.push({ win.ketcher.editor.event.change.handlers.push({
once: false, once: false,
priority: 0, priority: 0,
f: newStructureModalketcherToNewStructureModalTextInput, f: function () {
document.getElementById("node-smiles").value =
this.ketcher.getSmiles();
},
ketcher: win.ketcher, ketcher: win.ketcher,
}); });
} else { } else {
setTimeout(checkKetcherReady, 100); setTimeout(checkKetcherReady, 100);
} }
}; };
checkKetcherReady(); checkKetcherReady();
}); });
$(function () {
$("#add_pathway_node_modal_form_submit").on("click", function (e) {
e.preventDefault();
$(this).prop("disabled", true);
// submit form
$("#add_pathway_node_modal_form").submit();
});
});
});
</script> </script>

View File

@ -1,119 +1,137 @@
{% load static %} {% load static %}
<div <dialog
class="modal fade bs-modal-lg"
id="add_structure_modal" id="add_structure_modal"
tabindex="-1" class="modal"
aria-labelledby="add_structure_modal" x-data="modalForm()"
aria-modal="true" @close="reset()"
role="dialog"
> >
<div class="modal-dialog modal-lg"> <div class="modal-box max-w-4xl">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="text-lg font-bold">Create a new Structure</h3>
<button
type="button" <!-- Close button (X) -->
class="close" <form method="dialog">
data-dismiss="modal" <button
aria-label="Close" class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
> :disabled="isSubmitting"
<span aria-hidden="true">×</span> >
</button>
<h4 class="modal-title">Create a new Structure</h4> </button>
</div> </form>
<div class="modal-body">
<form <!-- Body -->
id="add_structure_modal_form" <div class="py-4">
accept-charset="UTF-8" <form
action="{% url 'package compound structure list' meta.current_package.uuid compound.uuid %}" id="add_structure_modal_form"
data-remote="true" accept-charset="UTF-8"
method="post" action="{% url 'package compound structure list' meta.current_package.uuid compound.uuid %}"
> data-remote="true"
{% csrf_token %} method="post"
<label for="structure-name">Name</label> >
{% csrf_token %}
<div class="form-control mb-3">
<label class="label" for="structure-name">
<span class="label-text">Name</span>
</label>
<input <input
id="structure-name" id="structure-name"
class="form-control" type="text"
class="input input-bordered w-full"
name="structure-name" name="structure-name"
placeholder="Name" placeholder="Name"
/> />
<label for="structure-description">Description</label> </div>
<div class="form-control mb-3">
<label class="label" for="structure-description">
<span class="label-text">Description</span>
</label>
<input <input
id="structure-description" id="structure-description"
class="form-control" type="text"
class="input input-bordered w-full"
name="structure-description" name="structure-description"
placeholder="Description" placeholder="Description"
/> />
<label for="structure-smiles">SMILES</label> </div>
<div class="form-control mb-3">
<label class="label" for="structure-smiles">
<span class="label-text">SMILES</span>
</label>
<input <input
type="text" type="text"
class="form-control" class="input input-bordered w-full"
name="structure-smiles" name="structure-smiles"
placeholder="SMILES" placeholder="SMILES"
id="structure-smiles" id="structure-smiles"
/> />
<p></p> </div>
<div>
<iframe <div class="mb-3">
id="add_structure_ketcher" <iframe
src="{% static '/js/ketcher2/ketcher.html' %}" id="add_structure_ketcher"
width="100%" src="{% static '/js/ketcher2/ketcher.html' %}"
height="510" width="100%"
></iframe> height="510"
</div> ></iframe>
<p></p> </div>
</form> </form>
</div> </div>
<div class="modal-footer">
<button <!-- Footer -->
type="button" <div class="modal-action">
class="btn btn-secondary pull-left" <button
data-dismiss="modal" type="button"
> class="btn"
Close onclick="this.closest('dialog').close()"
</button> :disabled="isSubmitting"
<button >
type="button" Close
class="btn btn-primary" </button>
id="add_structure_modal_form_submit" <button
> type="button"
Submit class="btn btn-primary"
</button> @click="submit('add_structure_modal_form')"
</div> :disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Submitting...</span>
</button>
</div> </div>
</div> </div>
</div>
<script>
function newStructureModalketcherToNewStructureModalTextInput() {
$("#structure-smiles").val(this.ketcher.getSmiles());
}
$(function () { <!-- Backdrop -->
$("#add_structure_ketcher").on("load", function () { <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>
<script>
document
.getElementById("add_structure_ketcher")
.addEventListener("load", function () {
const iframe = this;
const checkKetcherReady = () => { const checkKetcherReady = () => {
win = this.contentWindow; const win = iframe.contentWindow;
if (win.ketcher && "editor" in win.ketcher) { if (win.ketcher && "editor" in win.ketcher) {
win.ketcher.editor.event.change.handlers.push({ win.ketcher.editor.event.change.handlers.push({
once: false, once: false,
priority: 0, priority: 0,
f: newStructureModalketcherToNewStructureModalTextInput, f: function () {
document.getElementById("structure-smiles").value =
this.ketcher.getSmiles();
},
ketcher: win.ketcher, ketcher: win.ketcher,
}); });
} else { } else {
setTimeout(checkKetcherReady, 100); setTimeout(checkKetcherReady, 100);
} }
}; };
checkKetcherReady(); checkKetcherReady();
}); });
$(function () {
$("#add_structure_modal_form_submit").on("click", function (e) {
e.preventDefault();
$(this).prop("disabled", true);
// submit form
$("#add_structure_modal_form").submit();
});
});
});
</script> </script>

View File

@ -1,36 +1,48 @@
{% load static %} {% load static %}
<!-- Delete Edge --> <!-- Delete Edge -->
<div id="delete_pathway_edge_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="delete_pathway_edge_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
<h3 class="modal-title">Delete Edge</h3> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="text-lg font-bold">Delete Edge</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
<div class="modal-body"> :disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<p class="mb-4">
Deletes the Edge. Nodes referenced by this edge will remain. Deletes the Edge. Nodes referenced by this edge will remain.
<p></p> </p>
<form <form
id="delete-pathway-edge-modal-form" id="delete-pathway-edge-modal-form"
accept-charset="UTF-8" accept-charset="UTF-8"
action="" action=""
data-remote="true" method="post"
method="post" >
> {% csrf_token %}
{% csrf_token %} <div class="form-control">
<label class="label" for="delete_pathway_edge_edges">
<span class="label-text">Select Reaction to delete</span>
</label>
<select <select
id="delete_pathway_edge_edges" id="delete_pathway_edge_edges"
name="edge-url" name="edge-url"
data-actions-box="true" class="select select-bordered w-full"
class="form-control" x-model="selectedEdge"
data-width="100%" @change="imageUrl = selectedEdge ? selectedEdge + '?image=svg' : ''"
required
> >
<option value="" disabled selected> <option value="" disabled selected>
Select Reaction to delete Select Reaction to delete
@ -39,51 +51,44 @@
<option value="{{ e.url }}">{{ e.edge_label.name|safe }}</option> <option value="{{ e.url }}">{{ e.edge_label.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" id="hidden" name="hidden" value="delete" /> </div>
</form> <input type="hidden" id="hidden" name="hidden" value="delete" />
<p></p> </form>
<div id="delete_pathway_edge_image"></div>
</div> <!-- Image Preview -->
<div class="modal-footer"> <div class="mt-4" x-show="imageUrl" x-cloak>
<button type="button" class="btn btn-secondary" data-dismiss="modal"> <img :src="imageUrl" class="w-full" alt="Edge preview" />
Close
</button>
<button
type="button"
class="btn btn-primary"
id="delete-pathway-edge-modal-submit"
>
Delete
</button>
</div> </div>
</div> </div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-error"
@click="setFormAction('delete-pathway-edge-modal-form', selectedEdge); submit('delete-pathway-edge-modal-form')"
:disabled="isSubmitting || !selectedEdge"
>
<span x-show="!isSubmitting">Delete</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Deleting...</span>
</button>
</div>
</div> </div>
</div>
<script>
$(function () {
$("#delete_pathway_edge_edges").selectpicker();
$("#delete_pathway_edge_edges").on("change", function (e) { <!-- Backdrop -->
edge_url = $("#delete_pathway_edge_edges option:selected").val(); <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
if (edge_url !== "") { </form>
$("#delete_pathway_edge_image").empty(); </dialog>
$("#delete_pathway_edge_image").append(
"<img width='100%' src='" + edge_url + "?image=svg'>",
);
}
});
$("#delete-pathway-edge-modal-submit").click(function (e) {
e.preventDefault();
edge_url = $("#delete_pathway_edge_edges option:selected").val();
if (edge_url === "") {
return;
}
$("#delete-pathway-edge-modal-form").attr("action", edge_url);
$("#delete-pathway-edge-modal-form").submit();
});
});
</script>

View File

@ -1,38 +1,49 @@
{% load static %} {% load static %}
<!-- Delete Node --> <!-- Delete Node -->
<div id="delete_pathway_node_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="delete_pathway_node_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
<h3 class="modal-title">Delete Node</h3> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="text-lg font-bold">Delete Node</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
<div class="modal-body"> :disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<p class="mb-4">
Deletes the Node. Edges having this Node as Substrate or Product will be Deletes the Node. Edges having this Node as Substrate or Product will be
removed as well. removed as well.
<p></p> </p>
<form <form
id="delete-pathway-node-modal-form" id="delete-pathway-node-modal-form"
accept-charset="UTF-8" accept-charset="UTF-8"
action="" action=""
data-remote="true" method="post"
method="post" >
> {% csrf_token %}
{% csrf_token %} <div class="form-control">
<label class="label" for="delete_pathway_node_nodes">
<span class="label-text">Select Compound to delete</span>
</label>
<select <select
id="delete_pathway_node_nodes" id="delete_pathway_node_nodes"
name="node-url" name="node-url"
data-actions-box="true" class="select select-bordered w-full"
class="form-control" x-model="selectedNode"
data-width="100%" @change="imageUrl = selectedNode ? selectedNode + '?image=svg' : ''"
required
> >
<option value="" disabled selected> <option value="" disabled selected>
Select Compound to delete Select Compound to delete
@ -43,51 +54,44 @@
</option> </option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" id="hidden" name="hidden" value="delete" /> </div>
</form> <input type="hidden" id="hidden" name="hidden" value="delete" />
<p></p> </form>
<div id="delete_pathway_node_image"></div>
</div> <!-- Image Preview -->
<div class="modal-footer"> <div class="mt-4" x-show="imageUrl" x-cloak>
<button type="button" class="btn btn-secondary" data-dismiss="modal"> <img :src="imageUrl" class="w-full" alt="Node preview" />
Close
</button>
<button
type="button"
class="btn btn-primary"
id="delete-pathway-node-modal-submit"
>
Delete
</button>
</div> </div>
</div> </div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-error"
@click="setFormAction('delete-pathway-node-modal-form', selectedNode); submit('delete-pathway-node-modal-form')"
:disabled="isSubmitting || !selectedNode"
>
<span x-show="!isSubmitting">Delete</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Deleting...</span>
</button>
</div>
</div> </div>
</div>
<script>
$(function () {
$("#delete_pathway_node_nodes").selectpicker();
$("#delete_pathway_node_nodes").on("change", function (e) { <!-- Backdrop -->
node_url = $("#delete_pathway_node_nodes option:selected").val(); <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
if (node_url !== "") { </form>
$("#delete_pathway_node_image").empty(); </dialog>
$("#delete_pathway_node_image").append(
"<img width='100%' src='" + node_url + "?image=svg'>",
);
}
});
$("#delete-pathway-node-modal-submit").click(function (e) {
e.preventDefault();
node_url = $("#delete_pathway_node_nodes option:selected").val();
if (node_url === "") {
return;
}
$("#delete-pathway-node-modal-form").attr("action", node_url);
$("#delete-pathway-node-modal-form").submit();
});
});
</script>

View File

@ -1,53 +1,69 @@
{% load static %} {% load static %}
<!-- Download Pathway -->
<div id="download_pathway_csv_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="download_pathway_csv_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h3 class="modal-title">Download Pathway as CSV</h3> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Download Pathway as CSV</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<p>
By clicking on Download the Pathway will be converted into a CSV and By clicking on Download the Pathway will be converted into a CSV and
directly downloaded. directly downloaded.
<form </p>
id="download-pathway-csv-modal-form"
accept-charset="UTF-8" <form
action="{{ pathway.url }}" id="download-pathway-csv-modal-form"
data-remote="true" accept-charset="UTF-8"
method="GET" action="{{ pathway.url }}"
> method="GET"
<input type="hidden" name="download" value="true" /> >
</form> <input type="hidden" name="download" value="true" />
</div> </form>
<div class="modal-footer"> </div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
Close <!-- Footer -->
</button> <div class="modal-action">
<button <button
type="button" type="button"
class="btn btn-primary" class="btn"
id="download-pathway-csv-modal-submit" onclick="this.closest('dialog').close()"
> :disabled="isSubmitting"
Download >
</button> Close
</div> </button>
<button
type="button"
class="btn btn-primary"
@click="submit('download-pathway-csv-modal-form'); $el.closest('dialog').close();"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Download</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#download-pathway-csv-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#download-pathway-csv-modal-form").submit(); </dialog>
$("#download_pathway_csv_modal").modal("hide");
});
});
</script>

View File

@ -1,43 +1,57 @@
{% load static %} {% load static %}
<!-- Download Pathway -->
<div id="download_pathway_image_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="download_pathway_image_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h3 class="modal-title">Download Pathway as Image</h3> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Download Pathway as Image</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
By clicking on Download the Pathway will be saved as SVG. >
</div>
<div class="modal-footer"> </button>
<button type="button" class="btn btn-secondary" data-dismiss="modal"> </form>
Close
</button> <!-- Body -->
<button <div class="py-4">
type="button" <p>By clicking on Download the Pathway will be saved as SVG.</p>
class="btn btn-primary" </div>
id="download-pathway-image-modal-submit"
> <!-- Footer -->
Download <div class="modal-action">
</button> <button
</div> type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="isSubmitting = true; downloadSVG(document.getElementById('pwsvg'), '{{ pathway.name.split|join:'_' }}.svg'); $el.closest('dialog').close();"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Download</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#download-pathway-image-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
downloadSVG($("#pwsvg")[0], '{{ pathway.name.split|join:"_" }}.svg'); </dialog>
$("#download_pathway_image_modal").modal("hide");
});
});
</script>

View File

@ -1,70 +1,91 @@
{% load static %} {% load static %}
<!-- Edit Compound -->
<div id="edit_compound_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_compound_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h5 class="modal-title">Edit Compound</h5> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Edit Compound</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<p>Edit Compound.</p> >
<form
id="edit-compound-modal-form" </button>
accept-charset="UTF-8" </form>
action=""
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <form
{% csrf_token %} id="edit-compound-modal-form"
<p> accept-charset="UTF-8"
<label for="compound-name">Name</label> action=""
<input method="post"
id="compound-name" >
class="form-control" {% csrf_token %}
name="compound-name"
value="{{ compound.name|safe }}" <div class="form-control mb-3">
/> <label class="label" for="compound-name">
</p> <span class="label-text">Name</span>
<p> </label>
<label for="compound-description">Description</label> <input
<input id="compound-name"
id="compound-description" class="input input-bordered w-full"
type="text" name="compound-name"
class="form-control" value="{{ compound.name|safe }}"
value="{{ compound.description|safe }}" required
name="compound-description" />
/> </div>
</p>
</form> <div class="form-control mb-3">
</div> <label class="label" for="compound-description">
<div class="modal-footer"> <span class="label-text">Description</span>
<button type="button" class="btn btn-secondary" data-dismiss="modal"> </label>
Close <input
</button> id="compound-description"
<button type="text"
type="button" class="input input-bordered w-full"
class="btn btn-primary" value="{{ compound.description|safe }}"
id="edit-compound-modal-submit" name="compound-description"
> />
Update </div>
</button> </form>
</div> </div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('edit-compound-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-compound-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-compound-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,70 +1,91 @@
{% load static %} {% load static %}
<!-- Edit Compound -->
<div id="edit_compound_structure_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_compound_structure_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h5 class="modal-title">Create a Compound</h5> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Edit Compound Structure</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<p>Edit a Compound Structure.</p> >
<form
id="edit-compound-structure-modal-form" </button>
accept-charset="UTF-8" </form>
action=""
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <form
{% csrf_token %} id="edit-compound-structure-modal-form"
<p> accept-charset="UTF-8"
<label for="compound-structure-name">Name</label> action=""
<input method="post"
id="compound-structure-name" >
class="form-control" {% csrf_token %}
name="compound-structure-name"
value="{{ compound_structure.name|safe }}" <div class="form-control mb-3">
/> <label class="label" for="compound-structure-name">
</p> <span class="label-text">Name</span>
<p> </label>
<label for="compound-structure-description">Description</label> <input
<input id="compound-structure-name"
id="compound-structure-description" class="input input-bordered w-full"
type="text" name="compound-structure-name"
class="form-control" value="{{ compound_structure.name|safe }}"
value="{{ compound_structure.description|safe }}" required
name="compound-structure-description" />
/> </div>
</p>
</form> <div class="form-control mb-3">
</div> <label class="label" for="compound-structure-description">
<div class="modal-footer"> <span class="label-text">Description</span>
<button type="button" class="btn btn-secondary" data-dismiss="modal"> </label>
Close <input
</button> id="compound-structure-description"
<button type="text"
type="button" class="input input-bordered w-full"
class="btn btn-primary" value="{{ compound_structure.description|safe }}"
id="edit-compound-structure-modal-submit" name="compound-structure-description"
> />
Create </div>
</button> </form>
</div> </div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('edit-compound-structure-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-compound-structure-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-compound-structure-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,151 +1,150 @@
{% load static %} {% load static %}
<!-- Edit Package Permission --> <!-- Edit Group Member -->
<div id="edit_group_member_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_group_member_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="{
<h5 class="modal-title">Add or Remove Group Member</h5> isSubmitting: false,
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
To add member (either User or entire Groups) to this group select the
entity you want to add below and click the check mark.
<br />
To remove member simply click the <code>X</code> next to the member.
</p>
<div class="row"> reset() {
<div class="col-xs-8"> this.isSubmitting = false;
<legend>User or Group</legend> },
</div>
<div class="col-xs-4">
<legend>Add/Remove</legend>
</div>
</div>
<div class="row"> submitForm(form) {
<form if (form && form.checkValidity()) {
id="modal-form-group-member" form.submit();
class="form-inline" } else if (form) {
role="form" form.reportValidity();
accept-charset="UTF-8" }
action="" }
data-remote="true" }"
method="post" @close="reset()"
> >
{% csrf_token %} <div class="modal-box">
<div class="col-xs-8"> <!-- Header -->
<select <h3 class="text-lg font-bold">Add or Remove Group Member</h3>
id="select_member"
name="member" <!-- Close button (X) -->
data-actions-box="true" <form method="dialog">
class="selPackages" <button
data-width="100%" class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
> :disabled="isSubmitting"
<option disabled selected>User</option> >
</button>
</form>
<!-- Body -->
<div class="py-4">
<p class="mb-4">
To add member (either User or entire Groups) to this group select the
entity you want to add below and click the check mark.
<br />
To remove member simply click the X button next to the member.
</p>
<!-- Add Member Form -->
<form
id="modal-form-group-member"
accept-charset="UTF-8"
action=""
method="post"
class="mb-4"
>
{% csrf_token %}
<div class="flex gap-2 items-end">
<div class="form-control flex-1">
<label class="label">
<span class="label-text">User or Group</span>
</label>
<select
id="select_member"
name="member"
class="select select-bordered w-full"
required
>
<optgroup label="Users">
{% for u in users %} {% for u in users %}
<option value="{{ u.url }}">{{ u.username }}</option> <option value="{{ u.url }}">{{ u.username }}</option>
{% endfor %} {% endfor %}
<option disabled>Groups</option> </optgroup>
<optgroup label="Groups">
{% for g in groups %} {% for g in groups %}
<option value="{{ g.url }}">{{ g.name|safe }}</option> <option value="{{ g.url }}">{{ g.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </optgroup>
<input type="hidden" name="action" value="add" /> </select>
</div> <input type="hidden" name="action" value="add" />
<div class="col-xs-2"></div> </div>
<div class="col-xs-2"> <button type="submit" class="btn btn-primary">Add</button>
<button type="submit" style="width:60%;" class="btn col-xs-2">
<span class="glyphicon glyphicon-ok"></span>
</button>
</div>
</form>
</div> </div>
<p></p> </form>
{% for u in group.user_member.all %}
<div class="row"> <!-- User Members -->
{% if group.user_member.all %}
<div class="divider">User Members</div>
<div class="space-y-2">
{% for u in group.user_member.all %}
<form <form
id="modal-form-group-member_{{ u.uuid }}" id="modal-form-group-member_{{ u.uuid }}"
class="form-inline"
role="form"
accept-charset="UTF-8" accept-charset="UTF-8"
action="" action=""
data-remote="true"
method="post" method="post"
> >
{% csrf_token %} {% csrf_token %}
<div class="col-xs-8"> <div class="flex items-center gap-2">
{{ u.username }} <span class="flex-1">{{ u.username }}</span>
<input type="hidden" name="member" value="{{ u.url }}" /> <input type="hidden" name="member" value="{{ u.url }}" />
<input type="hidden" name="action" value="remove" /> <input type="hidden" name="action" value="remove" />
</div> <button type="submit" class="btn btn-error btn-sm">
<div class="col-xs-2"></div> Remove
<div class="col-xs-2">
<button type="submit" style="width:60%;" class="btn col-xs-2">
<span class="glyphicon glyphicon-trash"></span>
</button> </button>
</div> </div>
</form> </form>
</div> {% endfor %}
{% endfor %} </div>
<p></p> {% endif %}
{% for g in group.group_member.all %}
<div class="row"> <!-- Group Members -->
{% if group.group_member.all %}
<div class="divider">Group Members</div>
<div class="space-y-2">
{% for g in group.group_member.all %}
<form <form
id="modal-form-group-member_{{ g.uuid }}" id="modal-form-group-member_{{ g.uuid }}"
class="form-inline"
role="form"
accept-charset="UTF-8" accept-charset="UTF-8"
action="" action=""
data-remote="true"
method="post" method="post"
> >
{% csrf_token %} {% csrf_token %}
<div class="col-xs-8"> <div class="flex items-center gap-2">
{{ g.name|safe }} <span class="flex-1">{{ g.name|safe }}</span>
<input type="hidden" name="member" value="{{ g.url }}" /> <input type="hidden" name="member" value="{{ g.url }}" />
<input type="hidden" name="action" value="remove" /> <input type="hidden" name="action" value="remove" />
</div> <button type="submit" class="btn btn-error btn-sm">
<div class="col-xs-2"></div> Remove
<div class="col-xs-2">
<button type="submit" style="width:60%;" class="btn col-xs-2">
<span class="glyphicon glyphicon-trash"></span>
</button> </button>
</div> </div>
</form> </form>
</div> {% endfor %}
{% endfor %} </div>
</div> {% endif %}
<div class="modal-footer"> </div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
Close <!-- Footer -->
</button> <div class="modal-action">
<button <button
type="button" type="button"
class="btn btn-primary" class="btn"
id="edit-package-modal-submit" onclick="this.closest('dialog').close()"
> >
Update Close
</button> </button>
</div>
</div> </div>
</div> </div>
</div>
<script>
$(function () {
$("#edit-package-modal-submit").click(function (e) {
e.preventDefault();
$("#edit-package-modal-form").submit();
});
$("#select_member").selectpicker(); <!-- Backdrop -->
}); <form method="dialog" class="modal-backdrop">
</script> <button>close</button>
</form>
</dialog>

View File

@ -1,71 +1,94 @@
{% load static %} {% load static %}
<!-- Edit Model -->
<div id="edit_model_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_model_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<button @close="reset()"
type="button" >
class="close" <div class="modal-box">
data-dismiss="modal" <!-- Header -->
aria-label="Close" <h3 class="font-bold text-lg">Update Model</h3>
>
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
</button> <form method="dialog">
<h3 class="modal-title">Update Model</h3> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<p>Alter Name and Description of the Model.</p> >
<form
id="edit-model-modal-form" </button>
accept-charset="UTF-8" </form>
action=""
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <p class="mb-4">Alter Name and Description of the Model.</p>
{% csrf_token %}
<p> <form
<label for="model-name">Name</label> id="edit-model-modal-form"
<input accept-charset="UTF-8"
id="model-name" action=""
type="text" method="post"
class="form-control" >
name="model-name" {% csrf_token %}
value="{{ model.name|safe }}"
/> <div class="form-control mb-3">
</p> <label class="label" for="model-name">
<p> <span class="label-text">Name</span>
<label for="model-description">Description</label> </label>
<input <input
id="model-description" id="model-name"
type="text" type="text"
class="form-control" class="input input-bordered w-full"
name="model-description" name="model-name"
value="{{ model.description|safe }}" value="{{ model.name|safe }}"
/> required
</p> />
</form> </div>
</div>
<div class="modal-footer"> <div class="form-control mb-3">
<button type="button" class="btn btn-secondary" data-dismiss="modal"> <label class="label" for="model-description">
Close <span class="label-text">Description</span>
</button> </label>
<button <input
type="button" id="model-description"
class="btn btn-primary" type="text"
id="edit-model-modal-submit" class="input input-bordered w-full"
> name="model-description"
Update value="{{ model.description|safe }}"
</button> />
</div> </div>
</form>
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('edit-model-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-model-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-model-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,70 +1,91 @@
{% load static %} {% load static %}
<!-- Edit Node -->
<div id="edit_node_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_node_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h5 class="modal-title">Edit Node</h5> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Edit Node</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<p>Edit Node.</p> >
<form
id="edit-node-modal-form" </button>
accept-charset="UTF-8" </form>
action=""
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <form
{% csrf_token %} id="edit-node-modal-form"
<p> accept-charset="UTF-8"
<label for="node-name">Name</label> action=""
<input method="post"
id="node-name" >
class="form-control" {% csrf_token %}
name="node-name"
value="{{ node.name|safe }}" <div class="form-control mb-3">
/> <label class="label" for="node-name">
</p> <span class="label-text">Name</span>
<p> </label>
<label for="node-description">Description</label> <input
<input id="node-name"
id="node-description" class="input input-bordered w-full"
type="text" name="node-name"
class="form-control" value="{{ node.name|safe }}"
value="{{ node.description|safe }}" required
name="node-description" />
/> </div>
</p>
</form> <div class="form-control mb-3">
</div> <label class="label" for="node-description">
<div class="modal-footer"> <span class="label-text">Description</span>
<button type="button" class="btn btn-secondary" data-dismiss="modal"> </label>
Close <input
</button> id="node-description"
<button type="text"
type="button" class="input input-bordered w-full"
class="btn btn-primary" value="{{ node.description|safe }}"
id="edit-node-modal-submit" name="node-description"
> />
Create </div>
</button> </form>
</div> </div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('edit-node-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-node-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-node-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,70 +1,91 @@
{% load static %} {% load static %}
<!-- Edit Package -->
<div id="edit_package_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_package_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h5 class="modal-title">Update Package</h5> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Update Package</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<p>Edit a Package.</p> >
<form
id="edit-package-modal-form" </button>
accept-charset="UTF-8" </form>
action=""
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <form
{% csrf_token %} id="edit-package-modal-form"
<p> accept-charset="UTF-8"
<label for="package-name">Name</label> action=""
<input method="post"
id="package-name" >
class="form-control" {% csrf_token %}
name="package-name"
value="{{ package.name|safe }}" <div class="form-control mb-3">
/> <label class="label" for="package-name">
</p> <span class="label-text">Name</span>
<p> </label>
<label for="package-description">Description</label> <input
<input id="package-name"
id="package-description" class="input input-bordered w-full"
type="text" name="package-name"
class="form-control" value="{{ package.name|safe }}"
value="{{ package.description|safe }}" required
name="package-description" />
/> </div>
</p>
</form> <div class="form-control mb-3">
</div> <label class="label" for="package-description">
<div class="modal-footer"> <span class="label-text">Description</span>
<button type="button" class="btn btn-secondary" data-dismiss="modal"> </label>
Close <input
</button> id="package-description"
<button type="text"
type="button" class="input input-bordered w-full"
class="btn btn-primary" value="{{ package.description|safe }}"
id="edit-package-modal-submit" name="package-description"
> />
Update </div>
</button> </form>
</div> </div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('edit-package-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-package-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-package-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,264 +1,271 @@
{% load static %} {% load static %}
<!-- Edit Package Permission --> <!-- Edit Package Permissions -->
<div id="edit_package_permissions_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_package_permissions_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="{
<h5 class="modal-title">Grant or Revoke Permissions</h5> updatePermissions(checkbox) {
<button const parts = checkbox.id.split('_');
type="button" const perm = parts[0];
class="close" const id = parts[1];
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
Modify permissions for this package. Note that if you give
<code>write</code> permissions to a user or group,
<code>read</code> permissions will be granted automatically.
<br />
To allow users to perform destructive actions, such as deleting the
package, <code>owner</code>
permissions must be granted.
</p>
<div class="row"> const readBox = document.getElementById('read_' + id);
<div class="col-xs-4"> const writeBox = document.getElementById('write_' + id);
<legend>User or Group</legend> const ownerBox = document.getElementById('owner_' + id);
</div>
<div class="col-xs-2">
<legend>Read</legend>
</div>
<div class="col-xs-2">
<legend>Write</legend>
</div>
<div class="col-xs-2">
<legend>Owner</legend>
</div>
</div>
<div class="row"> if (perm === 'read' && !readBox.checked) {
<form writeBox.checked = false;
id="modal-form-permissions" ownerBox.checked = false;
class="form-inline" }
role="form"
accept-charset="UTF-8" if (perm === 'write') {
action="" if (writeBox.checked) {
data-remote="true" readBox.checked = true;
method="post" } else {
> ownerBox.checked = false;
{% csrf_token %} }
<div class="col-xs-4"> }
<select
id="select_grantee" if (perm === 'owner' && ownerBox.checked) {
name="grantee" readBox.checked = true;
data-actions-box="true" writeBox.checked = true;
class="selPackages" }
data-width="100%" }
> }"
<option disabled selected>User</option> >
<div class="modal-box max-w-2xl">
<!-- Header -->
<h3 class="text-lg font-bold">Grant or Revoke Permissions</h3>
<!-- Close button (X) -->
<form method="dialog">
<button class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2">
</button>
</form>
<!-- Body -->
<div class="py-4">
<p class="mb-4">
Modify permissions for this package. Note that if you give
<code class="badge badge-ghost">write</code> permissions to a user or
group, <code class="badge badge-ghost">read</code> permissions will be
granted automatically.
<br />
To allow users to perform destructive actions, such as deleting the
package, <code class="badge badge-ghost">owner</code> permissions must
be granted.
</p>
<!-- Add New Permission -->
<form
id="modal-form-permissions"
accept-charset="UTF-8"
action=""
method="post"
class="mb-4"
>
{% csrf_token %}
<div class="grid grid-cols-12 gap-2 items-end">
<div class="col-span-5">
<label class="label">
<span class="label-text">User or Group</span>
</label>
<select
id="select_grantee"
name="grantee"
class="select select-bordered w-full select-sm"
required
>
<optgroup label="Users">
{% for u in users %} {% for u in users %}
<option value="{{ u.url }}">{{ u.username }}</option> <option value="{{ u.url }}">{{ u.username }}</option>
{% endfor %} {% endfor %}
<option disabled>Groups</option> </optgroup>
<optgroup label="Groups">
{% for g in groups %} {% for g in groups %}
<option value="{{ g.url }}">{{ g.name|safe }}</option> <option value="{{ g.url }}">{{ g.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </optgroup>
</div> </select>
<div class="col-xs-2"> </div>
<input type="checkbox" name="read" id="read_new" /> <div class="col-span-2 text-center">
</div> <label class="label justify-center">
<div class="col-xs-2"> <span class="label-text">Read</span>
<input type="checkbox" name="write" id="write_new" /> </label>
</div> <input
<div class="col-xs-2"> type="checkbox"
<input type="checkbox" name="owner" id="owner_new" /> name="read"
</div> id="read_new"
<div class="col-xs-2"> class="checkbox"
<button @click="updatePermissions($el)"
type="submit" />
style="width:60%;" </div>
class="btn col-xs-2 modify-perm-button" <div class="col-span-2 text-center">
> <label class="label justify-center">
<span class="glyphicon glyphicon-plus"></span> <span class="label-text">Write</span>
</button> </label>
</div> <input
</form> type="checkbox"
name="write"
id="write_new"
class="checkbox"
@click="updatePermissions($el)"
/>
</div>
<div class="col-span-2 text-center">
<label class="label justify-center">
<span class="label-text">Owner</span>
</label>
<input
type="checkbox"
name="owner"
id="owner_new"
class="checkbox"
@click="updatePermissions($el)"
/>
</div>
<div class="col-span-1">
<button type="submit" class="btn btn-primary btn-sm">+</button>
</div>
</div> </div>
<p></p> </form>
{% for up in user_permissions %}
<div class="row"> <!-- User Permissions -->
{% if user_permissions %}
<div class="divider">User Permissions</div>
<div class="space-y-2">
{% for up in user_permissions %}
<form <form
id="modal-form-permissions_{{ up.user.uuid }}" id="modal-form-permissions_{{ up.user.uuid }}"
class="form-inline"
role="form"
accept-charset="UTF-8" accept-charset="UTF-8"
action="" action=""
data-remote="true"
method="post" method="post"
> >
{% csrf_token %} {% csrf_token %}
<div class="col-xs-4"> <div class="grid grid-cols-12 gap-2 items-center">
{{ up.user.username }} <div class="col-span-5 truncate">
<input type="hidden" name="grantee" value="{{ up.user.url }}" /> {{ up.user.username }}
</div> <input
<div class="col-xs-2"> type="hidden"
<input name="grantee"
type="checkbox" value="{{ up.user.url }}"
name="read" />
id="read_{{ up.user.uuid }}" </div>
{% if up.has_read %}checked{% endif %} <div class="col-span-2 text-center">
/> <input
</div> type="checkbox"
<div class="col-xs-2"> name="read"
<input id="read_{{ up.user.uuid }}"
type="checkbox" class="checkbox"
name="write" {% if up.has_read %}checked{% endif %}
id="write_{{ up.user.uuid }}" @click="updatePermissions($el)"
{% if up.has_write %}checked{% endif %} />
/> </div>
</div> <div class="col-span-2 text-center">
<div class="col-xs-2"> <input
<input type="checkbox"
type="checkbox" name="write"
name="owner" id="write_{{ up.user.uuid }}"
id="owner_{{ up.user.uuid }}" class="checkbox"
{% if up.has_all %}checked{% endif %} {% if up.has_write %}checked{% endif %}
/> @click="updatePermissions($el)"
</div> />
<div class="col-xs-2"> </div>
<button <div class="col-span-2 text-center">
type="submit" <input
style="width:60%;" type="checkbox"
class="btn col-xs-2 modify-perm-button" name="owner"
> id="owner_{{ up.user.uuid }}"
<span class="glyphicon glyphicon-ok"></span> class="checkbox"
</button> {% if up.has_all %}checked{% endif %}
@click="updatePermissions($el)"
/>
</div>
<div class="col-span-1">
<button type="submit" class="btn btn-sm btn-ghost"></button>
</div>
</div> </div>
</form> </form>
</div> {% endfor %}
{% endfor %} </div>
<p></p> {% endif %}
{% for gp in group_permissions %}
<div class="row"> <!-- Group Permissions -->
{% if group_permissions %}
<div class="divider">Group Permissions</div>
<div class="space-y-2">
{% for gp in group_permissions %}
<form <form
id="modal-form-permissions_{{ gp.user.uuid }}" id="modal-form-permissions_{{ gp.group.uuid }}"
class="form-inline"
role="form"
accept-charset="UTF-8" accept-charset="UTF-8"
action="" action=""
data-remote="true"
method="post" method="post"
> >
{% csrf_token %} {% csrf_token %}
<div class="col-xs-4"> <div class="grid grid-cols-12 gap-2 items-center">
{{ gp.group.name|safe }} <div class="col-span-5 truncate">
<input {{ gp.group.name|safe }}
type="hidden" <input
name="grantee" type="hidden"
value="{{ gp.group.url }}" name="grantee"
/> value="{{ gp.group.url }}"
</div> />
<div class="col-xs-2"> </div>
<input <div class="col-span-2 text-center">
type="checkbox" <input
name="read" type="checkbox"
id="read_{{ gp.group.uuid }}" name="read"
{% if gp.has_read %}checked{% endif %} id="read_{{ gp.group.uuid }}"
/> class="checkbox"
</div> {% if gp.has_read %}checked{% endif %}
<div class="col-xs-2"> @click="updatePermissions($el)"
<input />
type="checkbox" </div>
name="write" <div class="col-span-2 text-center">
id="write_{{ gp.group.uuid }}" <input
{% if gp.has_write %}checked{% endif %} type="checkbox"
/> name="write"
</div> id="write_{{ gp.group.uuid }}"
<div class="col-xs-2"> class="checkbox"
<input {% if gp.has_write %}checked{% endif %}
type="checkbox" @click="updatePermissions($el)"
name="owner" />
id="owner_{{ gp.group.uuid }}" </div>
{% if gp.has_all %}checked{% endif %} <div class="col-span-2 text-center">
/> <input
</div> type="checkbox"
<div class="col-xs-2"> name="owner"
<button id="owner_{{ gp.group.uuid }}"
type="submit" class="checkbox"
style="width:60%;" {% if gp.has_all %}checked{% endif %}
class="btn col-xs-2 modify-perm-button" @click="updatePermissions($el)"
> />
<span class="glyphicon glyphicon-ok"></span> </div>
</button> <div class="col-span-1">
<button type="submit" class="btn btn-sm btn-ghost"></button>
</div>
</div> </div>
</form> </form>
</div> {% endfor %}
{% endfor %} </div>
</div> {% endif %}
<div class="modal-footer"> </div>
<button type="button" class="btn btn-secondary" data-dismiss="modal">
Close <!-- Footer -->
</button> <div class="modal-action">
<button <button
type="button" type="button"
class="btn btn-primary" class="btn"
id="edit-package-modal-submit" onclick="this.closest('dialog').close()"
> >
Update Close
</button> </button>
</div>
</div> </div>
</div> </div>
</div>
<script>
function checkboxClick() {
// id looks like read_3cadef24-220e-4587-9fa5-0e9a17aca2da
parts = this.id.split("_");
perm = parts[0];
id = parts[1];
readbox = "#read_" + id; <!-- Backdrop -->
writebox = "#write_" + id; <form method="dialog" class="modal-backdrop">
ownerbox = "#owner_" + id; <button>close</button>
</form>
if (perm == "read" && !$(readbox).prop("checked")) { </dialog>
$(writebox).prop("checked", false);
$(ownerbox).prop("checked", false);
}
if (perm == "write") {
if ($(writebox).prop("checked")) {
$(readbox).prop("checked", true);
}
if (!$(writebox).prop("checked")) {
$(ownerbox).prop("checked", false);
}
}
if (perm == "owner") {
if ($(ownerbox).prop("checked")) {
$(readbox).prop("checked", true);
$(writebox).prop("checked", true);
}
}
}
$(function () {
$("#edit-package-modal-submit").click(function (e) {
e.preventDefault();
$("#edit-package-modal-form").submit();
});
$("#select_grantee").selectpicker();
// Add click functions to permission checkboxes
$('[id^="read_"]').on("click", checkboxClick);
$('[id^="write_"]').on("click", checkboxClick);
$('[id^="owner_"]').on("click", checkboxClick);
});
</script>

View File

@ -1,82 +1,119 @@
{% load static %} {% load static %}
<!-- Edit Package -->
<div id="edit_password_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_password_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h5 class="modal-title">Update your Password</h5> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Update your Password</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<p>To change your password please fill out the following inputs</p> >
<form
id="edit-password-modal-form" </button>
accept-charset="UTF-8" </form>
action=""
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <p class="mb-4">
{% csrf_token %} To change your password please fill out the following inputs
<p> </p>
<label for="old-password">Old Password</label>
<input <form
id="old-password" id="edit-password-modal-form"
class="form-control" accept-charset="UTF-8"
name="old-password" action=""
type="password" method="post"
autocomplete="current-password" >
/> {% csrf_token %}
</p> <input type="hidden" name="hidden" value="update-password" />
<p>
<label for="new-password">New Password</label> <div class="form-control mb-3">
<input <label class="label" for="old-password">
id="new-password" <span class="label-text">Old Password</span>
class="form-control" </label>
name="new-password" <input
type="password" id="old-password"
, class="input input-bordered w-full"
autocomplete="new-password" name="old-password"
/> type="password"
</p> autocomplete="current-password"
<p> required
<label for="new-password-repeat">Repeat New Password</label> />
<input </div>
id="new-password-repeat"
class="form-control" <div class="form-control mb-3">
name="new-password-repeat" <label class="label" for="new-password">
type="password" <span class="label-text">New Password</span>
autocomplete="new-password" </label>
/> <input
</p> id="new-password"
</form> class="input input-bordered w-full"
</div> name="new-password"
<div class="modal-footer"> type="password"
<button type="button" class="btn btn-secondary" data-dismiss="modal"> autocomplete="new-password"
Close required
</button> />
<button </div>
type="button"
class="btn btn-primary" <div class="form-control mb-3">
id="edit-password-modal-submit" <label class="label" for="new-password-repeat">
> <span class="label-text">Repeat New Password</span>
Update </label>
</button> <input
</div> id="new-password-repeat"
class="input input-bordered w-full"
name="new-password-repeat"
type="password"
autocomplete="new-password"
required
@input="validatePasswordMatch('new-password', 'new-password-repeat')"
/>
<label class="label" x-show="errors['new-password-repeat']">
<span
class="label-text-alt text-error"
x-text="errors['new-password-repeat']"
></span>
</label>
</div>
</form>
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="if (validatePasswordMatch('new-password', 'new-password-repeat')) submit('edit-password-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-password-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-password-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,72 +1,92 @@
{% load static %} {% load static %}
<!-- Edit Pathway -->
<div id="edit_pathway_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_pathway_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h5 class="modal-title">Edit Pathway</h5> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Edit Pathway</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<p>Edit Pathway.</p> >
<form
id="edit-pathway-modal-form" </button>
accept-charset="UTF-8" </form>
action=""
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <form
{% csrf_token %} id="edit-pathway-modal-form"
<p> accept-charset="UTF-8"
<label for="pathway-name">Name</label> action=""
<input method="post"
id="pathway-name" >
class="form-control" {% csrf_token %}
name="pathway-name"
value="{{ pathway.name|safe }}" <div class="form-control mb-3">
/> <label class="label" for="pathway-name">
</p> <span class="label-text">Name</span>
<p> </label>
<label for="pathway-description">Description</label> <input
<textarea id="pathway-name"
id="pathway-description" class="input input-bordered w-full"
type="text" name="pathway-name"
class="form-control" value="{{ pathway.name|safe }}"
name="pathway-description" required
rows="10" />
> </div>
<div class="form-control mb-3">
<label class="label" for="pathway-description">
<span class="label-text">Description</span>
</label>
<textarea
id="pathway-description"
class="textarea textarea-bordered w-full"
name="pathway-description"
rows="10"
>
{{ pathway.description|safe }}</textarea {{ pathway.description|safe }}</textarea
> >
</p> </div>
</form> </form>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal"> <!-- Footer -->
Close <div class="modal-action">
</button> <button
<button type="button"
type="button" class="btn"
class="btn btn-primary" onclick="this.closest('dialog').close()"
id="edit-pathway-modal-submit" :disabled="isSubmitting"
> >
Update Close
</button> </button>
</div> <button
type="button"
class="btn btn-primary"
@click="submit('edit-pathway-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-pathway-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-pathway-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,163 +1,156 @@
{% load static %} {% load static %}
<!-- Edit Package --> <!-- Edit Prediction Setting -->
<div id="update_prediction_settings_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="update_prediction_settings_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h5 class="modal-title">Update Prediction Setting</h5> @close="reset()"
<button >
type="button" <div class="modal-box max-w-3xl">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="text-lg font-bold">Update Prediction Setting</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
<div class="modal-body"> :disabled="isSubmitting"
<p> >
To update your prediction setting modify parameters in the form below
und click "Update" </button>
</p> </form>
<form
id="edit-prediction-setting-modal-form" <!-- Body -->
accept-charset="UTF-8" <div class="py-4">
action="" <p class="mb-4">
data-remote="true" To update your prediction setting modify parameters in the form below
method="post" and click "Update"
> </p>
{% csrf_token %} <form
<div id="prediction-setting" class="panel-collapse in collapse"> id="edit-prediction-setting-modal-form"
<div class="panel-body list-group-item"> accept-charset="UTF-8"
<table class="table-bordered table-hover table"> action=""
<tr style="background-color: rgba(0, 0, 0, 0.08);"> method="post"
<th scope="col" width="20%">Parameter</th> >
<th scope="col" width="80%">Value</th> {% csrf_token %}
<div class="overflow-x-auto">
<table class="table table-zebra w-full">
<thead>
<tr>
<th class="w-1/5">Parameter</th>
<th class="w-4/5">Value</th>
</tr>
</thead>
<tbody>
{% if 'model' in user.prediction_settings %}
<tr>
<td>Model</td>
<td>
<div class="form-control">
<select
id="model"
name="model"
class="select select-bordered w-full"
>
{% for m in models %}
<option
value="{{ m.id }}"
{% if user.prediction_settings.model.url == m.url %}selected{% endif %}
>
{{ m.name|safe }}
</option>
{% endfor %}
</select>
</div>
{% for k, v in user.prediction_settings.model_parameters.items %}
{% if k == 'threshold' %}
<div class="form-control mt-2">
<label class="label">
<span class="label-text">Threshold</span>
</label>
<input
type="number"
class="input input-bordered w-full"
name="{{ k }}"
value="{{ v }}"
min="0"
max="1"
step="0.05"
/>
</div>
{% endif %}
{% endfor %}
</td>
</tr> </tr>
<tbody> {% endif %}
{% if 'model' in user.prediction_settings %} {% for k, v in user.prediction_settings.truncator.items %}
<tr> <tr>
<td width="20%">Model</td> <td>
<td width="80%"> {% if k == 'max_nodes' %}
<table Max Nodes
width="100%" {% elif k == 'max_depth' %}
class="table-bordered table-hover table" Max Depth
> {% endif %}
<tbody> </td>
<tr> <td>
<td colspan="2"> {% if k == 'max_nodes' %}
<select <input
id="model" type="number"
name="model" class="input input-bordered w-full"
class="form-control" name="{{ k }}"
data-width="100%" value="{{ v }}"
> min="1"
{% for m in models %} max="50"
<option step="1"
value="{{ m.id }}" />
{% if user.prediction_settings.model.url == m.url %}selected{% endif %} {% elif k == 'max_depth' %}
> <input
{{ m.name|safe }} type="number"
</option> class="input input-bordered w-full"
{% endfor %} name="{{ k }}"
</select> value="{{ v }}"
</td> min="1"
</tr> max="8"
{% for k, v in user.prediction_settings.model_parameters.items %} step="1"
<tr> />
<th width="20%">Model Parameter</th> {% endif %}
<th width="80%">Parameter Value</th> </td>
</tr> </tr>
<tr> {% endfor %}
<td width="20%"> </tbody>
{% if k == 'threshold' %} </table>
Threshold </div>
{% endif %} </form>
</td> </div>
<td width="80%">
{% if k == 'threshold' %} <!-- Footer -->
<input <div class="modal-action">
type="number" <button
class="form-control" type="button"
name="{{ k }}" class="btn"
value="{{ v }}" onclick="this.closest('dialog').close()"
min="0" :disabled="isSubmitting"
max="1" >
step="0.05" Close
/> </button>
{% endif %} <button
</td> type="button"
</tr> class="btn btn-primary"
{% endfor %} @click="submit('edit-prediction-setting-modal-form')"
</tbody> :disabled="isSubmitting"
</table> >
</td> <span x-show="!isSubmitting">Update</span>
</tr> <span
{% endif %} x-show="isSubmitting"
{% for k, v in user.prediction_settings.truncator.items %} class="loading loading-spinner loading-sm"
<tr> ></span>
<td> <span x-show="isSubmitting">Updating...</span>
<p> </button>
{% if k == 'max_nodes' %}
Max Nodes
{% elif k == 'max_depth' %}
Max Depth
{% endif %}
</p>
</td>
<td>
<p>
{% if k == 'max_nodes' %}
<input
type="number"
class="form-control"
name="{{ k }}"
value="{{ v }}"
min="1"
max="50"
step="1"
/>
{% elif k == 'max_depth' %}
<input
type="number"
class="form-control"
name="{{ k }}"
value="{{ v }}"
min="1"
max="8"
step="1"
/>
{% endif %}
</p>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
Close
</button>
<button
type="button"
class="btn btn-primary"
id="edit-prediction-setting-modal-submit"
>
Update
</button>
</div>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-prediction-setting-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-prediction-setting-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,69 +1,91 @@
{% load static %} {% load static %}
<!-- Edit Reaction -->
<div id="edit_reaction_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_reaction_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<button @close="reset()"
type="button" >
class="close" <div class="modal-box">
data-dismiss="modal" <!-- Header -->
aria-label="Close" <h3 class="font-bold text-lg">Update Reaction</h3>
>
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
</button> <form method="dialog">
<h3 class="modal-title">Update Reaction</h3> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<form >
id="edit-reaction-modal-form"
accept-charset="UTF-8" </button>
action="" </form>
data-remote="true"
method="post" <!-- Body -->
> <div class="py-4">
{% csrf_token %} <form
<p> id="edit-reaction-modal-form"
<label for="reaction-name">Name</label> accept-charset="UTF-8"
<input action=""
id="reaction-name" method="post"
class="form-control" >
name="reaction-name" {% csrf_token %}
value="{{ reaction.name|safe }}"
/> <div class="form-control mb-3">
</p> <label class="label" for="reaction-name">
<p> <span class="label-text">Name</span>
<label for="reaction-description">Description</label> </label>
<input <input
id="reaction-description" id="reaction-name"
type="text" class="input input-bordered w-full"
class="form-control" name="reaction-name"
value="{{ reaction.description|safe }}" value="{{ reaction.name|safe }}"
name="reaction-description" required
/> />
</p> </div>
</form>
</div> <div class="form-control mb-3">
<div class="modal-footer"> <label class="label" for="reaction-description">
<button type="button" class="btn btn-secondary" data-dismiss="modal"> <span class="label-text">Description</span>
Close </label>
</button> <input
<button id="reaction-description"
type="button" type="text"
class="btn btn-primary" class="input input-bordered w-full"
id="edit-reaction-modal-submit" value="{{ reaction.description|safe }}"
> name="reaction-description"
Update />
</button> </div>
</div> </form>
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('edit-reaction-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-reaction-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-reaction-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,69 +1,91 @@
{% load static %} {% load static %}
<!-- Edit Rule -->
<div id="edit_rule_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_rule_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<button @close="reset()"
type="button" >
class="close" <div class="modal-box">
data-dismiss="modal" <!-- Header -->
aria-label="Close" <h3 class="font-bold text-lg">Update Rule</h3>
>
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
</button> <form method="dialog">
<h3 class="modal-title">Update Rule</h3> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<form >
id="edit-rule-modal-form"
accept-charset="UTF-8" </button>
action="" </form>
data-remote="true"
method="post" <!-- Body -->
> <div class="py-4">
{% csrf_token %} <form
<p> id="edit-rule-modal-form"
<label for="rule-name">Name</label> accept-charset="UTF-8"
<input action=""
id="rule-name" method="post"
class="form-control" >
name="rule-name" {% csrf_token %}
value="{{ rule.name|safe }}"
/> <div class="form-control mb-3">
</p> <label class="label" for="rule-name">
<p> <span class="label-text">Name</span>
<label for="rule-description">Description</label> </label>
<input <input
id="rule-description" id="rule-name"
type="text" class="input input-bordered w-full"
class="form-control" name="rule-name"
value="{{ rule.description|safe }}" value="{{ rule.name|safe }}"
name="rule-description" required
/> />
</p> </div>
</form>
</div> <div class="form-control mb-3">
<div class="modal-footer"> <label class="label" for="rule-description">
<button type="button" class="btn btn-secondary" data-dismiss="modal"> <span class="label-text">Description</span>
Close </label>
</button> <input
<button id="rule-description"
type="button" type="text"
class="btn btn-primary" class="input input-bordered w-full"
id="edit-rule-modal-submit" value="{{ rule.description|safe }}"
> name="rule-description"
Update />
</button> </div>
</div> </form>
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('edit-rule-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-rule-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-rule-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,110 +1,128 @@
{% load static %} {% load static %}
<!-- Edit User -->
<div id="edit_user_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="edit_user_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h5 class="modal-title">Update User Defaults</h5> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Update User Defaults</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
<p>Edit User Defaults.</p> >
<form
id="edit-user-modal-form" </button>
accept-charset="UTF-8" </form>
action=""
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <form
{% csrf_token %} id="edit-user-modal-form"
<p> accept-charset="UTF-8"
<label for="default-package">Default Package</label> action=""
<select method="post"
id="default-package" >
name="default-package" {% csrf_token %}
class="form-control"
data-width="100%" <div class="form-control mb-3">
> <label class="label" for="default-package">
<option disabled>Select a Package</option> <span class="label-text">Default Package</span>
{% for p in meta.writeable_packages %} </label>
<option <select
value="{{ p.url }}" id="default-package"
{% if p.id == meta.user.default_package.id %}selected{% endif %} name="default-package"
> class="select select-bordered w-full"
{{ p.name|safe }} >
</option> <option disabled>Select a Package</option>
{% endfor %} {% for p in meta.writeable_packages %}
</select> <option
</p> value="{{ p.url }}"
<p> {% if p.id == meta.user.default_package.id %}selected{% endif %}
<label for="default-group">Default Group</label> >
<select {{ p.name|safe }}
id="default-group" </option>
name="default-group" {% endfor %}
class="form-control" </select>
data-width="100%" </div>
>
<option disabled>Select a Group</option> <div class="form-control mb-3">
{% for g in meta.available_groups %} <label class="label" for="default-group">
<option <span class="label-text">Default Group</span>
value="{{ g.url }}" </label>
{% if g.id == meta.user.default_group.id %}selected{% endif %} <select
> id="default-group"
{{ g.name|safe }} name="default-group"
</option> class="select select-bordered w-full"
{% endfor %} >
</select> <option disabled>Select a Group</option>
</p> {% for g in meta.available_groups %}
<p> <option
<label for="default-prediction-setting" value="{{ g.url }}"
>Default Prediction Setting</label {% if g.id == meta.user.default_group.id %}selected{% endif %}
> >
<select {{ g.name|safe }}
id="default-prediction-setting" </option>
name="default-prediction-setting" {% endfor %}
class="form-control" </select>
data-width="100%" </div>
>
<option disabled>Select a Setting</option> <div class="form-control mb-3">
{% for s in meta.available_settings %} <label class="label" for="default-prediction-setting">
<option <span class="label-text">Default Prediction Setting</span>
value="{{ s.url }}" </label>
{% if s.id == meta.user.default_setting.id %}selected{% endif %} <select
> id="default-prediction-setting"
{{ s.name|safe }} name="default-prediction-setting"
</option> class="select select-bordered w-full"
{% endfor %} >
</select> <option disabled>Select a Setting</option>
</p> {% for s in meta.available_settings %}
</form> <option
</div> value="{{ s.url }}"
<div class="modal-footer"> {% if s.id == meta.user.default_setting.id %}selected{% endif %}
<button type="button" class="btn btn-secondary" data-dismiss="modal"> >
Close {{ s.name|safe }}
</button> </option>
<button {% endfor %}
type="button" </select>
class="btn btn-primary" </div>
id="edit-user-modal-submit" </form>
> </div>
Update
</button> <!-- Footer -->
</div> <div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('edit-user-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Update</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Updating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#edit-user-modal-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#edit-user-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,93 +1,123 @@
<div <dialog
class="modal fade"
tabindex="-1"
id="evaluate_model_modal" id="evaluate_model_modal"
role="dialog" class="modal"
aria-labelledby="evaluate_model_modal" x-data="modalForm()"
aria-hidden="true" @close="reset()"
> >
<div class="modal-dialog modal-lg"> <div class="modal-box max-w-3xl">
<div class="modal-content"> <!-- Header -->
<div class="modal-header"> <h3 class="text-lg font-bold">Evaluate Model</h3>
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> <!-- Close button (X) -->
<span class="sr-only">Close</span> <form method="dialog">
</button> <button
<h4 class="modal-title">Evaluate Model</h4> class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
</div> :disabled="isSubmitting"
<div class="modal-body"> >
<form
id="evaluate_model_form" </button>
accept-charset="UTF-8" </form>
action="{{ current_object.url }}"
data-remote="true" <!-- Body -->
method="post" <div class="py-4">
> <form
{% csrf_token %} id="evaluate_model_form"
<div class="jumbotron"> accept-charset="UTF-8"
action="{{ current_object.url }}"
method="post"
>
{% csrf_token %}
<div class="alert alert-info mb-4">
<span>
For evaluation, you need to select the packages you want to use. For evaluation, you need to select the packages you want to use.
While the model is evaluating, you can use the model for While the model is evaluating, you can use the model for
predictions. predictions.
</div> </span>
<!-- Evaluation Packages --> </div>
<label for="model-evaluation-packages">Evaluation Packages</label>
<!-- Evaluation Packages -->
<div class="form-control">
<label class="label" for="model-evaluation-packages">
<span class="label-text">Evaluation Packages</span>
</label>
<select <select
id="model-evaluation-packages" id="model-evaluation-packages"
name="model-evaluation-packages" name="model-evaluation-packages"
data-actions-box="true" class="select select-bordered w-full h-48"
class="form-control"
multiple multiple
data-width="100%" required
> >
<option disabled>Reviewed Packages</option> <optgroup label="Reviewed Packages">
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</optgroup>
<option disabled>Unreviewed Packages</option> <optgroup label="Unreviewed Packages">
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</optgroup>
</select> </select>
<label class="label">
<span class="label-text-alt"
>Hold Ctrl/Cmd to select multiple packages</span
>
</label>
</div>
<!-- Eval Type --> <!-- Eval Type -->
<label for="model-evaluation-type">Evaluation Type</label> <div class="form-control mt-4">
<label class="label" for="model-evaluation-type">
<span class="label-text">Evaluation Type</span>
</label>
<select <select
id="model-evaluation-type" id="model-evaluation-type"
name="model-evaluation-type" name="model-evaluation-type"
class="form-control" class="select select-bordered w-full"
required
> >
<option disabled selected>Select evaluation type</option> <option value="" disabled selected>Select evaluation type</option>
<option value="sg">Single Generation</option> <option value="sg">Single Generation</option>
<option value="mg">Multiple Generations</option> <option value="mg">Multiple Generations</option>
</select> </select>
</div>
<input type="hidden" name="hidden" value="evaluate" /> <input type="hidden" name="hidden" value="evaluate" />
</form> </form>
</div> </div>
<div class="modal-footer">
<a id="evaluate_model_form_submit" class="btn btn-primary" href="#" <!-- Footer -->
>Evaluate</a <div class="modal-action">
> <button
<button type="button" class="btn btn-default" data-dismiss="modal"> type="button"
Cancel class="btn"
</button> onclick="this.closest('dialog').close()"
</div> :disabled="isSubmitting"
>
Cancel
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('evaluate_model_form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Evaluate</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Evaluating...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#model-evaluation-packages").selectpicker(); <button :disabled="isSubmitting">close</button>
</form>
$("#evaluate_model_form_submit").on("click", function (e) { </dialog>
e.preventDefault();
$("#evaluate_model_form").submit();
});
});
</script>

View File

@ -1,53 +1,56 @@
{% load static %} {% load static %}
<!-- Export Package -->
<div id="export_package_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="export_package_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<h3 class="modal-title">Export Package as JSON</h3> @close="reset()"
<button >
type="button" <div class="modal-box">
class="close" <!-- Header -->
data-dismiss="modal" <h3 class="font-bold text-lg">Export Package as JSON</h3>
aria-label="Close"
> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
</button> <button
</div> class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
<div class="modal-body"> :disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<p>
By clicking on Export the Package will be serialized into a JSON and By clicking on Export the Package will be serialized into a JSON and
directly downloaded. opened in a new tab.
<form </p>
id="export-package-modal-form" </div>
accept-charset="UTF-8"
action="{{ package.url }}" <!-- Footer -->
data-remote="true" <div class="modal-action">
method="GET" <button
> type="button"
<input type="hidden" name="export" value="true" /> class="btn"
</form> onclick="this.closest('dialog').close()"
</div> :disabled="isSubmitting"
<div class="modal-footer"> >
<button type="button" class="btn btn-secondary" data-dismiss="modal"> Close
Close </button>
</button> <button
<button type="button"
type="button" class="btn btn-primary"
class="btn btn-primary" @click="window.open('{{ package.url }}?export=true', '_blank'); $el.closest('dialog').close();"
id="export-package-modal-form-submit" :disabled="isSubmitting"
> >
Export Export
</button> </button>
</div>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#export-package-modal-form-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#export-package-modal-form").submit(); </dialog>
$("#export_package_modal").modal("hide");
});
});
</script>

View File

@ -1,109 +1,142 @@
{% load static %} {% load static %}
<!-- Copy Object --> <!-- Copy Object -->
<div id="generic_copy_object_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="generic_copy_object_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="{
<h3 class="modal-title">Copy {{ object_type|capfirst }}</h3> isSubmitting: false,
<button errorMessage: '',
type="button" targetPackage: '',
class="close"
data-dismiss="modal" reset() {
aria-label="Close" this.isSubmitting = false;
> this.errorMessage = '';
<span aria-hidden="true">&times;</span> this.targetPackage = '';
</button> },
</div>
<div class="modal-body"> async submit() {
<form if (!this.targetPackage) return;
id="generic-copy-object-modal-form"
accept-charset="UTF-8" this.isSubmitting = true;
data-remote="true" this.errorMessage = '';
method="post"
> try {
{% csrf_token %} const response = await fetch(this.targetPackage, {
<label for="target-package" method: 'POST',
>Select the Target Package you want to copy this {{ object_type }} headers: {
into</label 'Content-Type': 'application/x-www-form-urlencoded',
> 'X-CSRFToken': document.querySelector('[name=csrfmiddlewaretoken]').value
},
body: new URLSearchParams({
hidden: 'copy',
object_to_copy: '{{ current_object.url }}'
})
});
const data = await response.json();
if (response.ok) {
window.location.href = data.success;
} else {
if (data.error && data.error.indexOf('to the same package') > -1) {
this.errorMessage = 'The target Package is the same as the source Package. Please select another target!';
} else {
this.errorMessage = data.error || 'An error occurred';
}
}
} catch (error) {
this.errorMessage = 'An error occurred while copying';
} finally {
this.isSubmitting = false;
}
}
}"
@close="reset()"
>
<div class="modal-box">
<!-- Header -->
<h3 class="text-lg font-bold">Copy {{ object_type|capfirst }}</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<form
id="generic-copy-object-modal-form"
accept-charset="UTF-8"
method="post"
>
{% csrf_token %}
<div class="form-control">
<label class="label" for="target-package">
<span class="label-text">
Select the Target Package you want to copy this {{ object_type }}
into
</span>
</label>
<select <select
id="target-package" id="target-package"
name="target-package" name="target-package"
data-actions-box="true" class="select select-bordered w-full"
class="form-control" x-model="targetPackage"
data-width="100%" required
> >
<option disabled selected>Select Target Package</option> <option value="" disabled selected>Select Target Package</option>
{% for p in meta.writeable_packages %} {% for p in meta.writeable_packages %}
<option value="{{ p.url }}">{{ p.name|safe }}</option> <option value="{{ p.url }}">{{ p.name|safe }}</option>
`
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" name="hidden" value="copy" /> </div>
</form> <input type="hidden" name="hidden" value="copy" />
<div </form>
id="copy-object-error-message"
class="alert alert-danger" <!-- Error Message -->
role="alert" <div
style="display: none" x-show="errorMessage"
></div> x-cloak
</div> class="alert alert-error mt-4"
<div class="modal-footer"> role="alert"
<button type="button" class="btn btn-secondary" data-dismiss="modal"> >
Close <span x-text="errorMessage"></span>
</button>
<button
type="button"
class="btn btn-primary"
id="generic-copy-object-modal-form-submit"
>
Copy
</button>
</div> </div>
</div> </div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit()"
:disabled="isSubmitting || !targetPackage"
>
<span x-show="!isSubmitting">Copy</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Copying...</span>
</button>
</div>
</div> </div>
</div>
<script>
$(function () {
$("#generic-copy-object-modal-form-submit").click(function (e) {
e.preventDefault();
$("#copy-object-error-message").hide();
const packageUrl = $("#target-package").find(":selected").val(); <!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
if ( <button :disabled="isSubmitting">close</button>
packageUrl === "Select Target Package" || </form>
packageUrl === "" || </dialog>
packageUrl === null ||
packageUrl === undefined
) {
return;
}
const formData = {
hidden: "copy",
object_to_copy: "{{ current_object.url }}",
};
$.ajax({
type: "post",
data: formData,
url: packageUrl,
success: function (data, textStatus) {
window.location.href = data.success;
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.responseJSON.error.indexOf("to the same package") > -1) {
$("#copy-object-error-message").append(
"<p>The target Package is the same as the source Package. Please select another target!</p>",
);
} else {
$("#copy-object-error-message").append(
"<p>" + jqXHR.responseJSON.error + "</p>",
);
}
$("#copy-object-error-message").show();
},
});
});
});
</script>

View File

@ -1,58 +1,99 @@
{% load static %} {% load static %}
<!-- Delete Object --> <!--
<div id="generic_delete_modal" class="modal" tabindex="-1"> Generic Delete Modal - Delete object with confirmation
<div class="modal-dialog">
<div class="modal-content"> Migrated from Bootstrap + jQuery to DaisyUI + Alpine.js
<div class="modal-header"> Uses native <dialog> element with .showModal() API
<h3 class="modal-title">Delete {{ object_type|capfirst }}</h3> -->
<button
type="button" <dialog
class="close" id="generic_delete_modal"
data-dismiss="modal" class="modal"
aria-label="Close" x-data="modalForm()"
@close="reset()"
>
<div class="modal-box">
<!-- Header -->
<h3 class="font-bold text-lg">Delete {{ object_type|capfirst }}</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
:disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<!-- Warning message -->
<div class="alert alert-warning">
<svg
xmlns="http://www.w3.org/2000/svg"
class="stroke-current shrink-0 h-6 w-6"
fill="none"
viewBox="0 0 24 24"
> >
<span aria-hidden="true">&times;</span> <path
</button> stroke-linecap="round"
</div> stroke-linejoin="round"
<div class="modal-body"> stroke-width="2"
{% if object_type == 'user' %} 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"
Clicking "Delete" will <strong>permanently</strong> delete the User />
and associated data. This action can't be undone! </svg>
{% else %} <span>
Deletes the {{ object_type|capfirst }}. Related objects that depend on {% if object_type == 'user' %}
this {{ object_type|capfirst }} will be deleted as well. Clicking "Delete" will <strong>permanently</strong> delete the User
{% endif %} and associated data. This action can't be undone!
<form {% else %}
id="generic-delete-modal-form" Deletes the {{ object_type|capfirst }}. Related objects that depend
accept-charset="UTF-8" on this {{ object_type|capfirst }} will be deleted as well.
action="{{ current_object.url }}" {% endif %}
data-remote="true" </span>
method="post"
>
{% csrf_token %}
<input type="hidden" id="hidden" name="hidden" value="delete" />
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
Close
</button>
<button
type="button"
class="btn btn-primary"
id="generic-delete-modal-form-submit"
>
Delete
</button>
</div> </div>
<!-- Hidden form -->
<form
id="generic-delete-modal-form"
accept-charset="UTF-8"
action="{{ current_object.url }}"
method="post"
>
{% csrf_token %}
<input type="hidden" name="hidden" value="delete" />
</form>
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Cancel
</button>
<button
type="button"
class="btn btn-error"
@click="submit('generic-delete-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Delete</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Deleting...</span>
</button>
</div> </div>
</div> </div>
</div>
<script> <!-- Backdrop (click to close) -->
$(function () { <form method="dialog" class="modal-backdrop">
$("#generic-delete-modal-form-submit").click(function (e) { <button :disabled="isSubmitting">close</button>
e.preventDefault(); </form>
$("#generic-delete-modal-form").submit(); </dialog>
});
});
</script>

View File

@ -1,213 +1,173 @@
{% load static %} {% load static %}
<style> <dialog
.alias-container {
display: flex;
flex-wrap: wrap;
align-items: center;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 6px;
cursor: text;
min-height: 38px;
background-color: #fff;
}
.alias {
display: inline-flex;
align-items: center;
background-color: #5bc0de;
color: white;
padding: 4px 8px;
margin: 3px 3px;
border-radius: 4px;
font-size: 13px;
line-height: 1.4;
}
.alias .remove {
margin-left: 6px;
cursor: pointer;
font-weight: bold;
line-height: 1;
}
.alias-input {
flex: 1;
min-width: 120px;
border: none;
outline: none;
margin: 3px 3px;
font-size: 14px;
}
.form-control.alias-container {
height: auto;
box-shadow: none;
}
</style>
<div
class="modal fade bs-modal-lg"
id="set_aliases_modal" id="set_aliases_modal"
tabindex="-1" class="modal"
aria-labelledby="set_aliases_modal" x-data="{
aria-modal="true" isSubmitting: false,
role="dialog" aliases: [{% for alias in current_object.aliases %}'{{ alias|escapejs }}'{% if not forloop.last %},{% endif %}{% endfor %}],
> newAlias: '',
<div class="modal-dialog modal-lg"> errorMessage: '',
<div class="modal-content">
<div class="modal-header">
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
>
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title">
Set Aliases for {{ current_object.name|safe }}
</h4>
</div>
<div class="modal-body">
<form
id="set_aliases_modal_form"
accept-charset="UTF-8"
action="{{ current_object.url }}"
data-remote="true"
method="post"
>
{% csrf_token %}
<label for="alias-input">Aliases:</label>
<div class="form-control alias-container" id="alias-box">
{% for alias in current_object.aliases %}
<span class="alias"
>{{ alias|escape }}<span class="remove">&times;</span></span
>
{% endfor %}
<input
type="text"
id="alias-input"
class="alias-input"
placeholder="Add Alias..."
/>
</div>
</form>
<div
id="add-alias-error-message"
class="alert alert-danger"
role="alert"
style="display: none"
></div>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary pull-left"
data-dismiss="modal"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
id="set_aliases_modal_form_submit"
>
Submit
</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
function addAlias(aliasText) {
aliasText = aliasText.trim();
if (aliasText === "") return;
// Avoid duplicate aliass reset() {
var exists = false; this.isSubmitting = false;
$("#alias-box .alias").each(function () { this.errorMessage = '';
if ( },
$(this).text().replace("×", "").trim().toLowerCase() ===
aliasText.toLowerCase() addAlias() {
) { const aliasText = this.newAlias.trim();
exists = true; if (aliasText === '') return;
return false;
} // Check for duplicates (case-insensitive)
}); const exists = this.aliases.some(
a => a.toLowerCase() === aliasText.toLowerCase()
);
if (!exists) { if (!exists) {
var aliasHtml = this.aliases.push(aliasText);
'<span class="alias">' +
$("<div>").text(aliasText).html() +
'<span class="remove">&times;</span></span>';
$(aliasHtml).insertBefore("#alias-input");
} }
$("#alias-input").val(""); this.newAlias = '';
} },
// Add alias when Enter is pressed removeAlias(index) {
$("#alias-input").on("keypress", function (e) { this.aliases.splice(index, 1);
if (e.which === 13) { },
handleKeypress(e) {
if (e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
addAlias($(this).val()); this.addAlias();
} }
}); },
// Add alias when input loses focus handleBlur() {
$("#alias-input").on("blur", function () { if (this.newAlias.trim() !== '') {
var val = $(this).val(); this.addAlias();
if (val.trim() !== "") {
addAlias(val);
} }
}); },
// Remove alias when clicking × async submit() {
$("#alias-box").on("click", ".remove", function () { this.isSubmitting = true;
$(this).closest(".alias").remove(); this.errorMessage = '';
});
// Focus input when clicking the container const formData = new URLSearchParams();
$("#alias-box").on("click", function () { if (this.aliases.length === 0) {
$("#alias-input").focus(); formData.append('aliases', '');
}); } else {
this.aliases.forEach(alias => {
$("#set_aliases_modal_form_submit").on("click", function (e) { formData.append('aliases', alias);
e.preventDefault(); });
let aliases = [];
$("#alias-box .alias").each(function () {
aliases.push($(this).text().replace("×", "").trim());
});
if (aliases.length === 0) {
// Set empty string for deletion of all aliases
// If empty list is sent, its gets removed entirely from post data
aliases = [""];
} }
formData = { try {
aliases: aliases, const response = await fetch('{{ current_object.url }}', {
}; method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
},
body: formData
});
$.ajax({ if (response.ok) {
type: "post", const data = await response.json();
data: formData,
url: "{{ current_object.url }}",
traditional: true,
success: function (data, textStatus) {
window.location.href = data.success; window.location.href = data.success;
}, } else {
error: function (jqXHR, textStatus, errorThrown) { this.errorMessage = 'Setting aliases failed!';
$("#add-alias-error-message").append( }
"<p>Setting aliases failed!</p>", } catch (error) {
); this.errorMessage = 'Setting aliases failed!';
$("#add-alias-error-message").show(); } finally {
}, this.isSubmitting = false;
}); }
}); }
}); }"
</script> @close="reset()"
>
<div class="modal-box max-w-4xl">
<!-- Header -->
<h3 class="text-lg font-bold">
Set Aliases for {{ current_object.name|safe }}
</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<div class="form-control">
<label class="label">
<span class="label-text">Aliases:</span>
</label>
<div
class="flex flex-wrap items-center gap-1 p-2 border border-base-300 rounded-lg bg-base-100 min-h-[38px] cursor-text"
@click="$refs.aliasInput.focus()"
>
<template x-for="(alias, index) in aliases" :key="index">
<span class="badge badge-info gap-1 py-3">
<span x-text="alias"></span>
<button
type="button"
class="btn btn-ghost btn-xs p-0 h-auto min-h-0"
@click.stop="removeAlias(index)"
>
</button>
</span>
</template>
<input
type="text"
x-ref="aliasInput"
x-model="newAlias"
class="flex-1 min-w-[120px] border-none outline-none bg-transparent text-sm"
placeholder="Add Alias..."
@keypress="handleKeypress($event)"
@blur="handleBlur()"
/>
</div>
</div>
<!-- Error Message -->
<div x-show="errorMessage" x-cloak class="alert alert-error mt-4">
<span x-text="errorMessage"></span>
</div>
</div>
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit()"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Submitting...</span>
</button>
</div>
</div>
<!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

@ -1,97 +1,137 @@
{% load static %} {% load static %}
<!-- Delete Object --> <!-- Set External Reference -->
<div id="generic_set_external_reference_modal" class="modal" tabindex="-1"> <dialog
<div class="modal-dialog"> id="generic_set_external_reference_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="{
<h3 class="modal-title">Add External References</h3> isSubmitting: false,
<button selectedDatabase: '',
type="button" placeholder: '',
class="close"
data-dismiss="modal" reset() {
aria-label="Close" this.isSubmitting = false;
> this.selectedDatabase = '';
<span aria-hidden="true">&times;</span> this.placeholder = '';
</button> },
</div>
<div class="modal-body"> updatePlaceholder() {
<form if (this.selectedDatabase) {
id="generic-set-external-reference-modal-form" const option = document.querySelector('#database-select option[value=\'' + this.selectedDatabase + '\']');
accept-charset="UTF-8" if (option) {
action="{{ current_object.url }}" this.placeholder = option.dataset.inputPlaceholder || '';
data-remote="true" }
method="post" }
> },
{% csrf_token %}
<label for="database-select" submit(formId) {
>Select the Database you want to attach an External Reference const form = document.getElementById(formId);
for</label if (form && form.checkValidity()) {
> this.isSubmitting = true;
form.submit();
} else if (form) {
form.reportValidity();
}
}
}"
@close="reset()"
>
<div class="modal-box">
<!-- Header -->
<h3 class="text-lg font-bold">Add External References</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button>
</form>
<!-- Body -->
<div class="py-4">
<form
id="generic-set-external-reference-modal-form"
accept-charset="UTF-8"
action="{{ current_object.url }}"
method="post"
>
{% csrf_token %}
<div class="form-control">
<label class="label" for="database-select">
<span class="label-text">
Select the Database you want to attach an External Reference for
</span>
</label>
<select <select
id="database-select" id="database-select"
name="selected-database" name="selected-database"
data-actions-box="true" class="select select-bordered w-full"
class="form-control" x-model="selectedDatabase"
data-width="100%" @change="updatePlaceholder()"
required
> >
<option disabled selected>Select Database</option> <option value="" disabled selected>Select Database</option>
{% for entity, databases in meta.external_databases.items %} {% for entity, databases in meta.external_databases.items %}
{% if entity == object_type %} {% if entity == object_type %}
{% for db in databases %} {% for db in databases %}
<option <option
id="db-select-{{ db.database.pk }}"
data-input-placeholder="{{ db.placeholder }}"
value="{{ db.database.id }}" value="{{ db.database.id }}"
data-input-placeholder="{{ db.placeholder }}"
> >
{{ db.database.name|safe }} {{ db.database.name|safe }}
</option> </option>
`
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
<p></p> </div>
<div id="input-div" style="display: none">
<label for="identifier">The reference</label> <div class="form-control mt-4" x-show="selectedDatabase" x-cloak>
<input <label class="label" for="identifier">
type="text" <span class="label-text">The reference</span>
id="identifier" </label>
name="identifier" <input
class="form-control" type="text"
placeholder="" id="identifier"
/> name="identifier"
</div> class="input input-bordered w-full"
</form> :placeholder="placeholder"
</div> required
<div class="modal-footer"> />
<button type="button" class="btn btn-secondary" data-dismiss="modal"> </div>
Close </form>
</button> </div>
<button
type="button" <!-- Footer -->
class="btn btn-primary" <div class="modal-action">
id="generic-set-external-reference-modal-form-submit" <button
> type="button"
Submit class="btn"
</button> onclick="this.closest('dialog').close()"
</div> :disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('generic-set-external-reference-modal-form')"
:disabled="isSubmitting"
>
<span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Submitting...</span>
</button>
</div> </div>
</div> </div>
</div>
<script>
$(function () {
$("#database-select").on("change", function () {
let selected = $(this).val();
$("#identifier").attr(
"placeholder",
$("#db-select-" + selected).data("input-placeholder"),
);
$("#input-div").show();
});
$("#generic-set-external-reference-modal-form-submit").click(function (e) { <!-- Backdrop -->
e.preventDefault(); <form method="dialog" class="modal-backdrop">
$("#generic-set-external-reference-modal-form").submit(); <button :disabled="isSubmitting">close</button>
}); </form>
}); </dialog>
</script>

Some files were not shown because too many files have changed in this diff Show More