21 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
4e80cd63cd [Fix] Added loading of envipytags (#201)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#201
2025-11-13 08:43:21 +13:00
6592f0a68e [Fix] Package Link + Adjusted License container (#197)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#197
2025-11-13 08:35:42 +13:00
21d30a923f [Refactor] Large scale formatting/linting (#193)
All html files now prettier formatted and fixes for incompatible blocks applied

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#193
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 22:47:10 +13:00
12a20756d6 [Fix] Add/Update missing nav links (#196)
Adds packages to the footer (instead of Browse link) and removes search from footer (search page not directly linked anymore).
Packages are also available in Browse now.

This is temporary until there is a proper data browsing page.

Co-authored-by: jebus <lorsbach@envipath.com>
Reviewed-on: enviPath/enviPy#196
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 22:24:33 +13:00
d20a705011 [Feature] Add per-package pathway prediction (#195)
## Major Changes

- Introduces a new view for per-package predictions

Co-authored-by: jebus <lorsbach@envipath.com>
Reviewed-on: enviPath/enviPy#195
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 22:01:34 +13:00
debbef8158 [Enhancement] Cleanup Landing Page Form (#194)
I changed the toggle style to be more self evident.
Do you think this is enough, or should I add an (ugly) label?

![image.png](/attachments/0e4ce043-7544-4852-9db9-460517b36d64)

Co-authored-by: jebus <lorsbach@envipath.com>
Reviewed-on: enviPath/enviPy#194
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 21:42:02 +13:00
2799718951 fix: open and close search modal (#192)
Modal now opens on badge click.
Modal now closes on random click around

Reviewed-on: enviPath/enviPy#192
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-12 20:53:52 +13:00
305fdc41fb [Fix] Replace datetime.now() with Djangos timezone.now() to get rid of NaiveTimestamp warning (#191)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#191
2025-11-12 11:04:00 +13:00
9deca8867e [Feature] Possibility to Retrain Models (#190)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#190
2025-11-12 10:28:35 +13:00
161 changed files with 16192 additions and 9943 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"Creating models: {options['model_names']}\n"
f"Data packages: {options['data_packages']}\n" f"Data packages: {options['data_packages']}\n"
f"Rule Packages (only for MLRR): {options['rule_packages']}\n" f"Rule Packages (only for MLRR): {options['rule_packages']}\n"
f"Eval Packages: {options['eval_packages']}\n" f"Eval Packages: {options['eval_packages']}\n"
f"Threshold: {options['threshold']:.2f}") 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,7 +94,7 @@ 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']}.",
@ -101,7 +105,7 @@ 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']}.",

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

@ -1,19 +1,22 @@
import csv import csv
import io import io
import logging import logging
from datetime import datetime
from typing import Any, Callable, List, Optional from typing import Any, Callable, List, Optional
from uuid import uuid4 from uuid import uuid4
from celery import shared_task from celery import shared_task
from celery.utils.functional import LRUCache from celery.utils.functional import LRUCache
from django.conf import settings as s
from django.utils import timezone
from epdb.logic import SPathway from epdb.logic import SPathway
from epdb.models import EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User, Edge from epdb.models import Edge, EPModel, JobLog, Node, 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:
@ -29,7 +32,7 @@ def dispatch_eager(user: "User", job: Callable, *args, **kwargs):
log.task_id = uuid4() log.task_id = uuid4()
log.job_name = job.__name__ log.job_name = job.__name__
log.status = "SUCCESS" log.status = "SUCCESS"
log.done_at = datetime.now() log.done_at = timezone.now()
log.task_result = str(x) if x else None log.task_result = str(x) if x else None
log.save() log.save()

View File

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

View File

@ -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):
@ -374,6 +437,22 @@ def predict_pathway(request):
return render(request, "predict_pathway.html", context) return render(request, "predict_pathway.html", context)
@package_permission_required()
def package_predict_pathway(request, package_uuid):
"""Package-specific predict pathway view."""
if request.method != "GET":
return HttpResponseNotAllowed(["GET"])
current_user = _anonymous_or_real(request)
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
context = get_base_context(request)
context["title"] = f"enviPath - {current_package.name} - Predict Pathway"
context["meta"]["current_package"] = current_package
return render(request, "predict_pathway.html", context)
def packages(request): def packages(request):
current_user = _anonymous_or_real(request) current_user = _anonymous_or_real(request)
@ -856,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)
@ -890,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:
@ -955,6 +1035,12 @@ def package_model(request, package_uuid, model_uuid):
] ]
dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids) dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids)
return redirect(current_model.url)
elif hidden == "retrain":
from .tasks import dispatch, retrain
dispatch(current_user, retrain, current_model.pk)
return redirect(current_model.url) return redirect(current_model.url)
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -1046,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")
@ -2387,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
@ -124,6 +110,14 @@ 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,14 +1,21 @@
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",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
},
body: new URLSearchParams({node: url})
})
.then(response => response.json())
.then(data => {
console.log("Success:", data); console.log("Success:", data);
window.location.href = data.success; window.location.href = data.success;
}) })
.fail(function (xhr, status, error) { .catch(error => {
console.error("Error:", xhr.status, xhr.responseText); console.error("Error:", error);
// show user-friendly message or log error
}); });
} }
@ -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;
opacity: 0;
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;
} }
}, 100); #custom-popover a {
}, popushowAtLeast); color: #2563eb;
text-decoration: none;
} }
}, popupWaitBeforeShow); #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,10 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_compound_modal"> <a
<span class="glyphicon glyphicon-plus"></span> New Compound</a> role="button"
onclick="document.getElementById('new_compound_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Compound</a
>
</li> </li>
{% endif %} {% endif %}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,10 @@
{% 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
<span class="glyphicon glyphicon-plus"></span> New Model</a> role="button"
onclick="document.getElementById('new_model_modal').showModal(); return false;"
>
<span class="glyphicon glyphicon-plus"></span> New Model</a
>
</li> </li>
{% endif %} {% endif %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,59 +1,104 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a class="button" data-toggle="modal" data-target="#add_pathway_node_modal"> <a
<i class="glyphicon glyphicon-plus"></i> Add Compound</a> class="button"
onclick="document.getElementById('add_pathway_node_modal').showModal(); return false;"
>
<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
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a> class="button"
onclick="document.getElementById('add_pathway_edge_modal').showModal(); return false;"
>
<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
<i class="glyphicon glyphicon-duplicate"></i> Copy</a> role="button"
</li> onclick="document.getElementById('generic_copy_object_modal').showModal(); return false;"
<li> >
<a class="button" data-toggle="modal" data-target="#download_pathway_csv_modal"> <i class="glyphicon glyphicon-duplicate"></i> Copy</a
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as CSV</a> >
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#download_pathway_image_modal"> <a
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as Image</a> class="button"
</li> onclick="document.getElementById('download_pathway_csv_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as CSV</a
>
</li>
<li>
<a
class="button"
onclick="document.getElementById('download_pathway_image_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as Image</a
>
</li>
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a class="button" data-toggle="modal" data-target="#identify_missing_rules_modal"> <a
<i class="glyphicon glyphicon-question-sign"></i> Identify Missing Rules</a> class="button"
onclick="document.getElementById('identify_missing_rules_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-question-sign"></i> Identify Missing
Rules</a
>
</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
<i class="glyphicon glyphicon-edit"></i> Edit Pathway</a> class="button"
onclick="document.getElementById('edit_pathway_modal').showModal(); return false;"
>
<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
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a> role="button"
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
>
<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
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a> role="button"
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
>
<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 class="button" data-toggle="modal" data-target="#delete_pathway_node_modal"> <a
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a> class="button"
onclick="document.getElementById('delete_pathway_node_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a
>
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#delete_pathway_edge_modal"> <a
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a> class="button"
onclick="document.getElementById('delete_pathway_edge_modal').showModal(); return false;"
>
<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
<i class="glyphicon glyphicon-trash"></i> Delete Pathway</a> class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Pathway</a
>
</li> </li>
{% endif %} {% endif %}

View File

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

View File

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

View File

@ -1,14 +1,26 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a class="button" data-toggle="modal" data-target="#add_additional_information_modal"> <a
<i class="glyphicon glyphicon-trash"></i> Add Additional Information</a> class="button"
onclick="document.getElementById('add_additional_information_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Add Additional Information</a
>
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#update_scenario_additional_information_modal"> <a
<i class="glyphicon glyphicon-trash"></i> Set Additional Information</a> class="button"
onclick="document.getElementById('update_scenario_additional_information_modal').showModal(); return false;"
>
<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
<i class="glyphicon glyphicon-trash"></i> Delete Scenario</a> class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Scenario</a
>
</li> </li>
{% endif %} {% endif %}

View File

@ -1,22 +1,38 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_user_modal"> <a
<i class="glyphicon glyphicon-edit"></i> Update</a> role="button"
onclick="document.getElementById('edit_user_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-edit"></i> Update</a
>
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#edit_password_modal"> <a
<i class="glyphicon glyphicon-lock"></i> Update Password</a> role="button"
onclick="document.getElementById('edit_password_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-lock"></i> Update Password</a
>
</li> </li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#new_prediction_setting_modal"> <a
<i class="glyphicon glyphicon-plus"></i> New Prediction Setting</a> role="button"
onclick="document.getElementById('new_prediction_setting_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-plus"></i> New Prediction Setting</a
>
</li> </li>
{# <li>#} {# <li>#}
{# <a role="button" data-toggle="modal" data-target="#manage_api_token_modal">#} {# <a role="button" data-toggle="modal" data-target="#manage_api_token_modal">#}
{# <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
<i class="glyphicon glyphicon-trash"></i> Delete Account</a> class="button"
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
>
<i class="glyphicon glyphicon-trash"></i> Delete Account</a
>
</li> </li>
{% endif %} {% endif %}

View File

@ -1,12 +1,17 @@
{% extends "framework.html" %} {% extends "framework.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div id="searchContent">
<div id=searchContent>
<form id="admin-form" action="{{ SERVER_BASE }}/admin" method="post"> <form id="admin-form" action="{{ SERVER_BASE }}/admin" method="post">
<div class="form-group"> <div class="form-group">
<label for="textarea">Query</label> <label for="textarea">Query</label>
<textarea id="textarea" class="form-control" rows="10" placeholder="Paste query here" required> <textarea
id="textarea"
class="form-control"
rows="10"
placeholder="Paste query here"
required
>
PREFIX pps: <http://localhost:8080/vocabulary#> PREFIX pps: <http://localhost:8080/vocabulary#>
SELECT ?name (count(?objId) as ?xcnt) SELECT ?name (count(?objId) as ?xcnt)
WHERE { WHERE {
@ -15,32 +20,29 @@ WHERE {
?packageId pps:reviewStatus 'reviewed' . ?packageId pps:reviewStatus 'reviewed' .
?packageId pps:pathway ?objId . ?packageId pps:pathway ?objId .
} GROUP BY ?name } GROUP BY ?name
</textarea> </textarea
>
</div> </div>
<button id="submit" type="button" class="btn btn-primary">Submit</button> <button id="submit" type="button" class="btn btn-primary">Submit</button>
</form> </form>
<p></p> <p></p>
</div> </div>
<div id="results"> <div id="results"></div>
</div> <div id="loading"></div>
<div id="loading"></div> <script>
</div> $(function () {
<script> $("#submit").on("click", function () {
$(function() {
$('#submit').on('click', function() {
makeLoadingGif("#loading", "{% static '/images/wait.gif' %}"); makeLoadingGif("#loading", "{% static '/images/wait.gif' %}");
data = { data = {
"query": $("#textarea").val() query: $("#textarea").val(),
} };
$.post("{{ SERVER_BASE }}/expire", data, function(result) { $.post("{{ SERVER_BASE }}/expire", data, function (result) {
$("#loading").empty(); $("#loading").empty();
queryResultToTable("results", result); queryResultToTable("results", result);
})
}); });
}) });
</script> });
</script>
{% endblock content %} {% endblock content %}

View File

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

View File

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

View File

@ -4,16 +4,32 @@
<div> <div>
<form action="" method="post"> <form action="" method="post">
{% csrf_token %} {% csrf_token %}
<input type="text" class="form-control" id="smiles" name="smiles" placeholder="SMILES" <input
value="{{ smiles }}"/> type="text"
<input type="text" class="form-control" id="smiles" name="smirks" placeholder="SMIRKS" class="form-control"
value="{{ smirks }}"/> id="smiles"
name="smiles"
placeholder="SMILES"
value="{{ smiles }}"
/>
<input
type="text"
class="form-control"
id="smiles"
name="smirks"
placeholder="SMIRKS"
value="{{ smirks }}"
/>
<button type="submit" class="btn btn-primary">Test</button> <button type="submit" class="btn btn-primary">Test</button>
</form> </form>
</div> </div>
{% if result %} {% if result %}
{{ smiles }}<p></p> {{ smiles }}
<img width='400' src='{% url 'depict' %}?smiles={{ smiles|urlencode }}'><br> <p></p>
<img
width="400"
src="{% url 'depict' %}?smiles={{ smiles|urlencode }}"
/><br />
<p></p> <p></p>
{% if rule %} {% if rule %}
{{ smirks }} {{ smirks }}
@ -22,9 +38,7 @@
<p></p> <p></p>
{{ rule.products_smarts }} {{ rule.products_smarts }}
<p></p> <p></p>
<div> <div>{{ rule.as_svg|safe }}</div>
{{ rule.as_svg|safe }}
</div>
{% endif %} {% endif %}
<h2>Diff</h2> <h2>Diff</h2>
{% if diff %} {% if diff %}
@ -38,16 +52,22 @@
<div class="col-md-6"> <div class="col-md-6">
<h2>Ambit</h2> <h2>Ambit</h2>
{% for p in ambit_res %} {% for p in ambit_res %}
{{ p }}<br> {{ p }}<br />
<img width='400' src='{% url 'depict' %}?smiles={{ p|urlencode }}'><br> <img
width="400"
src="{% url 'depict' %}?smiles={{ p|urlencode }}"
/><br />
{% endfor %} {% endfor %}
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
<h2>RDKit</h2> <h2>RDKit</h2>
{% for p in rdkit_res %} {% for p in rdkit_res %}
{{ p }}<br> {{ p }}<br />
<img width='400' src='{% url 'depict' %}?smiles={{ p|urlencode }}'><br> <img
width="400"
src="{% url 'depict' %}?smiles={{ p|urlencode }}"
/><br />
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -1,15 +1,77 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<div class="w-full max-w-2xl">
<div class="alert alert-error mb-6 shadow-lg">
<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="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="alert alert-error" role="alert"> <div class="card bg-base-100 shadow-xl">
<h4 class="alert-heading">Bad Request!</h4> <div class="card-body">
<p>Lorem</p> <h2 class="card-title mb-4 text-2xl">What happened?</h2>
<hr> <p class="text-base-content/70 mb-4">
<p class="mb-0"> The server couldn't process your request because it contains invalid
You can find out more about permissions in our <a target="_blank" 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" href="https://wiki.envipath.org/index.php/packages"
role="button">Wiki &gt;&gt;</a></p> target="_blank"
</div> 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>
{% endblock content %} {% endblock content %}

View File

@ -1,15 +1,80 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<div class="w-full max-w-2xl">
<div class="alert alert-warning mb-6 shadow-lg">
<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="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="alert alert-error" role="alert"> <div class="card bg-base-100 shadow-xl">
<h4 class="alert-heading">Access Denied!</h4> <div class="card-body">
<p>Access to X denied. </p> <h2 class="card-title mb-4 text-2xl">Permission Required</h2>
<hr> <p class="text-base-content/70 mb-4">
<p class="mb-0"> You need the appropriate permissions to access this content. If you
You can find out more about permissions in our <a target="_blank" 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" href="https://wiki.envipath.org/index.php/packages"
role="button">Wiki &gt;&gt;</a></p> target="_blank"
</div> 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>
{% endblock content %} {% endblock content %}

View File

@ -1,15 +1,77 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<div class="w-full max-w-2xl">
<div class="alert alert-info mb-6 shadow-lg">
<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="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="alert alert-error" role="alert"> <div class="card bg-base-100 shadow-xl">
<h4 class="alert-heading">Not Found!</h4> <div class="card-body">
<p>Does not exist</p> <h2 class="card-title mb-4 text-2xl">404 Error</h2>
<hr> <p class="text-base-content/70 mb-4">
<p class="mb-0"> The page or resource you requested could not be found. It may have
You can find out more about permissions in our <a target="_blank" 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" href="https://wiki.envipath.org/index.php/packages"
role="button">Wiki &gt;&gt;</a></p> target="_blank"
</div> 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>
{% endblock content %} {% endblock content %}

View File

@ -1,13 +1,76 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<div class="alert alert-danger" role="alert"> <div class="w-full max-w-2xl">
<h4 class="alert-heading">{{ error_message }}</h4> <div class="alert alert-error mb-6 shadow-lg">
<hr> <svg
<p class="mb-0"> xmlns="http://www.w3.org/2000/svg"
{{ error_detail }} 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> </p>
</div> </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>
{% endblock content %} {% endblock content %}

View File

@ -1,12 +1,81 @@
{% extends "framework.html" %} {% extends "framework_modern.html" %}
{% load static %} {% load static %}
{% block content %} {% block content %}
<div class="flex min-h-[60vh] flex-col items-center justify-center p-8">
<div class="alert alert-danger" role="alert"> <div class="w-full max-w-2xl">
<h4 class="alert-heading">Your account has not been activated yet!</h4> <div class="alert alert-warning mb-6 shadow-lg">
<p>Your account has not been activated yet. If you have questions <a href="mailto:admin@envipath.org">contact <svg
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>
<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>
{% endblock content %} {% endblock content %}

View File

@ -124,7 +124,7 @@
<!-- Collect the nav links, forms, and other content for toggling --> <!-- Collect the nav links, forms, and other content for toggling -->
<div <div
class="collapse navbar-collapse collapse-framework navbar-collapse-framework" class="navbar-collapse collapse-framework navbar-collapse-framework collapse"
id="navbarCollapse" id="navbarCollapse"
> >
<ul class="nav navbar-nav navbar-nav-framework"> <ul class="nav navbar-nav navbar-nav-framework">
@ -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>
@ -338,7 +325,7 @@
> >
</div> </div>
</div> </div>
<div id="license" class="panel-collapse collapse in"> <div id="license" class="panel-collapse in collapse">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
<a target="_blank" href="{{ meta.current_package.license.link }}"> <a target="_blank" href="{{ meta.current_package.license.link }}">
<img src="{{ meta.current_package.license.image_link }}" /> <img src="{{ meta.current_package.license.image_link }}" />
@ -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 -->
@ -80,7 +75,7 @@
{% block main_content %} {% block main_content %}
{# Breadcrumbs - outside main content, optional #} {# Breadcrumbs - outside main content, optional #}
{% if breadcrumbs %} {% if breadcrumbs %}
<div id="bread" class="max-w-7xl mx-auto px-4 py-4"> <div id="bread" class="mx-auto max-w-7xl px-4 py-4">
<div class="breadcrumbs text-sm"> <div class="breadcrumbs text-sm">
<ul> <ul>
{% for elem in breadcrumbs %} {% for elem in breadcrumbs %}
@ -113,7 +108,7 @@
{# License - inside paper if present #} {# License - inside paper if present #}
{% if meta.url_contains_package and meta.current_package.license %} {% if meta.url_contains_package and meta.current_package.license %}
<div class="collapse collapse-arrow bg-base-200 m-8"> <div class="collapse-arrow bg-base-200 collapse p-8">
<input type="checkbox" checked /> <input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">License</div> <div class="collapse-title text-xl font-medium">License</div>
<div class="collapse-content"> <div class="collapse-content">
@ -137,7 +132,7 @@
{# Floating Help Tab #} {# Floating Help Tab #}
{% if not public_mode %} {% if not public_mode %}
<div class="fixed right-0 top-1/2 -translate-y-1/2 z-50"> <div class="fixed top-1/2 right-0 z-50 -translate-y-1/2">
<a <a
href="https://community.envipath.org/" href="https://community.envipath.org/"
target="_blank" target="_blank"
@ -171,13 +166,31 @@
{% 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 = "";
} }
}); });
// Open search modal function
function openSearchModal() {
const searchModal = document.getElementById("search_modal");
if (searchModal) {
searchModal.showModal();
}
}
// Click handler for search badge
const searchTrigger = document.getElementById("search-trigger");
if (searchTrigger) {
searchTrigger.addEventListener("click", function (event) {
event.preventDefault();
openSearchModal();
});
}
// Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux) // Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux)
document.addEventListener("keydown", function (event) { document.addEventListener("keydown", function (event) {
// Check if user is typing in an input field // Check if user is typing in an input field
@ -198,7 +211,7 @@
if (isCorrectModifier && event.key === "k") { if (isCorrectModifier && event.key === "k") {
event.preventDefault(); event.preventDefault();
search_modal.showModal(); openSearchModal();
} }
}); });
</script> </script>

View File

@ -1,17 +1,20 @@
{% load static %} {% load static %}
<div class="lg:max-w-5xl mt-10 mx-auto bg-base-300 text-base-content"> <div class="bg-base-300 text-base-content mx-auto mt-10 lg:max-w-5xl">
<footer class="footer sm:footer-horizontal p-10"> <footer class="footer sm:footer-horizontal p-10">
{% if not public_mode %} {% if not public_mode %}
<nav> <nav>
<h6 class="footer-title">Services</h6> <h6 class="footer-title">Services</h6>
<a class="link link-hover" href="/">Predict</a> <a class="link link-hover" href="/predict">Predict</a>
<a class="link link-hover" href="/search">Search</a> <a class="link link-hover" href="/package">Packages</a>
<a class="link link-hover" href="/package">Browse</a>
{% if user.is_authenticated %} {% if user.is_authenticated %}
<a class="link link-hover" href="/model">Your Collections</a> <a class="link link-hover" href="/model">Your Collections</a>
{% endif %} {% endif %}
<a href="https://wiki.envipath.org/" target="_blank" class="link link-hover">Documentation</a> <a
href="https://wiki.envipath.org/"
target="_blank"
class="link link-hover"
>Documentation</a
>
</nav> </nav>
{% endif %} {% endif %}
<nav> <nav>
@ -29,39 +32,61 @@
<a class="link link-hover" href="/cite">Cite enviPath</a> <a class="link link-hover" href="/cite">Cite enviPath</a>
</nav> </nav>
</footer> </footer>
<footer class="footer border-neutral-300 border-t-2 px-10 py-4"> <footer class="footer border-t-2 border-neutral-300 px-10 py-4">
<div class="flex flex-row justify-between w-full items-start"> <div class="flex w-full flex-row items-start justify-between">
<aside class="grid-flow-col items-center"> <aside class="grid-flow-col items-center">
<svg class="fill-neutral-content flex-shrink-0 h-14 m-2" viewbox="0 0 65 65" > <svg
class="fill-neutral-content m-2 h-14 flex-shrink-0"
viewbox="0 0 65 65"
>
<use <use
href="{% static "/images/logo-square.svg" %}#ep-logo-square" href="{% static "/images/logo-square.svg" %}#ep-logo-square"
> ></use>
</use>
</svg> </svg>
enviPath Ltd. enviPath Ltd.
<br /> <br />
Biodegredation prediction since 2015. Biodegredation prediction since 2015.
</p>
</aside> </aside>
<aside class="text-sm text-base-200 mt-2"><span class="text-xs tracking-tight">Version</span> <span class="text-base font-bold">{{ meta.version }}</span></aside> <aside class="text-base-200 mt-2 text-sm">
<span class="text-xs tracking-tight">Version</span>
<span class="text-base font-bold">{{ meta.version }}</span>
</aside>
</div> </div>
<nav class="md:place-self-center md:justify-self-end"> <nav class="md:place-self-center md:justify-self-end">
<div class="grid grid-flow-col gap-4"> <div class="grid grid-flow-col gap-4">
<a href="https://www.youtube.com/@envipath7231" target="_blank"> <a href="https://www.youtube.com/@envipath7231" target="_blank">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 fill-current"> <svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 fill-current"
>
<title>YouTube</title> <title>YouTube</title>
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/> <path
d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"
/>
</svg> </svg>
</a> </a>
<a href="https://community.envipath.org/" target="_blank"> <a href="https://community.envipath.org/" target="_blank">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-6 h-6 fill-current"> <svg
role="img"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6 fill-current"
>
<title>Discourse</title> <title>Discourse</title>
<path d="M12.103 0C18.666 0 24 5.485 24 11.997c0 6.51-5.33 11.99-11.9 11.99L0 24V11.79C0 5.28 5.532 0 12.103 0zm.116 4.563c-2.593-.003-4.996 1.352-6.337 3.57-1.33 2.208-1.387 4.957-.148 7.22L4.4 19.61l4.794-1.074c2.745 1.225 5.965.676 8.136-1.39 2.17-2.054 2.86-5.228 1.737-7.997-1.135-2.778-3.84-4.59-6.84-4.585h-.008z"/> <path
d="M12.103 0C18.666 0 24 5.485 24 11.997c0 6.51-5.33 11.99-11.9 11.99L0 24V11.79C0 5.28 5.532 0 12.103 0zm.116 4.563c-2.593-.003-4.996 1.352-6.337 3.57-1.33 2.208-1.387 4.957-.148 7.22L4.4 19.61l4.794-1.074c2.745 1.225 5.965.676 8.136-1.39 2.17-2.054 2.86-5.228 1.737-7.997-1.135-2.778-3.84-4.59-6.84-4.585h-.008z"
/>
</svg> </svg>
</a> </a>
<a href="https://www.linkedin.com/company/envipath/" target="_blank"> <a href="https://www.linkedin.com/company/envipath/" target="_blank">
<img src="{% static "/images/linkedin.png" %}" alt="LinkedIn" class="w-6 h-6"> <img
src="{% static "/images/linkedin.png" %}"
alt="LinkedIn"
class="h-6 w-6"
/>
</a> </a>
</div> </div>
</nav> </nav>

View File

@ -1,15 +1,44 @@
{% load static %} {% load static %}
{# Modern DaisyUI Navbar #} {# Modern DaisyUI Navbar with Mobile Drawer Menu #}
<div class="navbar bg-neutral-50 text-neutral-950 shadow-lg x-50"> <div class="drawer drawer-mobile">
<input id="drawer-toggle" type="checkbox" class="drawer-toggle" />
<div class="drawer-content flex flex-col">
{# Navbar #}
<div class="navbar x-50 bg-neutral-50 text-neutral-950 shadow-lg">
<div class="navbar-start"> <div class="navbar-start">
<a href="{{ meta.server_url }}" class="btn btn-ghost normal-case text-xl"> {# Hamburger menu button - visible on mobile, hidden on desktop #}
<svg class="h-8 fill-base-content" viewBox="0 0 104 26" role="img"> {% if not public_mode %}
<label
for="drawer-toggle"
class="btn btn-square btn-ghost drawer-button lg:hidden"
>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="inline-block h-5 w-5 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M4 6h16M4 12h16M4 18h16"
></path>
</svg>
</label>
{% endif %}
<a
href="{{ meta.server_url }}"
class="btn btn-ghost text-xl normal-case"
>
<svg class="fill-base-content h-8" viewBox="0 0 104 26" role="img">
<use href="{% static "/images/logo-name.svg" %}#ep-logo-name" /> <use href="{% static "/images/logo-name.svg" %}#ep-logo-name" />
</svg> </svg>
</a> </a>
</div> </div>
{% if not public_mode %} {% if not public_mode %}
{# Desktop menu - hidden on mobile, visible on desktop #}
<div class="navbar-center hidden lg:flex"> <div class="navbar-center hidden lg:flex">
<a <a
href="{{ meta.server_url }}/predict" href="{{ meta.server_url }}/predict"
@ -18,8 +47,6 @@
id="predictLink" id="predictLink"
>Predict</a >Predict</a
> >
<!-- <li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li> -->
<!--<li><a href="{{ meta.server_url }}/browse" id="browseLink">Browse</a></li>-->
<div class="dropdown dropdown-center"> <div class="dropdown dropdown-center">
<div tabindex="0" role="button" class="btn btn-ghost">Browse</div> <div tabindex="0" role="button" class="btn btn-ghost">Browse</div>
<ul <ul
@ -27,9 +54,18 @@
class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm" class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm"
> >
<li> <li>
<a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a> <a href="{{ meta.server_url }}/package" id="packageLink"
>Package</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>
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li>
<li> <li>
<a href="{{ meta.server_url }}/compound" id="compoundLink" <a href="{{ meta.server_url }}/compound" id="compoundLink"
>Compound</a >Compound</a
@ -41,7 +77,9 @@
> >
</li> </li>
<li> <li>
<a href="{{ meta.server_url }}/model" id="relative-reasoningLink" <a
href="{{ meta.server_url }}/model"
id="relative-reasoningLink"
>Model</a >Model</a
> >
</li> </li>
@ -57,9 +95,9 @@
<div class="navbar-end"> <div class="navbar-end">
{% if not public_mode %} {% if not public_mode %}
<a href="/search" role="button"> <a id="search-trigger" role="button" class="cursor-pointer">
<div <div
class="flex items-center badge badge-dash space-x-1 bg-base-200 text-base-content/50 p-2 m-1" class="badge badge-dash bg-base-200 text-base-content/50 m-1 flex items-center space-x-1 p-2"
> >
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -87,7 +125,7 @@
<div <div
tabindex="0" tabindex="0"
role="button" role="button"
class="btn btn-ghost m-1 btn-circle" class="btn btn-ghost btn-circle m-1"
id="loggedInButton" id="loggedInButton"
> >
<svg <svg
@ -111,7 +149,9 @@
tabindex="-1" tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-sm" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-sm"
> >
<li><a href="{{ meta.user.url }}" id="accountbutton">Settings</a></li> <li>
<a href="{{ meta.user.url }}" id="accountbutton">Settings</a>
</li>
<li> <li>
<form <form
id="logoutForm" id="logoutForm"
@ -133,6 +173,96 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
</div>
</div>
{# Mobile drawer menu - slides in from the left #}
<div class="drawer-side">
<label for="drawer-toggle" class="drawer-overlay"></label>
<ul class="menu min-h-full w-80 bg-base-200 p-4 text-base-content">
{# Drawer header with close button #}
<li class="mb-4">
<div class="flex items-center justify-between">
<span class="font-bold text-lg">Menu</span>
<label
for="drawer-toggle"
class="btn btn-sm btn-circle btn-ghost"
aria-label="Close menu"
>
<svg
xmlns="http://www.w3.org/2000/svg"
class="h-6 w-6"
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>
</li>
{% if not public_mode %}
{# Predict link #}
<li>
<a
href="{{ meta.server_url }}/predict"
class="text-lg"
id="predictLinkMobile"
>Predict</a
>
</li>
{# Browse menu with submenu #}
<li>
<details>
<summary class="text-lg">Browse</summary>
<ul>
<li>
<a href="{{ meta.server_url }}/package" id="packageLinkMobile"
>Package</a
>
</li>
<li>
<a href="{{ meta.server_url }}/pathway" id="pathwayLinkMobile"
>Pathway</a
>
</li>
<li>
<a href="{{ meta.server_url }}/rule" id="ruleLinkMobile"
>Rule</a
>
</li>
<li>
<a href="{{ meta.server_url }}/compound" id="compoundLinkMobile"
>Compound</a
>
</li>
<li>
<a href="{{ meta.server_url }}/reaction" id="reactionLinkMobile"
>Reaction</a
>
</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>
<script> <script>

View File

@ -2,36 +2,90 @@
{% load static %} {% load static %}
{% block main_content %} {% block main_content %}
<!-- Hero Section with Logo and Search --> <!-- Hero Section with Logo and Search -->
<section class="hero h-fit max-w-5xl w-full shadow-none mx-auto relative"> <section class="hero relative mx-auto h-fit w-full max-w-5xl shadow-none">
<div <div
class="hero min-h-[calc(100vh*0.4)] bg-gradient-to-br from-primary-800 to-primary-600" class="hero from-primary-800 to-primary-600 min-h-[calc(100vh*0.4)] bg-gradient-to-br"
style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;" style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;"
> >
<div class="hero-overlay"></div> <div class="hero-overlay"></div>
<!-- Predict Pathway text over the image --> <!-- Predict Pathway text over the image -->
<div class="absolute bottom-40 left-1/8 -translate-x-8 z-10"> <div class="absolute bottom-40 left-1/8 -translate-x-8">
<h2 class="text-3xl text-base-100 text-shadow-lg text-left"> <h2 class="text-base-100 text-left text-3xl text-shadow-lg">
Predict Your Pathway Predict Your Pathway
</h2> </h2>
</div> </div>
</div> </div>
</section> </section>
<div class="shadow-md max-w-5xl mx-auto bg-base-200"> <div class="bg-base-200 mx-auto max-w-5xl shadow-md">
<!-- Predict Pathway Section --> <!-- Predict Pathway Section -->
<div <div
class="flex-col lg:flex-row-reverse w-full mx-auto -mt-32 relative z-20 mb-10 " class="relative mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
> >
<div <div
class="card bg-base-100 shrink-0 shadow-xl w-3/4 mx-auto transition-all duration-300 ease-in-out" class="card bg-base-100 mx-auto w-3/4 shrink-0 shadow-xl transition-all duration-300 ease-in-out"
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">
<!-- Input Mode Toggle - Fixed position outside fieldset --> <div class="my-4 ml-8 flex h-fit flex-row items-center justify-start">
<div class="flex flex-row justify-start items-center h-fit ml-8 my-4"> <div class="flex items-center gap-1">
<div class="flex items-center gap-2"> <label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
<!-- <span class="text-sm text-neutral-500">Input Mode:</span> --> <input type="checkbox" x-model="drawMode" />
<label class="toggle text-base-content toggle-md"> <span class="swap-on flex items-center gap-1">
<input type="checkbox" /> <div
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
>
<svg <svg
aria-label="smiles mode" aria-label="smiles mode"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -52,6 +106,13 @@
/> />
</g> </g>
</svg> </svg>
</div>
<span class="ext-xs">SMILES</span>
</span>
<span class="swap-off flex items-center gap-1">
<div
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
>
<svg <svg
aria-label="draw mode" aria-label="draw mode"
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
@ -64,57 +125,78 @@
d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z"
/> />
</svg> </svg>
</div>
<span class="text-base/50 text-xs">Draw</span>
</span>
</label> </label>
</div> </div>
</div> </div>
<fieldset <fieldset
class="fieldset transition-all duration-300 ease-in-out overflow-hidden" class="fieldset overflow-hidden transition-all duration-300 ease-in-out"
: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="transition-all duration-300 ease-in-out opacity-100 transform scale-100" 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 w-full mx-auto"> <div class="join mx-auto w-full">
<input <input
type="text" type="text"
id="index-form-text-input" id="index-form-text-input"
placeholder="canonical SMILES string" placeholder="canonical SMILES string"
class="input grow input-md join-item" class="input input-md join-item grow"
x-model="smiles"
/> />
<button class="btn btn-neutral join-item">Predict!</button> <button class="btn btn-neutral join-item">Predict!</button>
</div> </div>
<div class="label relative w-full mt-1"> <div class="label relative mt-1 w-full">
<div class="flex gap-2"> <div class="flex gap-2">
<a <a
href="#" href="#"
class="example-link cursor-pointer hover:text-primary" class="example-link hover:text-primary cursor-pointer"
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
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 cursor-pointer hover:text-primary" class="example-link hover:text-primary cursor-pointer"
data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
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 transition-all duration-300 ease-in-out opacity-0 transform scale-95" 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"
@ -124,11 +206,13 @@
class="rounded-lg" class="rounded-lg"
></iframe> ></iframe>
<button <button
class="btn btn-lg bg-primary-950 text-primary-50 join-item w-full mt-2" class="btn btn-lg bg-primary-950 text-primary-50 join-item mt-2 w-full"
> >
Predict! Predict!
</button> </button>
<a class="label mx-auto w-full mt-1" href="#">Advanced</a> <div class="mt-1 flex w-full justify-end">
<a class="label justify-end" href="/predict">Advanced</a>
</div>
</div> </div>
<input <input
type="hidden" type="hidden"
@ -150,18 +234,18 @@
</div> </div>
<!-- Community News Section --> <!-- Community News Section -->
<section class="py-16 bg-base-200 z-10 mx-8"> <section class="bg-base-200 z-10 mx-8 py-16">
<div class="max-w-7xl mx-auto px-4"> <div class="mx-auto max-w-7xl px-4">
<h2 class="h2 font-bold text-left mb-8">Community Updates</h2> <h2 class="h2 mb-8 text-left font-bold">Community Updates</h2>
<div id="community-news-container" class="flex gap-4 justify-center"> <div id="community-news-container" class="flex justify-center gap-4">
<!-- News cards will be populated here --> <!-- News cards will be populated here -->
<div id="loading" class="flex justify-center w-full"> <div id="loading" class="flex w-full justify-center">
<span class="loading loading-spinner loading-lg"></span> <span class="loading loading-spinner loading-lg"></span>
</div> </div>
</div> </div>
<div class="text-right mt-6"> <div class="mt-6 text-right">
<a <a
href="https://community.envipath.org/c/announcements/10" href="https://community.envipath.org/c/announcements/10"
target="_blank" target="_blank"
@ -177,18 +261,18 @@
</section> </section>
<!-- Mission Statement Section --> <!-- Mission Statement Section -->
<section class="py-16 from-base-200 to-base-100 bg-gradient-to-b"> <section class="from-base-200 to-base-100 bg-gradient-to-b py-16">
<div class="mx-auto px-8 md:px-12"> <div class="mx-auto px-8 md:px-12">
<div class="flex flex-row gap-4"> <div class="flex flex-row gap-4">
<div class="w-1/3"> <div class="w-1/3">
<img <img
src="{% static "/images/ep-rule-artwork.png" %}" src="{% static "/images/ep-rule-artwork.png" %}"
alt="rule-based iterative tree greneration" alt="rule-based iterative tree greneration"
class="w-full h-full object-contain" class="h-full w-full object-contain"
/> />
</div> </div>
<div class="space-y-4 text-left w-2/3 mr-8"> <div class="mr-8 w-2/3 space-y-4 text-left">
<h2 class="h2 font-bold mb-8">About enviPath</h2> <h2 class="h2 mb-8 font-bold">About enviPath</h2>
<p class=""> <p class="">
enviPath is a database and prediction system for the microbial enviPath is a database and prediction system for the microbial
biotransformation of organic environmental contaminants. The biotransformation of organic environmental contaminants. The
@ -201,7 +285,7 @@
products. Explore our tools and contribute to advancing products. Explore our tools and contribute to advancing
environmental biotransformation research. environmental biotransformation research.
</p> </p>
<div class="flex flex-row gap-4 float-right"> <div class="float-right flex flex-row gap-4">
<a href="/about" class="btn btn-ghost-neutral">Read More</a> <a href="/about" class="btn btn-ghost-neutral">Read More</a>
<a href="/about" class="btn btn-neutral">Publications</a> <a href="/about" class="btn btn-neutral">Publications</a>
</div> </div>
@ -211,7 +295,7 @@
</section> </section>
<!-- Partners Section --> <!-- Partners Section -->
<section class="py-14 sm:py-12 bg-base-100"> <section class="bg-base-100 py-14 sm:py-12">
<div class="mx-auto px-6 lg:px-8"> <div class="mx-auto px-6 lg:px-8">
<div class="divider"> <div class="divider">
<h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2> <h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2>
@ -222,12 +306,12 @@
<img <img
src="{% static "/images/uoa-logo-small.png" %}" src="{% static "/images/uoa-logo-small.png" %}"
alt="The University of Auckland" alt="The University of Auckland"
class=" max-h-20 w-full object-contain lg:col-span-1" class="max-h-20 w-full object-contain lg:col-span-1"
/> />
<img <img
src="{% static "/images/logo-eawag.svg" %}" src="{% static "/images/logo-eawag.svg" %}"
alt="Eawag" alt="Eawag"
class=" max-h-12 w-full object-contain lg:col-span-1" class="max-h-12 w-full object-contain lg:col-span-1"
/> />
<img <img
src="{% static "/images/uzh-logo.svg" %}" src="{% static "/images/uzh-logo.svg" %}"
@ -242,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
@ -264,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">
@ -299,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,44 +1,71 @@
{% extends "framework.html" %} {% extends "framework.html" %}
{% block content %} {% block content %}
<div class="panel-group" id="migration-detail"> <div class="panel-group" id="migration-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div
class="panel-heading"
id="headingPanel"
style="font-size:2rem;height: 46px"
>
Migration Status BT Rules Migration Status BT Rules
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p>Rules with Error: {{ error }}/{{ total }} </p> <p>Rules with Error: {{ error }}/{{ total }}</p>
<p>Rules without Error: {{ success }}/{{ total }}</p> <p>Rules without Error: {{ success }}/{{ total }}</p>
</div> </div>
{% for obj in results %} {% for obj in results %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
>
{% if obj.status %} {% if obj.status %}
<span class="glyphicon glyphicon-ok" aria-hidden="true" <span
style="float:right" data-toggle="tooltip" class="glyphicon glyphicon-ok"
data-placement="top" title="" data-original-title="Reviewed"> aria-hidden="true"
style="float:right"
data-toggle="tooltip"
data-placement="top"
title=""
data-original-title="Reviewed"
>
</span> </span>
{% else %} {% else %}
<span class="glyphicon glyphicon-remove" aria-hidden="true" <span
style="float:right" data-toggle="tooltip" class="glyphicon glyphicon-remove"
data-placement="top" title="" data-original-title="Reviewed"> aria-hidden="true"
style="float:right"
data-toggle="tooltip"
data-placement="top"
title=""
data-original-title="Reviewed"
>
</span> </span>
{% endif %} {% endif %}
<h4 class="panel-title"> <h4 class="panel-title">
<a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail" <a
href="#{{ obj.id }}">{{ obj.name|safe }}</a> id="{{ obj.id }}-link"
data-toggle="collapse"
data-parent="#migration-detail"
href="#{{ obj.id }}"
>{{ obj.name|safe }}</a
>
</h4> </h4>
</div> </div>
<div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}"> <div
id="{{ obj.id }}"
class="panel-collapse {% if not obj.status %}in{% endif %} collapse"
>
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
<a class="list-group-item" href="{{ obj.detail_url }}">{{ obj.name|safe }} Migration Detail Page</a> <a class="list-group-item" href="{{ obj.detail_url }}"
>{{ obj.name|safe }} Migration Detail Page</a
>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
<script> <script></script>
</script>
{% endblock content %} {% endblock content %}

View File

@ -3,34 +3,68 @@
{% block content %} {% block content %}
<div class="panel-group" id="migration-detail"> <div class="panel-group" id="migration-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div
class="panel-heading"
id="headingPanel"
style="font-size:2rem;height: 46px"
>
Migration Status for {{ bt_rule_name }} Migration Status for {{ bt_rule_name }}
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p>A package contains pathways, rules, etc. and can reflect specific experimental <p>
conditions. <a target="_blank" href="https://wiki.envipath.org/index.php/packages" role="button">Learn A package contains pathways, rules, etc. and can reflect specific
more &gt;&gt;</a></p> experimental conditions.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/packages"
role="button"
>Learn more &gt;&gt;</a
>
</p>
</div> </div>
{% for obj in results %} {% for obj in results %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
>
{% if obj.status %} {% if obj.status %}
<span class="glyphicon glyphicon-ok" aria-hidden="true" <span
style="float:right" data-toggle="tooltip" class="glyphicon glyphicon-ok"
data-placement="top" title="" data-original-title="Reviewed"> aria-hidden="true"
style="float:right"
data-toggle="tooltip"
data-placement="top"
title=""
data-original-title="Reviewed"
>
</span> </span>
{% else %} {% else %}
<span class="glyphicon glyphicon-remove" aria-hidden="true" <span
style="float:right" data-toggle="tooltip" class="glyphicon glyphicon-remove"
data-placement="top" title="" data-original-title="Reviewed"> aria-hidden="true"
style="float:right"
data-toggle="tooltip"
data-placement="top"
title=""
data-original-title="Reviewed"
>
</span> </span>
{% endif %} {% endif %}
<h4 class="panel-title"> <h4 class="panel-title">
<a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail" <a
href="#{{ obj.id }}">{{ obj.name|safe }}</a> id="{{ obj.id }}-link"
data-toggle="collapse"
data-parent="#migration-detail"
href="#{{ obj.id }}"
>{{ obj.name|safe }}</a
>
</h4> </h4>
</div> </div>
<div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}"> <div
id="{{ obj.id }}"
class="panel-collapse {% if not obj.status %}in{% endif %} collapse"
>
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
<pre>{{ obj.detail }}</pre> <pre>{{ obj.detail }}</pre>
</div> </div>
@ -39,7 +73,5 @@
</div> </div>
</div> </div>
<script> <script></script>
</script>
{% endblock content %} {% endblock content %}

View File

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

View File

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

View File

@ -1,78 +1,137 @@
{% load static %} {% load static %}
<div class="modal fade bs-modal-lg" id="new_compound_modal" tabindex="-1" aria-labelledby="new_compound_modal" aria-modal="true"
role="dialog"> <dialog
<div class="modal-dialog modal-lg"> id="new_compound_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> @close="reset()"
<span aria-hidden="true">×</span> >
<div class="modal-box max-w-3xl">
<!-- Header -->
<h3 class="text-lg font-bold">Create a new Compound</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button> </button>
<h4 class="modal-title">Create a new Compound</h4> </form>
</div>
<div class="modal-body"> <!-- Body -->
<form id="new_compound_modal_form" accept-charset="UTF-8" action="{% url 'package compound list' meta.current_package.uuid %}" data-remote="true" method="post"> <div class="py-4">
<form
id="new-compound-modal-form"
accept-charset="UTF-8"
action="{% url 'package compound list' meta.current_package.uuid %}"
method="post"
>
{% csrf_token %} {% csrf_token %}
<label for="compound-name">Name</label>
<input id="compound-name" class="form-control" name="compound-name" placeholder="Name"/> <div class="form-control mb-3">
<label for="compound-description">Description</label> <label class="label" for="compound-name">
<input id="compound-description" class="form-control" name="compound-description" placeholder="Description"/> <span class="label-text">Name</span>
<label for="compound-smiles">SMILES</label> </label>
<input type="text" class="form-control" name="compound-smiles" placeholder="SMILES" id="compound-smiles"> <input
<p></p> id="compound-name"
<div> class="input input-bordered w-full"
<iframe id="new_compound_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%" name="compound-name"
height="510"></iframe> placeholder="Name"
required
/>
</div>
<div class="form-control mb-3">
<label class="label" for="compound-description">
<span class="label-text">Description</span>
</label>
<input
id="compound-description"
class="input input-bordered w-full"
name="compound-description"
placeholder="Description"
/>
</div>
<div class="form-control mb-3">
<label class="label" for="compound-smiles">
<span class="label-text">SMILES</span>
</label>
<input
type="text"
class="input input-bordered w-full"
name="compound-smiles"
placeholder="SMILES"
id="compound-smiles"
/>
</div>
<div class="mb-3">
<iframe
id="new_compound_ketcher"
src="{% static '/js/ketcher2/ketcher.html' %}"
width="100%"
height="510"
></iframe>
</div> </div>
<p></p>
</form> </form>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary pull-left" data-dismiss="modal">Close <!-- 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('new-compound-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> </button>
<button type="button" class="btn btn-primary" id="new_compound_modal_form_submit">Submit</button>
</div>
</div> </div>
</div> </div>
</div> <!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>
<script> <script>
document
function newCompoundModalketcherToNewCompoundModalTextInput() { .getElementById("new_compound_ketcher")
$('#compound-smiles').val(this.ketcher.getSmiles()); .addEventListener("load", function () {
} const iframe = this;
$(function() {
$('#new_compound_ketcher').on('load', function() {
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 () {
ketcher: win.ketcher document.getElementById("compound-smiles").value =
this.ketcher.getSmiles();
},
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,45 +1,96 @@
<div class="modal fade" tabindex="-1" id="new_group_modal" role="dialog" aria-labelledby="new_group_modal" {% load static %}
aria-hidden="true">
<div class="modal-dialog"> <dialog
<div class="modal-content"> id="new_group_modal"
<div class="modal-header"> class="modal"
<button type="button" class="close" data-dismiss="modal"> x-data="modalForm()"
<span aria-hidden="true">&times;</span> @close="reset()"
<span class="sr-only">Close</span> >
<div class="modal-box">
<!-- Header -->
<h3 class="font-bold text-lg">New Group</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
:disabled="isSubmitting"
>
</button> </button>
<h4 class="modal-title">New Group</h4> </form>
</div>
<div class="modal-body"> <!-- Body -->
<p>Create new Group. You can assign users to the group once <div class="py-4">
it is created. Description can be changed after creation.</p> <p class="mb-4">
<form id="new_group_modal_form" accept-charset="UTF-8" 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>
<form
id="new-group-modal-form"
accept-charset="UTF-8"
action="{{ SERVER_BASE }}/group"
method="post"
>
{% csrf_token %} {% csrf_token %}
<p>
<label for="name">Name</label> <div class="form-control mb-3">
<input id="name" type="text" name="group-name" class="form-control" placeholder="Name"/> <label class="label" for="group-name">
</p> <span class="label-text">Name</span>
<p> </label>
<label for="description">Description</label> <input
<input id="description" type="text" class="form-control" placeholder="Description..." id="group-name"
name="group-description"/> class="input input-bordered w-full"
</p> name="group-name"
placeholder="Name"
required
/>
</div>
<div class="form-control mb-3">
<label class="label" for="group-description">
<span class="label-text">Description</span>
</label>
<input
id="group-description"
type="text"
class="input input-bordered w-full"
placeholder="Description..."
name="group-description"
/>
</div>
</form> </form>
</div> </div>
<div class="modal-footer">
<a id="new_group_modal_form_submit" class="btn btn-primary" href="#">Submit</a> <!-- Footer -->
<button type="button" class="btn btn-default" data-dismiss="modal"> <div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Cancel Cancel
</button> </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>
</div> <!-- Backdrop -->
<script> <form method="dialog" class="modal-backdrop">
$(function() { <button :disabled="isSubmitting">close</button>
$('#new_group_modal_form_submit').on('click', function() { </form>
$('#new_group_modal_form').submit(); </dialog>
});
});
</script>

View File

@ -1,188 +1,337 @@
<dialog
id="new_model_modal"
class="modal"
x-data="{
isSubmitting: false,
modelType: '',
buildAppDomain: false,
<div class="modal fade" tabindex="-1" id="new_model_modal" role="dialog" aria-labelledby="new_model_modal" reset() {
aria-hidden="true"> this.isSubmitting = false;
<div class="modal-dialog modal-lg"> this.modelType = '';
<div class="modal-content"> this.buildAppDomain = false;
<div class="modal-header"> },
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> get showMlrr() {
<span class="sr-only">Close</span> 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-box max-w-3xl">
<!-- Header -->
<h3 class="text-lg font-bold">New Model</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button> </button>
<h4 class="modal-title">New Model</h4> </form>
</div>
<div class="modal-body"> <!-- Body -->
<form id="new_model_form" accept-charset="UTF-8" action="{{ meta.current_package.url }}/model" <div class="py-4">
data-remote="true" method="post"> <form
id="new_model_form"
accept-charset="UTF-8"
action="{{ meta.current_package.url }}/model"
method="post"
>
{% csrf_token %} {% csrf_token %}
<div class="jumbotron">Create a new Model to <div class="alert alert-info mb-4">
limit the number of degradation products in the <span>
prediction. You just need to set a name and the packages Create a new Model to limit the number of degradation products in
you want the object to be based on. There are multiple types of models available. the prediction. You just need to set a name and the packages you
For additional information have a look at our want the object to be based on. There are multiple types of models
<a target="_blank" href="https://wiki.envipath.org/index.php/relative-reasoning" role="button">wiki available. For additional information have a look at our
&gt;&gt;</a> <a
target="_blank"
href="https://wiki.envipath.org/index.php/relative-reasoning"
class="link"
>wiki &gt;&gt;</a
>
</span>
</div> </div>
<!-- Name --> <!-- Name -->
<label for="model-name">Name</label> <div class="form-control mb-3">
<input id="model-name" name="model-name" class="form-control" placeholder="Name"/> <label class="label" for="model-name">
<span class="label-text">Name</span>
</label>
<input
id="model-name"
name="model-name"
class="input input-bordered w-full"
placeholder="Name"
required
/>
</div>
<!-- Description --> <!-- Description -->
<label for="model-description">Description</label> <div class="form-control mb-3">
<input id="model-description" name="model-description" class="form-control" <label class="label" for="model-description">
placeholder="Description"/> <span class="label-text">Description</span>
</label>
<input
id="model-description"
name="model-description"
class="input input-bordered w-full"
placeholder="Description"
/>
</div>
<!-- Model Type --> <!-- Model Type -->
<label for="model-type">Model Type</label> <div class="form-control mb-3">
<select id="model-type" name="model-type" class="form-control" data-width='100%'> <label class="label" for="model-type">
<option disabled selected>Select Model Type</option> <span class="label-text">Model Type</span>
</label>
<select
id="model-type"
name="model-type"
class="select select-bordered w-full"
x-model="modelType"
required
>
<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 id="model-rule-packages" name="model-rule-packages" data-actions-box='true' <span class="label-text">Rule Packages</span>
class="form-control" multiple data-width='100%'> </label>
<option disabled>Reviewed Packages</option> <select
id="model-rule-packages"
name="model-rule-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 %}
</optgroup>
</select> </select>
<label class="label">
<span class="label-text-alt">Hold Ctrl/Cmd to select multiple</span>
</label>
</div> </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 id="model-data-packages" name="model-data-packages" data-actions-box='true' x-show="showMlrr || showRbrr || showEnviformer"
class="form-control" multiple data-width='100%'> x-cloak
<option disabled>Reviewed Packages</option> >
<label class="label" for="model-data-packages">
<span class="label-text">Data Packages</span>
</label>
<select
id="model-data-packages"
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 %}
</optgroup>
</select> </select>
<label class="label">
<span class="label-text-alt">Hold Ctrl/Cmd to select multiple</span>
</label>
</div> </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 id="model-fingerprinter" name="model-fingerprinter" data-actions-box='true' <span class="label-text">Fingerprinter</span>
class="form-control" multiple data-width='100%'> </label>
<select
id="model-fingerprinter"
name="model-fingerprinter"
class="select select-bordered w-full h-32"
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>Select Additional Fingerprinter / Descriptor</option> <optgroup label="Additional Fingerprinter / Descriptor">
{% 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 %}
</optgroup>
{% endif %} {% endif %}
</select> </select>
</div> <label class="label">
<span class="label-text-alt">Hold Ctrl/Cmd to select multiple</span>
<!-- Threshold -->
<div id="threshold" class="ep-model-param mlrr enviformer">
<label for="model-threshold">Threshold</label>
<input type="number" min="0" max="1" step="0.05" value="0.5" id="model-threshold"
name="model-threshold" class="form-control">
</div>
<div id="appdomain" class="ep-model-param mlrr">
{% if meta.enabled_features.APPLICABILITY_DOMAIN %}
<!-- Build AD? -->
<div class="checkbox">
<label>
<input type="checkbox" id="build-app-domain" name="build-app-domain">Also build an
Applicability Domain?
</label> </label>
</div> </div>
<div id="ad-params" style="display:none">
<!-- Num Neighbors --> <!-- Threshold (MLRR, Enviformer) -->
<label for="num-neighbors">Number of Neighbors</label> <div
<input id="num-neighbors" name="num-neighbors" type="number" class="form-control" class="form-control mb-3"
value="5" x-show="showMlrr || showEnviformer"
step="1" min="0" max="10"> x-cloak
<!-- Local Compatibility --> >
<label for="local-compatibility-threshold">Local Compatibility Threshold</label> <label class="label" for="model-threshold">
<input id="local-compatibility-threshold" name="local-compatibility-threshold" <span class="label-text">Threshold</span>
</label>
<input
type="number" type="number"
class="form-control" value="0.5" step="0.01" min="0" max="1"> min="0"
<!-- Reliability --> max="1"
<label for="reliability-threshold">Reliability Threshold</label> step="0.05"
<input id="reliability-threshold" name="reliability-threshold" type="number" value="0.5"
class="form-control" value="0.5" step="0.01" min="0" max="1"> id="model-threshold"
name="model-threshold"
class="input input-bordered w-full"
/>
</div>
<!-- Applicability Domain (MLRR) -->
{% if meta.enabled_features.APPLICABILITY_DOMAIN %}
<div x-show="showMlrr" x-cloak>
<div class="form-control mb-3">
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
id="build-app-domain"
name="build-app-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>
<input
id="num-neighbors"
name="num-neighbors"
type="number"
class="input input-bordered w-full"
value="5"
step="1"
min="0"
max="10"
/>
</div>
<div class="form-control">
<label class="label" for="local-compatibility-threshold">
<span class="label-text">Local Compatibility Threshold</span>
</label>
<input
id="local-compatibility-threshold"
name="local-compatibility-threshold"
type="number"
class="input input-bordered w-full"
value="0.5"
step="0.01"
min="0"
max="1"
/>
</div>
<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>
{% endif %} {% endif %}
</div>
</form> </form>
</div> </div>
<div class="modal-footer">
<a id="new_model_modal_form_submit" class="btn btn-primary" href="#">Submit</a> <!-- Footer -->
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button> <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_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>
</div>
<script> <!-- Backdrop -->
$(function () { <form method="dialog" class="modal-backdrop">
// Built in Model Types <button :disabled="isSubmitting">close</button>
var nativeModelTypes = [ </form>
"mlrr", </dialog>
"rbrr",
"enviformer",
]
// 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,62 +1,93 @@
<div class="modal fade" {% load static %}
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-content"> <div class="modal-box">
<div class="modal-header"> <!-- Header -->
<button type="button" <h3 class="font-bold text-lg">New Package</h3>
class="close"
data-dismiss="modal"> <!-- Close button (X) -->
<span aria-hidden="true">&times;</span> <form method="dialog">
<span class="sr-only">Close</span> <button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
:disabled="isSubmitting"
>
</button> </button>
<h4 class="modal-title">New Package</h4> </form>
</div>
<div class="modal-body"> <!-- Body -->
<p>Create new package. Description can be changed later.</p> <div class="py-4">
<form id="new_package_modal_form" <p class="mb-4">Create new package. Description can be changed later.</p>
<form
id="new-package-modal-form"
accept-charset="UTF-8" accept-charset="UTF-8"
action="" action=""
data-remote="true" method="post"
method="post"> >
{% csrf_token %} {% csrf_token %}
<p>
<label for="name">Name</label> <div class="form-control mb-3">
<input id="name" class="form-control" <label class="label" for="package-name">
<span class="label-text">Name</span>
</label>
<input
id="package-name"
class="input input-bordered w-full"
name="package-name" name="package-name"
placeholder="Name"/> placeholder="Name"
</p> required
<p> />
<label for="description">Description</label> </div>
<input id="description"
<div class="form-control mb-3">
<label class="label" for="package-description">
<span class="label-text">Description</span>
</label>
<input
id="package-description"
type="text" type="text"
rows="3" class="input input-bordered w-full"
class="form-control"
placeholder="Description..." placeholder="Description..."
name="package-description"/> name="package-description"
</p> />
</div>
</form> </form>
</div> </div>
<div class="modal-footer">
<a id="new_package_modal_form_submit" <!-- 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" class="btn btn-primary"
href="#">Submit</a> @click="submit('new-package-modal-form')"
<button type="button" :disabled="isSubmitting"
class="btn btn-default" >
data-dismiss="modal">Cancel <span x-show="!isSubmitting">Submit</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
<span x-show="isSubmitting">Creating...</span>
</button> </button>
</div> </div>
</div> </div>
</div>
</div> <!-- Backdrop -->
<script> <form method="dialog" class="modal-backdrop">
$(function() { <button :disabled="isSubmitting">close</button>
$('#new_package_modal_form_submit').on('click', function (e) { </form>
e.preventDefault(); </dialog>
$('#new_package_modal_form').submit();
});
});
</script>

View File

@ -1,302 +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,109 +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 type="button" class="close" data-dismiss="modal" aria-label="Close"> tpMethod: '',
<span aria-hidden="true">&times;</span>
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> </button>
</div> </form>
<div class="modal-body">
<p>To create a Prediction Setting fill the form below and click "Create"</p> <!-- Body -->
<form id="new-prediction-setting-modal-form" accept-charset="UTF-8" action="" data-remote="true" <div class="py-4">
method="post"> <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 %} {% csrf_token %}
<label for="prediction-setting-name">Name</label> <div class="form-control mb-3">
<input id="prediction-setting-name" name="prediction-setting-name" class="form-control" placeholder="Name"/> <label class="label" for="prediction-setting-name">
<label for="prediction-setting-description">Description</label> <span class="label-text">Name</span>
<input id="prediction-setting-description" name="prediction-setting-description" class="form-control" </label>
placeholder="Description"/> <input
id="prediction-setting-name"
name="prediction-setting-name"
class="input input-bordered w-full"
placeholder="Name"
required
/>
</div>
<label for="prediction-setting-max-nodes">Max #Nodes</label> <div class="form-control mb-3">
<input id="prediction-setting-max-nodes" type="number" class="form-control" name="prediction-setting-max-nodes" value="30" min="1" max="50" step="1"> <label class="label" for="prediction-setting-description">
<label for="prediction-setting-max-depth">Max Depth</label> <span class="label-text">Description</span>
<input id="prediction-setting-max-depth" type="number" class="form-control" name="prediction-setting-max-depth" value="5" min="1" max="8" step="1"> </label>
<input
id="prediction-setting-description"
name="prediction-setting-description"
class="input input-bordered w-full"
placeholder="Description"
/>
</div>
<label for="tp-generation-method">TP Generation Method</label> <div class="form-control mb-3">
<select id="tp-generation-method" name="tp-generation-method" class="form-control" data-width='100%'> <label class="label" for="prediction-setting-max-nodes">
<option disabled selected>Select how TPs are generated</option> <span class="label-text">Max #Nodes</span>
</label>
<input
id="prediction-setting-max-nodes"
type="number"
class="input input-bordered w-full"
name="prediction-setting-max-nodes"
value="30"
min="1"
max="50"
step="1"
/>
</div>
<div class="form-control mb-3">
<label class="label" for="prediction-setting-max-depth">
<span class="label-text">Max Depth</span>
</label>
<input
id="prediction-setting-max-depth"
type="number"
class="input input-bordered w-full"
name="prediction-setting-max-depth"
value="5"
min="1"
max="8"
step="1"
/>
</div>
<div class="form-control mb-3">
<label class="label" for="tp-generation-method">
<span class="label-text">TP Generation Method</span>
</label>
<select
id="tp-generation-method"
name="tp-generation-method"
class="select select-bordered w-full"
x-model="tpMethod"
required
>
<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 -->
<select id="rule-based-prediction-setting-packages" name="rule-based-prediction-setting-packages" <div x-show="tpMethod === 'rule-based-prediction-setting'" x-cloak>
data-actions-box='true' class="form-control" multiple data-width='100%'> <div class="form-control mb-3">
<option disabled>Reviewed Packages</option> <label class="label">
<span class="label-text">Rule Packages</span>
</label>
<select
id="rule-based-prediction-setting-packages"
name="rule-based-prediction-setting-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 %}
</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>
<select id="model-based-prediction-setting-model" name="model-based-prediction-setting-model" class="form-control" data-width='100%'> <!-- Model Based Settings -->
<option disabled selected>Select the model</option> <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
id="model-based-prediction-setting-model"
name="model-based-prediction-setting-model"
class="select select-bordered w-full"
>
<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">Threshold</label>
<input id="model-based-prediction-setting-threshold" name="model-based-prediction-setting-threshold" class="form-control" placeholder="0.25" type="number"/>
</div> </div>
<input class="form-check-input" type="checkbox" value="on" id="prediction-setting-new-default" name="prediction-setting-new-default"> <div class="form-control mb-3">
<label class="form-check-label" for="prediction-setting-new-default">Set this setting as new default</label> <label class="label" for="model-based-prediction-setting-threshold">
<span class="label-text">Threshold</span>
</label>
<input
id="model-based-prediction-setting-threshold"
name="model-based-prediction-setting-threshold"
class="input input-bordered w-full"
placeholder="0.25"
type="number"
min="0"
max="1"
step="0.05"
/>
</div>
</div>
<div class="form-control">
<label class="label cursor-pointer justify-start gap-3">
<input
type="checkbox"
class="checkbox"
value="on"
id="prediction-setting-new-default"
name="prediction-setting-new-default"
/>
<span class="label-text">Set this setting as new default</span>
</label>
</div>
</form> </form>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <!-- Footer -->
<button type="button" class="btn btn-primary" id="new-prediction-setting-modal-submit">Create</button> <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">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>
</div>
<script>
$(function() {
// Initially hide all "specific" forms <!-- Backdrop -->
$("div[id$='-specific-form']").each( function() { <form method="dialog" class="modal-backdrop">
$(this).hide(); <button :disabled="isSubmitting">close</button>
}); </form>
</dialog>
$("#rule-based-prediction-setting-packages").selectpicker();
// On change hide all and show only selected
$("#tp-generation-method").change(function() {
$("div[id$='-specific-form']").each( function() {
$(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,50 +1,105 @@
{% load static %} {% load static %}
<div class="modal fade bs-modal-lg" id="new_reaction_modal" tabindex="-1" aria-labelledby="new_reaction_modal" aria-modal="true"
role="dialog"> <dialog
<div class="modal-dialog modal-lg"> id="new_reaction_modal"
<div class="modal-content"> class="modal"
<div class="modal-header"> x-data="modalForm()"
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> @close="reset()"
<span aria-hidden="true">×</span> >
<div class="modal-box max-w-3xl">
<!-- Header -->
<h3 class="font-bold text-lg">Create a new Reaction</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
:disabled="isSubmitting"
>
</button> </button>
<h4 class="modal-title">Create a new Reaction</h4> </form>
</div>
<div class="modal-body"> <!-- Body -->
<form id="new_reaction_modal_form" accept-charset="UTF-8" action="{% url 'package reaction list' meta.current_package.uuid %}" data-remote="true" method="post"> <div class="py-4">
<form
id="new-reaction-modal-form"
accept-charset="UTF-8"
action="{% url 'package reaction list' meta.current_package.uuid %}"
method="post"
>
{% csrf_token %} {% csrf_token %}
<label for="reaction-name">Name</label>
<input id="reaction-name" class="form-control" name="reaction-name" placeholder="Name"/> <div class="form-control mb-3">
<label for="reaction-description">Description</label> <label class="label" for="reaction-name">
<input id="reaction-description" class="form-control" name="reaction-description" placeholder="Description"/> <span class="label-text">Name</span>
<p></p> </label>
<div> <input
<iframe id="new_reaction_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%" id="reaction-name"
height="510"></iframe> class="input input-bordered w-full"
name="reaction-name"
placeholder="Name"
required
/>
</div> </div>
<input type="hidden" name="reaction-smirks" id="reaction-smirks">
<p></p> <div class="form-control mb-3">
<label class="label" for="reaction-description">
<span class="label-text">Description</span>
</label>
<input
id="reaction-description"
class="input input-bordered w-full"
name="reaction-description"
placeholder="Description"
/>
</div>
<div class="mb-3">
<iframe
id="new_reaction_ketcher"
src="{% static '/js/ketcher2/ketcher.html' %}"
width="100%"
height="510"
></iframe>
</div>
<input type="hidden" name="reaction-smirks" id="reaction-smirks" />
</form> </form>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary pull-left" data-dismiss="modal">Close <!-- 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="
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> </button>
<button type="button" class="btn btn-primary" id="new_reaction_modal_form_submit">Submit</button>
</div>
</div> </div>
</div> </div>
</div> <!-- Backdrop -->
<script> <form method="dialog" class="modal-backdrop">
$(function() { <button :disabled="isSubmitting">close</button>
$('#new_reaction_modal_form_submit').on('click', function(e) { </form>
e.preventDefault(); </dialog>
$(this).prop("disabled",true);
k = getKetcher('new_reaction_ketcher');
$('#reaction-smirks').val(k.getSmiles());
// submit form
$('#new_reaction_modal_form').submit();
});
});
</script>

View File

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

View File

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

View File

@ -1,61 +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 type="button" class="close" data-dismiss="modal" aria-label="Close"> isSubmitting: false,
<span aria-hidden="true">&times;</span> selectedType: '',
reset() {
this.isSubmitting = false;
this.selectedType = '';
},
submit() {
if (!this.selectedType) return;
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> </button>
<h3 class="modal-title">Add Additional Information</h3> </form>
</div>
<div class="modal-body"> <!-- Body -->
<select id="select-additional-information-type" data-actions-box='true' class="form-control" data-width='100%'> <div class="py-4">
<option selected disabled>Select the type to add</option> <div class="form-control">
<label class="label" for="select-additional-information-type">
<span class="label-text">Select the type to add</span>
</label>
<select
id="select-additional-information-type"
class="select select-bordered w-full"
x-model="selectedType"
>
<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 }}">{{ add_inf.display_name }}</option> <option value="{{ add_inf.name }}">
{{ add_inf.display_name }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</div>
{% for add_inf in available_additional_information %} {% for add_inf in available_additional_information %}
<div class="aiform {{ add_inf.name }}" style="display: none;"> <div
<form id="add_{{ add_inf.name }}_add-additional-information-modal-form" accept-charset="UTF-8" class="mt-4"
action="" data-remote="true" method="post"> x-show="selectedType === '{{ add_inf.name }}'"
x-cloak
>
<form
id="add_{{ add_inf.name }}_add-additional-information-modal-form"
accept-charset="UTF-8"
action=""
method="post"
>
{% csrf_token %} {% csrf_token %}
{{ add_inf.widget|safe }} {{ add_inf.widget|safe }}
<input type="hidden" name="hidden" value="add-additional-information"> <input
type="hidden"
name="hidden"
value="add-additional-information"
/>
</form> </form>
</div> </div>
{% endfor %} {% endfor %}
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <!-- Footer -->
<button type="button" class="btn btn-primary" id="add-additional-information-modal-submit">Add <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> </button>
</div> </div>
</div> </div>
</div>
</div>
<script>
$(function() {
$('#select-additional-information-type').change(function(e){ <!-- Backdrop -->
var selectedType = $("#select-additional-information-type :selected").val(); <form method="dialog" class="modal-backdrop">
$('.aiform').hide(); <button :disabled="isSubmitting">close</button>
$('.' + selectedType).show(); </form>
}) </dialog>
$('#add-additional-information-modal-submit').click(function(e){
e.preventDefault();
var selectedType = $("#select-additional-information-type :selected").val();
console.log(selectedType);
if (selectedType !== null && selectedType !== undefined && selectedType !== '') {
$('.' + selectedType + ' >form').submit();
}
});
});
</script>

View File

@ -1,127 +1,181 @@
{% load static %} {% load static %}
<div class="modal fade bs-modal-lg" id="add_pathway_edge_modal" tabindex="-1" aria-labelledby="add_pathway_edge_modal" <dialog
aria-modal="true" id="add_pathway_edge_modal"
role="dialog"> class="modal"
<div class="modal-dialog modal-lg"> x-data="{
<div class="modal-content"> isSubmitting: false,
<div class="modal-header"> reactionImageUrl: '',
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> 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-box max-w-4xl">
<!-- Header -->
<h3 class="text-lg font-bold">Add a Reaction</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button> </button>
<h4 class="modal-title">Add a Reaction</h4> </form>
</div>
<div class="modal-body"> <!-- Body -->
<form id="add_pathway_edge_modal_form" accept-charset="UTF-8" <div class="py-4">
<form
id="add_pathway_edge_modal_form"
accept-charset="UTF-8"
action="{% url 'package pathway edge list' meta.current_package.uuid pathway.uuid %}" action="{% url 'package pathway edge list' meta.current_package.uuid pathway.uuid %}"
data-remote="true" method="post"> data-remote="true"
method="post"
>
{% csrf_token %} {% csrf_token %}
<label for="edge-name">Name</label> <div class="form-control mb-3">
<input id="edge-name" class="form-control" name="edge-name" placeholder="Name"/> <label class="label" for="edge-name">
<label for="edge-description">Description</label> <span class="label-text">Name</span>
<input id="edge-description" class="form-control" name="edge-description" placeholder="Description"/> </label>
<p></p> <input
<div class="row"> id="edge-name"
<div class="col-xs-5"> type="text"
<legend>Substrate(s)</legend> class="input input-bordered w-full"
name="edge-name"
placeholder="Name"
/>
</div> </div>
<div class="col-xs-2">
<div class="form-control mb-3">
<label class="label" for="edge-description">
<span class="label-text">Description</span>
</label>
<input
id="edge-description"
type="text"
class="input input-bordered w-full"
name="edge-description"
placeholder="Description"
/>
</div> </div>
<div class="col-xs-5">
<legend>Product(s)</legend> <div class="mb-3 grid grid-cols-11 gap-2">
</div> <div class="col-span-5">
</div> <div class="form-control">
<div class="row"> <label class="label">
<div class="col-xs-5"> <span class="label-text font-semibold">Substrate(s)</span>
<select id="add_pathway_edge_substrates" name="edge-substrates" </label>
data-actions-box='true' class="form-control" multiple data-width='100%'> <select
id="add_pathway_edge_substrates"
name="edge-substrates"
class="select select-bordered h-32 w-full"
multiple
@change="updateReactionImage()"
>
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option> <option
{% endfor %} data-smiles="{{ n.default_node_label.smiles }}"
</select> value="{{ n.url }}"
</div> >
<div class="col-xs-2" style="display: flex; justify-content: center; align-items: center;"> {{ n.default_node_label.name|safe }}
<i class="glyphicon glyphicon-arrow-right"></i> </option>
</div>
<div class="col-xs-5">
<select id="add_pathway_edge_products" name="edge-products"
data-actions-box='true' class="form-control" multiple data-width='100%'>
{% for n in pathway.nodes %}
<option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
</div> </div>
<div class="row"> <div class="col-span-1 flex items-center justify-center">
<p></p> <span class="text-2xl"></span>
<div class="col-xs-12" id="reaction_image">
</div> </div>
<div class="col-span-5">
<div class="form-control">
<label class="label">
<span class="label-text font-semibold">Product(s)</span>
</label>
<select
id="add_pathway_edge_products"
name="edge-products"
class="select select-bordered h-32 w-full"
multiple
@change="updateReactionImage()"
>
{% for n in pathway.nodes %}
<option
data-smiles="{{ n.default_node_label.smiles }}"
value="{{ n.url }}"
>
{{ n.default_node_label.name|safe }}
</option>
{% endfor %}
</select>
</div>
</div>
</div>
<div class="mb-3" x-show="reactionImageUrl" x-cloak>
<img :src="reactionImageUrl" class="w-full" alt="Reaction preview" />
</div> </div>
</form> </form>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary pull-left" data-dismiss="modal">Close <!-- 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> </button>
<button type="button" class="btn btn-primary" id="add_pathway_edge_modal_form_submit">Submit</button>
</div>
</div> </div>
</div> </div>
</div> <!-- Backdrop -->
<script> <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
function reactionImage() { </form>
var substrates = []; </dialog>
$('#add_pathway_edge_substrates option:selected').each(function () {
var smiles = $(this).data('smiles'); // read data-smiles attribute
substrates.push(smiles);
});
var products = []
$('#add_pathway_edge_products option:selected').each(function () {
var smiles = $(this).data('smiles'); // read data-smiles attribute
products.push(smiles);
});
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,78 +1,137 @@
{% load static %} {% load static %}
<div class="modal fade bs-modal-lg" id="add_pathway_node_modal" tabindex="-1" aria-labelledby="add_pathway_node_modal" aria-modal="true" <dialog
role="dialog"> id="add_pathway_node_modal"
<div class="modal-dialog modal-lg"> class="modal"
<div class="modal-content"> x-data="modalForm()"
<div class="modal-header"> @close="reset()"
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-box max-w-4xl">
<!-- Header -->
<h3 class="text-lg font-bold">Add a Node</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button> </button>
<h4 class="modal-title">Add a Node</h4> </form>
</div>
<div class="modal-body"> <!-- Body -->
<form id="add_pathway_node_modal_form" accept-charset="UTF-8" action="{% url 'package pathway node list' meta.current_package.uuid pathway.uuid %}" data-remote="true" method="post"> <div class="py-4">
<form
id="add_pathway_node_modal_form"
accept-charset="UTF-8"
action="{% url 'package pathway node list' meta.current_package.uuid pathway.uuid %}"
data-remote="true"
method="post"
>
{% csrf_token %} {% csrf_token %}
<label for="node-name">Name</label> <div class="form-control mb-3">
<input id="node-name" class="form-control" name="node-name" placeholder="Name"/> <label class="label" for="node-name">
<label for="node-description">Description</label> <span class="label-text">Name</span>
<input id="node-description" class="form-control" name="node-description" placeholder="Description"/> </label>
<label for="node-smiles">SMILES</label> <input
<input type="text" class="form-control" name="node-smiles" placeholder="SMILES" id="node-smiles"> id="node-name"
<p></p> type="text"
<div> class="input input-bordered w-full"
<iframe id="add_node_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%" name="node-name"
height="510"></iframe> placeholder="Name"
/>
</div>
<div class="form-control mb-3">
<label class="label" for="node-description">
<span class="label-text">Description</span>
</label>
<input
id="node-description"
type="text"
class="input input-bordered w-full"
name="node-description"
placeholder="Description"
/>
</div>
<div class="form-control mb-3">
<label class="label" for="node-smiles">
<span class="label-text">SMILES</span>
</label>
<input
type="text"
class="input input-bordered w-full"
name="node-smiles"
placeholder="SMILES"
id="node-smiles"
/>
</div>
<div class="mb-3">
<iframe
id="add_node_ketcher"
src="{% static '/js/ketcher2/ketcher.html' %}"
width="100%"
height="510"
></iframe>
</div> </div>
<p></p>
</form> </form>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary pull-left" data-dismiss="modal">Close <!-- 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('add_pathway_node_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> </button>
<button type="button" class="btn btn-primary" id="add_pathway_node_modal_form_submit">Submit</button>
</div>
</div> </div>
</div> </div>
</div> <!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>
<script> <script>
document
function newStructureModalketcherToNewStructureModalTextInput() { .getElementById("add_node_ketcher")
$('#node-smiles').val(this.ketcher.getSmiles()); .addEventListener("load", function () {
} const iframe = this;
$(function() {
$('#add_node_ketcher').on('load', function() {
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 () {
ketcher: win.ketcher document.getElementById("node-smiles").value =
this.ketcher.getSmiles();
},
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,78 +1,137 @@
{% load static %} {% load static %}
<div class="modal fade bs-modal-lg" id="add_structure_modal" tabindex="-1" aria-labelledby="add_structure_modal" aria-modal="true" <dialog
role="dialog"> id="add_structure_modal"
<div class="modal-dialog modal-lg"> class="modal"
<div class="modal-content"> x-data="modalForm()"
<div class="modal-header"> @close="reset()"
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> >
<span aria-hidden="true">×</span> <div class="modal-box max-w-4xl">
<!-- Header -->
<h3 class="text-lg font-bold">Create a new Structure</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button> </button>
<h4 class="modal-title">Create a new Structure</h4> </form>
</div>
<div class="modal-body"> <!-- Body -->
<form id="add_structure_modal_form" accept-charset="UTF-8" action="{% url 'package compound structure list' meta.current_package.uuid compound.uuid %}" data-remote="true" method="post"> <div class="py-4">
<form
id="add_structure_modal_form"
accept-charset="UTF-8"
action="{% url 'package compound structure list' meta.current_package.uuid compound.uuid %}"
data-remote="true"
method="post"
>
{% csrf_token %} {% csrf_token %}
<label for="structure-name">Name</label> <div class="form-control mb-3">
<input id="structure-name" class="form-control" name="structure-name" placeholder="Name"/> <label class="label" for="structure-name">
<label for="structure-description">Description</label> <span class="label-text">Name</span>
<input id="structure-description" class="form-control" name="structure-description" placeholder="Description"/> </label>
<label for="structure-smiles">SMILES</label> <input
<input type="text" class="form-control" name="structure-smiles" placeholder="SMILES" id="structure-smiles"> id="structure-name"
<p></p> type="text"
<div> class="input input-bordered w-full"
<iframe id="add_structure_ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%" name="structure-name"
height="510"></iframe> placeholder="Name"
/>
</div>
<div class="form-control mb-3">
<label class="label" for="structure-description">
<span class="label-text">Description</span>
</label>
<input
id="structure-description"
type="text"
class="input input-bordered w-full"
name="structure-description"
placeholder="Description"
/>
</div>
<div class="form-control mb-3">
<label class="label" for="structure-smiles">
<span class="label-text">SMILES</span>
</label>
<input
type="text"
class="input input-bordered w-full"
name="structure-smiles"
placeholder="SMILES"
id="structure-smiles"
/>
</div>
<div class="mb-3">
<iframe
id="add_structure_ketcher"
src="{% static '/js/ketcher2/ketcher.html' %}"
width="100%"
height="510"
></iframe>
</div> </div>
<p></p>
</form> </form>
</div> </div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary pull-left" data-dismiss="modal">Close <!-- 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('add_structure_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> </button>
<button type="button" class="btn btn-primary" id="add_structure_modal_form_submit">Submit</button>
</div>
</div> </div>
</div> </div>
</div> <!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>
<script> <script>
document
function newStructureModalketcherToNewStructureModalTextInput() { .getElementById("add_structure_ketcher")
$('#structure-smiles').val(this.ketcher.getSmiles()); .addEventListener("load", function () {
} const iframe = this;
$(function() {
$('#add_structure_ketcher').on('load', function() {
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 () {
ketcher: win.ketcher document.getElementById("structure-smiles").value =
this.ketcher.getSmiles();
},
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,66 +1,94 @@
{% 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" class="close" data-dismiss="modal" aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-box">
<!-- Header -->
<h3 class="text-lg font-bold">Delete Edge</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button> </button>
</div> </form>
<div class="modal-body">
<!-- 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 id="delete-pathway-edge-modal-form" accept-charset="UTF-8" action="" data-remote="true" <form
method="post"> id="delete-pathway-edge-modal-form"
accept-charset="UTF-8"
action=""
method="post"
>
{% csrf_token %} {% csrf_token %}
<select id="delete_pathway_edge_edges" name="edge-url" <div class="form-control">
data-actions-box='true' class="form-control" data-width='100%'> <label class="label" for="delete_pathway_edge_edges">
<option value="" disabled selected>Select Reaction to delete</option> <span class="label-text">Select Reaction to delete</span>
</label>
<select
id="delete_pathway_edge_edges"
name="edge-url"
class="select select-bordered w-full"
x-model="selectedEdge"
@change="imageUrl = selectedEdge ? selectedEdge + '?image=svg' : ''"
required
>
<option value="" disabled selected>
Select Reaction to delete
</option>
{% for e in pathway.edges %} {% for e in pathway.edges %}
<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>
<input type="hidden" id="hidden" name="hidden" value="delete" />
</form> </form>
<p></p>
<div id="delete_pathway_edge_image"></div> <!-- Image Preview -->
</div> <div class="mt-4" x-show="imageUrl" x-cloak>
<div class="modal-footer"> <img :src="imageUrl" class="w-full" alt="Edge preview" />
<button type="button" class="btn btn-secondary" data-dismiss="modal">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,66 +1,97 @@
{% 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" class="close" data-dismiss="modal" aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-box">
<!-- Header -->
<h3 class="text-lg font-bold">Delete Node</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
</button> </button>
</div> </form>
<div class="modal-body">
Deletes the Node. Edges having this Node as Substrate or Product will be removed as well. <!-- Body -->
<p></p> <div class="py-4">
<form id="delete-pathway-node-modal-form" accept-charset="UTF-8" action="" data-remote="true" <p class="mb-4">
method="post"> Deletes the Node. Edges having this Node as Substrate or Product will be
removed as well.
</p>
<form
id="delete-pathway-node-modal-form"
accept-charset="UTF-8"
action=""
method="post"
>
{% csrf_token %} {% csrf_token %}
<select id="delete_pathway_node_nodes" name="node-url" <div class="form-control">
data-actions-box='true' class="form-control" data-width='100%'> <label class="label" for="delete_pathway_node_nodes">
<option value="" disabled selected>Select Compound to delete</option> <span class="label-text">Select Compound to delete</span>
</label>
<select
id="delete_pathway_node_nodes"
name="node-url"
class="select select-bordered w-full"
x-model="selectedNode"
@change="imageUrl = selectedNode ? selectedNode + '?image=svg' : ''"
required
>
<option value="" disabled selected>
Select Compound to delete
</option>
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option> <option value="{{ n.url }}">
{{ n.default_node_label.name|safe }}
</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" id="hidden" name="hidden" value="delete"/> </div>
<input type="hidden" id="hidden" name="hidden" value="delete" />
</form> </form>
<p></p>
<div id="delete_pathway_node_image"></div> <!-- Image Preview -->
</div> <div class="mt-4" x-show="imageUrl" x-cloak>
<div class="modal-footer"> <img :src="imageUrl" class="w-full" alt="Node preview" />
<button type="button" class="btn btn-secondary" data-dismiss="modal">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,36 +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" class="close" data-dismiss="modal" aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-box">
<!-- Header -->
<h3 class="font-bold text-lg">Download Pathway as CSV</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
:disabled="isSubmitting"
>
</button> </button>
</div> </form>
<div class="modal-body">
By clicking on Download the Pathway will be converted into a CSV and directly downloaded. <!-- Body -->
<form id="download-pathway-csv-modal-form" accept-charset="UTF-8" action="{{ pathway.url }}" <div class="py-4">
data-remote="true" method="GET"> <p>
<input type="hidden" name="download" value="true"/> By clicking on Download the Pathway will be converted into a CSV and
directly downloaded.
</p>
<form
id="download-pathway-csv-modal-form"
accept-charset="UTF-8"
action="{{ pathway.url }}"
method="GET"
>
<input type="hidden" name="download" value="true" />
</form> </form>
</div> </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="download-pathway-csv-modal-submit">Download</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#download-pathway-csv-modal-submit').click(function (e) { <!-- Footer -->
e.preventDefault(); <div class="modal-action">
$('#download-pathway-csv-modal-form').submit(); <button
$('#download_pathway_csv_modal').modal('hide'); type="button"
}); class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</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>
}) <!-- Backdrop -->
</script> <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

@ -1,32 +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" class="close" data-dismiss="modal" aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-box">
<!-- Header -->
<h3 class="font-bold text-lg">Download Pathway as Image</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">
<p>By clicking on Download the Pathway will be saved as SVG.</p>
</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="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> </button>
</div> </div>
<div class="modal-body">
By clicking on Download the Pathway will be saved as SVG.
</div> </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="download-pathway-image-modal-submit">Download</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#download-pathway-image-modal-submit').click(function (e) { <!-- Backdrop -->
e.preventDefault(); <form method="dialog" class="modal-backdrop">
downloadSVG($('#pwsvg')[0], '{{ pathway.name.split|join:"_" }}.svg') <button :disabled="isSubmitting">close</button>
$('#download_pathway_image_modal').modal('hide'); </form>
}); </dialog>
})
</script>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,45 +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" class="close" data-dismiss="modal" aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-box">
<!-- Header -->
<h3 class="font-bold text-lg">Update Package</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
:disabled="isSubmitting"
>
</button> </button>
</div> </form>
<div class="modal-body">
<p>Edit a Package.</p> <!-- Body -->
<form id="edit-package-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post"> <div class="py-4">
<form
id="edit-package-modal-form"
accept-charset="UTF-8"
action=""
method="post"
>
{% csrf_token %} {% csrf_token %}
<p>
<label for="package-name">Name</label> <div class="form-control mb-3">
<input id="package-name" class="form-control" name="package-name" value="{{ package.name|safe}}"> <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-description" type="text" class="form-control" id="package-name"
class="input input-bordered w-full"
name="package-name"
value="{{ package.name|safe }}"
required
/>
</div>
<div class="form-control mb-3">
<label class="label" for="package-description">
<span class="label-text">Description</span>
</label>
<input
id="package-description"
type="text"
class="input input-bordered w-full"
value="{{ package.description|safe }}" value="{{ package.description|safe }}"
name="package-description"> name="package-description"
</p> />
</div>
</form> </form>
</div> </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-package-modal-submit">Update</button>
</div>
</div>
</div>
</div>
<script>
$(function() {
$('#edit-package-modal-submit').click(function(e){ <!-- Footer -->
e.preventDefault(); <div class="modal-action">
$('#edit-package-modal-form').submit(); <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>
}) <!-- Backdrop -->
</script> <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

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

View File

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

View File

@ -1,106 +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" class="close" data-dismiss="modal" aria-label="Close"> >
<span aria-hidden="true">&times;</span> <div class="modal-box max-w-3xl">
</button> <!-- Header -->
</div> <h3 class="text-lg font-bold">Update Prediction Setting</h3>
<div class="modal-body">
<p>To update your prediction setting modify parameters in the form below und click "Update"</p>
<form id="edit-prediction-setting-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %}
<div id="prediction-setting" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<table class="table table-bordered table-hover"> <!-- Close button (X) -->
<tr style="background-color: rgba(0, 0, 0, 0.08);"> <form method="dialog">
<th scope="col" width="20%">Parameter</th> <button
<th scope="col" width="80%">Value</th> 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 update your prediction setting modify parameters in the form below
and click "Update"
</p>
<form
id="edit-prediction-setting-modal-form"
accept-charset="UTF-8"
action=""
method="post"
>
{% 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> </tr>
</thead>
<tbody> <tbody>
{% if 'model' in user.prediction_settings %} {% if 'model' in user.prediction_settings %}
<tr> <tr>
<td width="20%">Model</td> <td>Model</td>
<td width="80%"> <td>
<table width="100%" class="table table-bordered table-hover"> <div class="form-control">
<tbody> <select
<tr> id="model"
<td colspan="2"> name="model"
<select id="model" name="model" class="form-control" data-width='100%'> class="select select-bordered w-full"
>
{% for m in models %} {% for m in models %}
<option value="{{ m.id }}" {% if user.prediction_settings.model.url == m.url %}selected{% endif %}>{{ m.name|safe }}</option> <option
value="{{ m.id }}"
{% if user.prediction_settings.model.url == m.url %}selected{% endif %}
>
{{ m.name|safe }}
</option>
{% endfor %} {% endfor %}
</select> </select>
</td> </div>
</tr>
{% for k, v in user.prediction_settings.model_parameters.items %} {% for k, v in user.prediction_settings.model_parameters.items %}
<tr>
<th width="20%">Model Parameter</th>
<th width="80%">Parameter Value</th>
</tr>
<tr>
<td width="20%">
{% if k == 'threshold' %} {% if k == 'threshold' %}
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 %} {% endif %}
</td>
<td width="80%">
{% if k == 'threshold' %}
<input type="number" class="form-control" name="{{k}}" value="{{v}}"
min="0" max="1" step="0.05">
{% endif %}
</td>
</tr>
{% endfor %} {% endfor %}
</tbody>
</table>
</td> </td>
</tr> </tr>
{% endif %} {% endif %}
{% for k, v in user.prediction_settings.truncator.items %} {% for k, v in user.prediction_settings.truncator.items %}
<tr> <tr>
<td><p> <td>
{% if k == 'max_nodes' %} {% if k == 'max_nodes' %}
Max Nodes Max Nodes
{% elif k == 'max_depth' %} {% elif k == 'max_depth' %}
Max Depth Max Depth
{% endif %} {% endif %}
</p></td> </td>
<td><p> <td>
{% if k == 'max_nodes' %} {% if k == 'max_nodes' %}
<input type="number" class="form-control" name="{{k}}" value="{{v}}" min="1" <input
max="50" step="1"> type="number"
class="input input-bordered w-full"
name="{{ k }}"
value="{{ v }}"
min="1"
max="50"
step="1"
/>
{% elif k == 'max_depth' %} {% elif k == 'max_depth' %}
<input type="number" class="form-control" name="{{k}}" value="{{v}}" min="1" <input
max="8" step="1"> type="number"
class="input input-bordered w-full"
name="{{ k }}"
value="{{ v }}"
min="1"
max="8"
step="1"
/>
{% endif %} {% endif %}
</p></td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
</form> </form>
</div> </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>
<script>
$(function() {
$('#edit-prediction-setting-modal-submit').click(function(e){ <!-- Footer -->
e.preventDefault(); <div class="modal-action">
$('#edit-prediction-setting-modal-form').submit(); <button
}); type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
Close
</button>
<button
type="button"
class="btn btn-primary"
@click="submit('edit-prediction-setting-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>
}) <!-- Backdrop -->
</script> <form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

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

View File

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