11 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
136 changed files with 12185 additions and 9689 deletions

View File

@ -8,6 +8,7 @@ on:
jobs:
test:
if: ${{ !contains(gitea.event.pull_request.title, 'WIP') }}
runs-on: ubuntu-latest
services:
@ -99,6 +100,18 @@ jobs:
- name: Setup venv
run: |
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
run: |
@ -110,7 +123,12 @@ jobs:
source .venv/bin/activate
python manage.py migrate --noinput
- name: Run frontend tests
run: |
source .venv/bin/activate
python manage.py test --tag frontend
- name: Run Django tests
run: |
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
debug.log
scratches/
test-results/
data/

View File

@ -49,9 +49,23 @@ INSTALLED_APPS = [
"oauth2_provider",
# Custom
"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 = [
"django.contrib.auth.backends.ModelBackend",
]

View File

@ -23,12 +23,20 @@ from .api import api_v1, api_legacy
urlpatterns = [
path("", include("epdb.urls")),
path("", include("migration.urls")),
path("admin/", admin.site.urls),
path("api/v1/", api_v1.urls),
path("api/legacy/", api_legacy.urls),
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:
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 .models import (
User,
UserPackagePermission,
Group,
GroupPackagePermission,
Package,
MLRelativeReasoning,
EnviFormer,
Compound,
CompoundStructure,
SimpleAmbitRule,
ParallelRule,
Reaction,
Pathway,
Node,
Edge,
Scenario,
Setting,
EnviFormer,
ExternalDatabase,
ExternalIdentifier,
Group,
GroupPackagePermission,
JobLog,
License,
MLRelativeReasoning,
Node,
ParallelRule,
Pathway,
Reaction,
Scenario,
Setting,
SimpleAmbitRule,
User,
UserPackagePermission,
)
Package = s.GET_PACKAGE_MODEL()
class UserAdmin(admin.ModelAdmin):
list_display = ["username", "email", "is_active"]

View File

@ -1,4 +1,9 @@
import logging
from django.apps import AppConfig
from django.conf import settings
logger = logging.getLogger(__name__)
class EPDBConfig(AppConfig):
@ -7,3 +12,6 @@ class EPDBConfig(AppConfig):
def ready(self):
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 .models import Package
from django.conf import settings as s
def package_context(request):
@ -20,7 +20,7 @@ def package_context(request):
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
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.http import HttpResponse
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 .logic import PackageManager, UserManager, SettingManager
from utilities.misc import PackageExporter
from .logic import GroupManager, PackageManager, SettingManager, UserManager, SearchManager
from .models import (
Compound,
CompoundStructure,
Package,
Edge,
EPModel,
Node,
Pathway,
Reaction,
Rule,
Scenario,
SimpleAmbitRule,
User,
UserPackagePermission,
Rule,
Reaction,
Scenario,
Pathway,
Node,
Edge,
SimpleAmbitRule,
ParallelRule,
)
Package = s.GET_PACKAGE_MODEL()
def _anonymous_or_real(request):
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")
# router = Router(auth=SessionAuth())
router = Router()
router = Router(auth=SessionAuth(csrf=False))
class Error(Schema):
@ -118,13 +125,16 @@ class SimpleEdge(SimpleObject):
identifier: str = "edge"
class SimpleModel(SimpleObject):
identifier: str = "relative-reasoning"
################
# 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]):
from django.contrib.auth import authenticate
from django.contrib.auth import login
from django.contrib.auth import authenticate, login
email = User.objects.get(username=loginusername).email
user = authenticate(username=email, password=loginpassword)
@ -167,9 +177,13 @@ class UserSchema(Schema):
return SettingManager.get_all_settings(obj)
class Me(Schema):
whoami: str | None = None
@router.get("/user", response={200: UserWrapper, 403: Error})
def get_users(request, whoami: str = None):
if whoami:
def get_users(request, me: Query[Me]):
if me.whoami:
return {"user": [request.user]}
else:
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 #
###########
@ -251,67 +320,110 @@ def get_packages(request):
}
@router.get("/package/{uuid:package_uuid}", response={200: PackageSchema, 403: Error})
def get_package(request, package_uuid):
class GetPackage(Schema):
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:
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:
return 403, {
"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")
def create_packages(
request, packageName: Form[str], packageDescription: Optional[str] = Form(None)
request,
p: Form[CreatePackage],
):
try:
if packageName.strip() == "":
if p.packageName.strip() == "":
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)
except ValueError as 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})
def update_package(
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),
):
def update_package(request, package_uuid, pack: Form[UpdatePackage]):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
if hiddenMethod:
if hiddenMethod == "DELETE":
if pack.hiddenMethod:
if pack.hiddenMethod == "DELETE":
p.delete()
elif packageDescription and packageDescription.strip() != "":
p.description = packageDescription
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
elif pack.packageDescription is not None:
description = nh3.clean(pack.packageDescription, tags=s.ALLOWED_HTML_TAGS).strip()
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:
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 #
################################
@ -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 #
#########
@ -672,6 +861,73 @@ def _get_package_rule(request, package_uuid, rule_uuid):
# 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(
"/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 #
############
@ -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 #
############
@ -823,7 +1190,7 @@ class ScenarioSchema(Schema):
description: str = Field(None, alias="description")
id: str = Field(None, alias="url")
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")
pathways: List["SimplePathway"] = Field([], alias="related_pathways")
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 #
###########
@ -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")
def create_pathway(
request,
package_uuid,
smilesinput: Form[str],
name: Optional[str] = Form(None),
description: Optional[str] = Form(None),
rootOnly: Optional[str] = Form(None),
selectedSetting: Optional[str] = Form(None),
pw: Form[CreatePathway],
):
try:
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"
if rootOnly and rootOnly == "true":
if pw.rootOnly and pw.rootOnly.strip() == "true":
pw_mode = "build"
pw.kv.update({"mode": pw_mode})
pw.save()
new_pw.kv.update({"mode": pw_mode})
new_pw.save()
if pw_mode == "predict":
setting = request.user.prediction_settings()
if selectedSetting:
setting = SettingManager.get_setting_by_url(request.user, selectedSetting)
if pw.selectedSetting:
setting = SettingManager.get_setting_by_url(request.user, pw.selectedSetting)
pw.setting = setting
pw.save()
new_pw.setting = setting
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:
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 #
########
@ -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 #
###########

View File

@ -1,39 +1,40 @@
import re
import logging
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
import nh3
from django.conf import settings as s
from django.contrib.auth import get_user_model
from django.db import transaction
from django.conf import settings as s
from pydantic import ValidationError
from epdb.models import (
User,
Package,
UserPackagePermission,
GroupPackagePermission,
Permission,
Group,
Setting,
EPModel,
UserSettingPermission,
Rule,
Pathway,
Node,
Edge,
Compound,
Reaction,
CompoundStructure,
Edge,
EnzymeLink,
EPModel,
Group,
GroupPackagePermission,
Node,
Pathway,
Permission,
Reaction,
Rule,
Setting,
User,
UserPackagePermission,
UserSettingPermission,
)
from utilities.chem import FormatConverter
from utilities.misc import PackageImporter, PackageExporter
from utilities.misc import PackageExporter, PackageImporter
logger = logging.getLogger(__name__)
Package = s.GET_PACKAGE_MODEL()
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}"
@ -578,30 +579,39 @@ class PackageManager(object):
else:
_ = 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
@transaction.atomic
def import_legacy_package(
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 datetime import datetime
from uuid import UUID, uuid4
from envipy_additional_information import AdditionalInformationConverter
from .models import (
Package,
Compound,
CompoundStructure,
SimpleRule,
SimpleAmbitRule,
Edge,
Node,
ParallelRule,
Pathway,
Reaction,
Scenario,
SequentialRule,
SequentialRuleOrdering,
Reaction,
Pathway,
Node,
Edge,
Scenario,
SimpleAmbitRule,
SimpleRule,
)
from envipy_additional_information import AdditionalInformationConverter
pack = Package()
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.db import transaction
from epdb.models import MLRelativeReasoning, EnviFormer, Package
from epdb.models import EnviFormer, MLRelativeReasoning
Package = s.GET_PACKAGE_MODEL()
class Command(BaseCommand):
@ -75,11 +77,13 @@ class Command(BaseCommand):
return packages
# 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"Rule Packages (only for MLRR): {options['rule_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"])
eval_packages = decode_packages(options["eval_packages"])
rule_packages = decode_packages(options["rule_packages"])
@ -90,7 +94,7 @@ class Command(BaseCommand):
pack,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=options['threshold'],
threshold=options["threshold"],
name=f"EnviFormer - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description=f"EnviFormer transformer trained on {options['data_packages']} "
f"evaluated on {options['eval_packages']}.",
@ -101,7 +105,7 @@ class Command(BaseCommand):
rule_packages=rule_packages,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=options['threshold'],
threshold=options["threshold"],
name=f"ECC - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description=f"ML Relative Reasoning trained on {options['data_packages']} with rules from "
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.db import transaction
from epdb.models import EnviFormer, Package
from epdb.models import EnviFormer
Package = s.GET_PACKAGE_MODEL()
class Command(BaseCommand):

View File

@ -1,8 +1,8 @@
from django.apps import apps
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db.models import F, Value, TextField, JSONField
from django.db.models.functions import Replace, Cast
from django.db.models import F, JSONField, TextField, Value
from django.db.models.functions import Cast, Replace
from epdb.models import EnviPathModel
@ -23,10 +23,13 @@ class Command(BaseCommand):
)
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 = [
"User",
"Group",
"Package",
"Compound",
"CompoundStructure",
"Pathway",

View File

@ -2,40 +2,41 @@ import abc
import hashlib
import json
import logging
import math
import os
import secrets
from abc import abstractmethod
from collections import defaultdict
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
import math
import joblib
import nh3
import numpy as np
from django.conf import settings as s
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.postgres.fields import ArrayField
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.functional import cached_property
from envipy_additional_information import EnviPyModel
from model_utils.models import TimeStampedModel
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 utilities.chem import FormatConverter, ProductSet, PredictionResult, IndigoUtils
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
from utilities.ml import (
RuleBasedDataset,
ApplicabilityDomainPCA,
EnsembleClassifierChain,
RelativeReasoning,
EnviFormerDataset,
Dataset,
EnsembleClassifierChain,
EnviFormerDataset,
RelativeReasoning,
RuleBasedDataset,
)
logger = logging.getLogger(__name__)
@ -44,8 +45,6 @@ logger = logging.getLogger(__name__)
##########################
# User/Groups/Permission #
##########################
class User(AbstractUser):
email = models.EmailField(unique=True)
uuid = models.UUIDField(
@ -53,7 +52,10 @@ class User(AbstractUser):
)
url = models.TextField(blank=False, null=True, verbose_name="URL", unique=True)
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(
"Group",
@ -243,7 +245,7 @@ class UserPackagePermission(Permission):
)
user = models.ForeignKey("User", verbose_name="Permission to", on_delete=models.CASCADE)
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:
@ -259,7 +261,7 @@ class GroupPackagePermission(Permission):
)
group = models.ForeignKey("Group", verbose_name="Permission to", on_delete=models.CASCADE)
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:
@ -728,10 +730,13 @@ class Package(EnviPathModel):
rules = sorted(rules, key=lambda x: x.url)
return rules
class Meta:
swappable = "EPDB_PACKAGE_MODEL"
class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin):
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(
"CompoundStructure",
@ -781,7 +786,7 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
@staticmethod
@transaction.atomic
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":
if smiles is None or smiles.strip() == "":
raise ValueError("SMILES is required")
@ -1061,7 +1066,7 @@ class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
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
@ -1074,6 +1079,10 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
def apply(self, *args, **kwargs):
pass
@abc.abstractmethod
def get_rule_identifier(self) -> str:
pass
@staticmethod
def cls_for_type(rule_type: str):
if rule_type == "SimpleAmbitRule":
@ -1167,7 +1176,7 @@ class SimpleAmbitRule(SimpleRule):
@staticmethod
@transaction.atomic
def create(
package: Package,
package: "Package",
name: str = None,
description: str = None,
smirks: str = None,
@ -1228,6 +1237,9 @@ class SimpleAmbitRule(SimpleRule):
def _url(self):
return "{}/simple-ambit-rule/{}".format(self.package.url, self.uuid)
def get_rule_identifier(self) -> str:
return "simple-rule"
def apply(self, smiles):
return FormatConverter.apply(smiles, self.smirks)
@ -1241,7 +1253,7 @@ class SimpleAmbitRule(SimpleRule):
@property
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")
@property
@ -1273,6 +1285,9 @@ class ParallelRule(Rule):
def _url(self):
return "{}/parallel-rule/{}".format(self.package.url, self.uuid)
def get_rule_identifier(self) -> str:
return "parallel-rule"
@cached_property
def srs(self) -> QuerySet:
return self.simple_rules.all()
@ -1304,6 +1319,57 @@ class ParallelRule(Rule):
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):
simple_rules = models.ManyToManyField(
@ -1313,6 +1379,9 @@ class SequentialRule(Rule):
def _url(self):
return "{}/sequential-rule/{}".format(self.compound.url, self.uuid)
def get_rule_identifier(self) -> str:
return "sequential-rule"
@property
def srs(self):
return self.simple_rules.all()
@ -1333,7 +1402,7 @@ class SequentialRuleOrdering(models.Model):
class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin):
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(
"epdb.CompoundStructure", verbose_name="Educts", related_name="reaction_educts"
@ -1355,7 +1424,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
@staticmethod
@transaction.atomic
def create(
package: Package,
package: "Package",
name: str = None,
description: str = None,
educts: Union[List[str], List[CompoundStructure]] = None,
@ -1514,7 +1583,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
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(
"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):
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):
@ -2085,17 +2154,17 @@ class EPModel(PolymorphicModel, EnviPathModel):
class PackageBasedModel(EPModel):
rule_packages = models.ManyToManyField(
"Package",
s.EPDB_PACKAGE_MODEL,
verbose_name="Rule Packages",
related_name="%(app_label)s_%(class)s_rule_packages",
)
data_packages = models.ManyToManyField(
"Package",
s.EPDB_PACKAGE_MODEL,
verbose_name="Data Packages",
related_name="%(app_label)s_%(class)s_data_packages",
)
eval_packages = models.ManyToManyField(
"Package",
s.EPDB_PACKAGE_MODEL,
verbose_name="Evaluation Packages",
related_name="%(app_label)s_%(class)s_eval_packages",
)
@ -3400,7 +3469,7 @@ class PluginModel(EPModel):
class Scenario(EnviPathModel):
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_type = models.CharField(
@ -3555,7 +3624,7 @@ class Setting(EnviPathModel):
)
rule_packages = models.ManyToManyField(
"Package",
s.EPDB_PACKAGE_MODEL,
verbose_name="Setting Rule Packages",
related_name="setting_rule_packages",
blank=True,

View File

@ -6,14 +6,17 @@ from uuid import uuid4
from celery import shared_task
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.models import Edge, EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User
from epdb.models import Edge, EPModel, JobLog, Node, Pathway, Rule, Setting, User
logger = logging.getLogger(__name__)
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):
if model_pk not in ML_CACHE:

View File

@ -1,58 +1,60 @@
import json
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.contrib.auth import get_user_model
from django.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest
from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, JsonResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from envipy_additional_information import NAME_MAPPING
from oauth2_provider.decorators import protected_resource
import nh3
from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required
from utilities.misc import HTMLGenerator
from .logic import (
EPDBURLParser,
GroupManager,
PackageManager,
UserManager,
SettingManager,
SearchManager,
EPDBURLParser,
SettingManager,
UserManager,
)
from .models import (
Package,
GroupPackagePermission,
Group,
CompoundStructure,
APIToken,
Compound,
CompoundStructure,
Edge,
EnviFormer,
EnzymeLink,
EPModel,
ExternalDatabase,
ExternalIdentifier,
Group,
GroupPackagePermission,
JobLog,
License,
MLRelativeReasoning,
Node,
Pathway,
Permission,
Reaction,
Rule,
Pathway,
Node,
EPModel,
EnviFormer,
MLRelativeReasoning,
RuleBasedRelativeReasoning,
Scenario,
SimpleAmbitRule,
APIToken,
UserPackagePermission,
Permission,
License,
User,
Edge,
ExternalDatabase,
ExternalIdentifier,
EnzymeLink,
JobLog,
UserPackagePermission,
)
logger = logging.getLogger(__name__)
Package = s.GET_PACKAGE_MODEL()
def log_post_params(request):
if s.DEBUG:
@ -60,6 +62,26 @@ def log_post_params(request):
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):
context = get_base_context(request)
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)
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):
context = get_base_context(request)
@ -83,8 +147,7 @@ def login(request):
return render(request, "static/login.html", context)
elif request.method == "POST":
from django.contrib.auth import authenticate
from django.contrib.auth import login
from django.contrib.auth import authenticate, login
username = request.POST.get("username").strip()
if username != request.POST.get("username"):
@ -191,8 +254,8 @@ def register(request):
def editable(request, user):
if user.is_superuser:
return True
# if user.is_superuser:
# return True
url = request.build_absolute_uri(request.path)
if PackageManager.is_package_url(url):
@ -872,7 +935,7 @@ def package_models(request, package_uuid):
request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
)
from .tasks import dispatch, build_model
from .tasks import build_model, dispatch
dispatch(current_user, build_model, mod.pk)
@ -906,9 +969,10 @@ def package_model(request, package_uuid, model_uuid):
if classify:
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 = []
for pr in pred_res:
@ -1068,9 +1132,7 @@ def package(request, package_uuid):
return redirect(s.SERVER_URL + "/package")
elif hidden == "publish-package":
for g in Group.objects.filter(public=True):
PackageManager.update_permissions(
current_user, current_package, g, Permission.READ[0]
)
PackageManager.grant_read(current_user, current_package, g)
return redirect(current_package.url)
elif hidden == "copy":
object_to_copy = request.POST.get("object_to_copy")
@ -2409,9 +2471,9 @@ def package_scenarios(request, package_uuid):
context["unreviewed_objects"] = unreviewed_scenario_qs
from envipy_additional_information import (
SEDIMENT_ADDITIONAL_INFORMATION,
SLUDGE_ADDITIONAL_INFORMATION,
SOIL_ADDITIONAL_INFORMATION,
SEDIMENT_ADDITIONAL_INFORMATION,
)
context["scenario_types"] = {

View File

@ -1,24 +1,21 @@
import gzip
import json
import logging
import os.path
from datetime import datetime
from django.conf import settings as s
from django.http import HttpResponseNotAllowed
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.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__)
Package = s.GET_PACKAGE_MODEL()
def normalize_smiles(smiles):
m1 = Chem.MolFromSmiles(smiles)
@ -59,9 +56,7 @@ def run_both_engines(SMILES, SMIRKS):
set(
[
normalize_smiles(str(x))
for x in FormatConverter.sanitize_smiles(
[str(s) for s in all_rdkit_prods]
)[0]
for x in FormatConverter.sanitize_smiles([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"
)
ALL_SMILES = [
cs.smiles
for cs in CompoundStructure.objects.filter(compound__package=BBD)
cs.smiles for cs in CompoundStructure.objects.filter(compound__package=BBD)
]
RULES = SimpleAmbitRule.objects.filter(package=BBD)
@ -142,9 +136,7 @@ def migration(request):
)
for r in migration_status["results"]:
r["detail_url"] = r["detail_url"].replace(
"http://localhost:8000", s.SERVER_URL
)
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
context.update(**migration_status)
@ -152,8 +144,6 @@ def migration(request):
def migration_detail(request, package_uuid, rule_uuid):
current_user = _anonymous_or_real(request)
if request.method == "GET":
context = get_base_context(request)
@ -235,9 +225,7 @@ def compare(request):
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"
)
context["smiles"] = (
"C(CC(=O)N[C@@H](CS[Se-])C(=O)NCC(=O)[O-])[C@@H](C(=O)[O-])N"
)
context["smiles"] = "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)
elif request.method == "POST":

View File

@ -34,7 +34,7 @@ dependencies = [
[tool.uv.sources]
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-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" }
[project.optional-dependencies]
@ -45,6 +45,8 @@ dev = [
"poethepoet>=0.37.0",
"pre-commit>=4.3.0",
"ruff>=0.13.3",
"pytest-playwright>=0.7.1",
"pytest-django>=4.11.1",
]
[tool.ruff]
@ -66,47 +68,31 @@ docstring-code-format = true
[tool.poe.tasks]
# Main tasks
setup = { sequence = ["db-up", "migrate", "bootstrap"], help = "Complete setup: start database, run migrations, and bootstrap data" }
dev = { shell = """
# Start pnpm CSS watcher in background
pnpm run dev &
PNPM_PID=$!
echo "Started CSS watcher (PID: $PNPM_PID)"
# Cleanup function
cleanup() {
echo "\nShutting down..."
if kill -0 $PNPM_PID 2>/dev/null; then
kill $PNPM_PID
echo " CSS watcher stopped"
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" }
setup = { sequence = [
"db-up",
"migrate",
"bootstrap",
], help = "Complete setup: start database, run migrations, and bootstrap data" }
dev = { cmd = "uv run python scripts/dev_server.py", 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
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" }
# 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
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." }
# Django tasks
@ -124,6 +110,14 @@ echo " Password: SuperSafe"
""", help = "Bootstrap initial data (anonymous user, packages, models)" }
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"] }
collectstatic = { cmd = "uv run python manage.py collectstatic --noinput", help = "Collect static files for production", deps = ["build-frontend"] }
build-frontend = { cmd = "uv run python scripts/pnpm_wrapper.py run build", help = "Build frontend assets using pnpm", deps = [
"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";
/* 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
*/
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
return excerpt
const cleaned = excerpt
.replace(/<[^>]*>/g, '') // Remove HTML tags
.replace(/&nbsp;/g, ' ') // Replace &nbsp; with spaces
.replace(/&amp;/g, '&') // Replace &amp; with &
.replace(/&lt;/g, '<') // Replace &lt; with <
.replace(/&gt;/g, '>') // Replace &gt; with >
.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")
function predictFromNode(url) {
$.post("", {node: url})
.done(function (data) {
fetch("", {
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);
window.location.href = data.success;
})
.fail(function (xhr, status, error) {
console.error("Error:", xhr.status, xhr.responseText);
// show user-friendly message or log error
.catch(error => {
console.error("Error:", error);
});
}
@ -103,6 +110,9 @@ function draw(pathway, elem) {
}
function dragstarted(event, d) {
// Prevent zoom pan when dragging nodes
event.sourceEvent.stopPropagation();
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
@ -117,6 +127,9 @@ function draw(pathway, elem) {
}
function dragged(event, d) {
// Prevent zoom pan when dragging nodes
event.sourceEvent.stopPropagation();
d.fx = event.x;
d.fy = event.y;
@ -127,6 +140,9 @@ function draw(pathway, elem) {
}
function dragended(event, d) {
// Prevent zoom pan when dragging nodes
event.sourceEvent.stopPropagation();
if (!event.active) simulation.alphaTarget(0);
// 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"));
}
// Wait one second before showing popup
// Wait before showing popup (ms)
var popupWaitBeforeShow = 1000;
// Keep Popup at least for one second
var popushowAtLeast = 1000;
function pop_show_e(element) {
var e = element;
setTimeout(function () {
if ($(e).is(':hover')) { // if element is still hovered
$(e).popover("show");
// Custom popover element
let popoverTimeout = null;
// workaround to set fixed positions
pop = $(e).attr("aria-describedby")
h = $('#' + pop).height();
$('#' + pop).attr("style", `position: fixed; top: ${clientY - (h / 2.0)}px; left: ${clientX + 10}px; margin: 0px; max-width: 1000px; display: block;`)
setTimeout(function () {
var close = setInterval(function () {
if (!$(".popover:hover").length // mouse outside popover
&& !$(e).is(':hover')) { // mouse outside element
$(e).popover('hide');
clearInterval(close);
function createPopover() {
const popover = document.createElement('div');
popover.id = 'custom-popover';
popover.className = 'fixed z-50';
popover.style.cssText = `
background: #ffffff;
border: 1px solid #d1d5db;
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
max-width: 320px;
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);
}, popushowAtLeast);
#custom-popover a {
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) {
objects.attr("id", "pop")
.attr("data-container", "body")
.attr("data-toggle", "popover")
.attr("data-placement", "right")
.attr("title", title);
objects.each(function (d) {
const element = this;
objects.each(function (d, i) {
options = {trigger: "manual", html: true, animation: false};
this_ = this;
var p = $(this).popover(options).on("mouseenter", function () {
pop_show_e(this);
element.addEventListener('mouseenter', () => {
if (popoverTimeout) clearTimeout(popoverTimeout);
popoverTimeout = setTimeout(() => {
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
p.attr('data-content', contentFunction(d));
p.data("bs.popover").setContent();
p.data("bs.popover").tip().css({"max-width": "1000px"});
element.addEventListener('mouseleave', () => {
if (popoverTimeout) {
clearTimeout(popoverTimeout);
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) {
popupContent += '<b>Half-lives and related scenarios:</b><br>'
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;
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;
@ -285,7 +402,7 @@ function draw(pathway, elem) {
popupContent += adcontent;
}
popupContent += "<img src='" + e.image + "' width='" + 20 * nodeRadius + "'><br>"
popupContent += "<img src='" + e.image + "'><br>"
if (e.reaction_probability) {
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 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
const zoom = d3.zoom()
@ -316,7 +450,12 @@ function draw(pathway, elem) {
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'];
links = pathway['links'];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,28 +1,32 @@
{% extends "framework.html" %}
{% extends "framework_modern.html" %}
{% load static %}
{% block content %}
{% if object_type != 'package' %}
<div>
<div id="load-all-error" style="display: none;">
<div class="alert alert-danger" role="alert">
<span
class="glyphicon glyphicon-exclamation-sign"
aria-hidden="true"
></span>
<span class="sr-only">Error:</span>
Getting objects failed!
</div>
</div>
{# Serialize objects data for Alpine pagination #}
{# prettier-ignore-start #}
{# FIXME: This is a hack to get the objects data into the JavaScript code. #}
<script>
window.reviewedObjects = [
{% for obj in reviewed_objects %}
{ "name": "{{ obj.name|escapejs }}", "url": "{{ obj.url }}" }{% if not forloop.last %},{% endif %}
{% endfor %}
];
window.unreviewedObjects = [
{% for obj in unreviewed_objects %}
{ "name": "{{ obj.name|escapejs }}", "url": "{{ obj.url }}" }{% if not forloop.last %},{% endif %}
{% endfor %}
];
</script>
{# prettier-ignore-end #}
{% if object_type != 'package' %}
<div class="px-8 py-4">
<input
type="text"
id="object-search"
class="form-control"
class="input input-bordered hidden w-full max-w-xs"
placeholder="Search by name"
style="display: none;"
/>
<p></p>
</div>
{% endif %}
@ -56,13 +60,12 @@
{% endif %}
{% endblock action_modals %}
<div class="panel-group" id="reviewListAccordion">
<div class="panel panel-default">
<div
class="panel-heading"
id="headingPanel"
style="font-size:2rem;height: 46px"
>
<div class="px-8 py-4">
<!-- Header Section -->
<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' %}
Packages
{% elif object_type == 'compound' %}
@ -90,22 +93,31 @@
{% elif object_type == 'group' %}
Groups
{% endif %}
<div
id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"
</h2>
<div id="actionsButton" class="dropdown dropdown-end hidden">
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
<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-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"
>
<a
href="#"
class="dropdown-toggle"
data-toggle="dropdown"
role="button"
aria-haspopup="true"
aria-expanded="false"
><span class="glyphicon glyphicon-wrench"></span> Actions
<span class="caret"></span><span style="padding-right:1em"></span
></a>
<ul id="actionsList" class="dropdown-menu">
{% block actions %}
{% if object_type == 'package' %}
{% include "actions/collections/package.html" %}
@ -136,7 +148,7 @@
</ul>
</div>
</div>
<div class="panel-body">
<div class="mt-2">
<!-- Set Text above links -->
{% if object_type == 'package' %}
<p>
@ -145,7 +157,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/packages"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -156,7 +168,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/compounds"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -166,7 +178,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/compounds"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -177,7 +189,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/Rules"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -188,7 +200,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/reactions"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -199,7 +211,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/pathways"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -209,7 +221,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/nodes"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -219,7 +231,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/edges"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -230,7 +242,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/scenarios"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -241,17 +253,18 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/relative_reasoning"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
{% elif object_type == 'setting' %}
<p>
A setting includes configuration parameters for pathway predictions.
A setting includes configuration parameters for pathway
predictions.
<a
target="_blank"
href="https://wiki.envipath.org/index.php/settings"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -262,7 +275,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/users"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -272,7 +285,7 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/groups"
role="button"
class="link link-primary"
>Learn more &gt;&gt;</a
>
</p>
@ -281,7 +294,7 @@
<!-- If theres nothing to show extend the text above -->
{% if reviewed_objects and unreviewed_objects %}
{% if reviewed_objects|length == 0 and unreviewed_objects|length == 0 %}
<p>
<p class="mt-4">
Nothing found. There are two possible reasons: <br /><br />1.
There is no content yet.<br />2. You have no reading
permissions.<br /><br />Please be sure you have at least reading
@ -290,189 +303,231 @@
{% endif %}
{% endif %}
</div>
</div>
</div>
<!-- Lists Container - Full Width with Reviewed on Right -->
<div class="w-full">
{% if reviewed_objects %}
{% if reviewed_objects|length > 0 %}
<!-- Reviewed -->
<div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
class="collapse-arrow bg-base-200 collapse order-2 w-full"
x-data="paginatedList(window.reviewedObjects || [], { isReviewed: true, instanceId: 'reviewed' })"
>
<h4 class="panel-title">
<a
id="ReviewedLink"
data-toggle="collapse"
data-parent="#reviewListAccordion"
href="#Reviewed"
>Reviewed</a
>
</h4>
</div>
<div id="Reviewed" class="panel-collapse in collapse">
<div class="panel-body list-group-item" id="ReviewedContent">
{% if object_type == 'package' %}
{% for obj in reviewed_objects %}
<a class="list-group-item" href="{{ obj.url }}"
>{{ obj.name|safe }}
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
Reviewed
<span
class="glyphicon glyphicon-star"
aria-hidden="true"
style="float:right"
data-toggle="tooltip"
data-placement="top"
title=""
data-original-title="Reviewed"
class="badge badge-sm badge-neutral ml-2"
x-text="totalItems"
></span>
</div>
<div class="collapse-content w-full">
<ul class="menu bg-base-100 rounded-box w-full">
<template x-for="obj in paginatedItems" :key="obj.url">
<li>
<a :href="obj.url" class="hover:bg-base-200">
<span x-text="obj.name"></span>
<span
class="tooltip tooltip-left ml-auto"
data-tip="Reviewed"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-star"
>
<polygon
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
/>
</svg>
</span>
</a>
{% endfor %}
{% else %}
{% for obj in reviewed_objects|slice:":50" %}
<a class="list-group-item" href="{{ obj.url }}"
>{{ obj.name|safe }}{# <i>({{ obj.package.name }})</i> #}
<span
class="glyphicon glyphicon-star"
aria-hidden="true"
style="float:right"
data-toggle="tooltip"
data-placement="top"
title=""
data-original-title="Reviewed"
</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>
</a>
{% endfor %}
{% endif %}
<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>
{% endif %}
{% endif %}
{% if unreviewed_objects %}
<!-- Unreviewed -->
<div
class="panel panel-default panel-heading list-group-item"
style="background-color:silver"
class="collapse-arrow bg-base-200 collapse order-1 w-full"
x-data="paginatedList(window.unreviewedObjects || [], { isReviewed: false, instanceId: 'unreviewed' })"
>
<h4 class="panel-title">
<input
type="checkbox"
{% if reviewed_objects|length == 0 or object_type == 'package' %}checked{% endif %}
/>
<div class="collapse-title text-xl font-medium">
Unreviewed
<span
class="badge badge-sm badge-neutral ml-2"
x-text="totalItems"
></span>
</div>
<div class="collapse-content w-full">
<ul class="menu bg-base-100 rounded-box w-full">
<template x-for="obj in paginatedItems" :key="obj.url">
<li>
<a
id="UnreviewedLink"
data-toggle="collapse"
data-parent="#unReviewListAccordion"
href="#Unreviewed"
>Unreviewed</a
>
</h4>
</div>
:href="obj.url"
class="hover:bg-base-200"
x-text="obj.name"
></a>
</li>
</template>
</ul>
<!-- Pagination Controls -->
<div
id="Unreviewed"
class="panel-collapse {% if reviewed_objects|length == 0 or object_type == 'package' %}in{% endif %} collapse"
x-show="totalPages > 1"
class="mt-4 flex items-center justify-between px-2"
>
<div class="panel-body list-group-item" id="UnreviewedContent">
{% if object_type == 'package' %}
{% for obj in unreviewed_objects %}
<a class="list-group-item" href="{{ obj.url }}"
>{{ obj.name|safe }}</a
<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()"
>
{% endfor %}
{% else %}
{% for obj in unreviewed_objects|slice:":50" %}
<a class="list-group-item" href="{{ obj.url }}"
>{{ obj.name|safe }}</a
«
</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()"
>
{% endfor %}
{% endif %}
»
</button>
</div>
</div>
</div>
</div>
{% endif %}
</div>
{% if objects %}
<!-- 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 %}
{% 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 %}
<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 %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
</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>
{# prettier-ignore-start #}
<script>
$(function () {
$('#object-search').show();
{% if object_type != 'package' and object_type != 'user' and object_type != 'group' %}
{% if reviewed_objects|length > 50 or unreviewed_objects|length > 50 %}
$('#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>');
}
document.addEventListener("DOMContentLoaded", function () {
// Show actions button if there are actions
const actionsButton = document.getElementById("actionsButton");
const actionsList = actionsButton?.querySelector("ul");
if (actionsList && actionsList.children.length > 0) {
actionsButton?.classList.remove("hidden");
}
$('#load-all-loading').hide();
$('#load-remaining').hide();
}).fail(function (resp) {
$('#load-all-loading').hide();
$('#load-all-error').show();
// Show search input and connect to Alpine pagination
const objectSearch = document.getElementById("object-search");
if (objectSearch) {
objectSearch.classList.remove("hidden");
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);
{% endif %}
{% endif %}
$('#modal-form-delete-submit').on('click', function (e) {
// Delete form submit handler
const deleteSubmit = document.getElementById("modal-form-delete-submit");
const deleteForm = document.getElementById("modal-form-delete");
if (deleteSubmit && deleteForm) {
deleteSubmit.addEventListener("click", function (e) {
e.preventDefault();
$('#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>
{# prettier-ignore-end #}
{% endblock content %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -228,15 +228,7 @@
>Documentation Wiki</a
>
</li>
<li>
<a
href="#"
id="citeButton"
data-toggle="modal"
data-target="#citemodal"
>How to cite enviPath</a
>
</li>
<li class="divider"></li>
<li><a>Version: {{ meta.version }}</a></li>
</ul>
@ -408,10 +400,5 @@
}
});
</script>
{% block modals %}
{% include "modals/cite_modal.html" %}
{% include "modals/predict_modal.html" %}
{% include "modals/batch_predict_modal.html" %}
{% endblock %}
</body>
</html>

View File

@ -21,8 +21,14 @@
type="text/css"
/>
{# jQuery - Keep for compatibility with existing JS #}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
{# Alpine.js - For reactive components #}
<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 #}
<link
@ -35,21 +41,10 @@
<script>
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>
{# General EP JS #}
<script src="{% static 'js/pps.js' %}"></script>
{# Modal Steps for Stepwise Modal Wizards #}
<script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script>
{% if not debug %}
<!-- Matomo -->
@ -171,10 +166,11 @@
{% endblock %}
<script>
$(function () {
// Hide actionsbutton if there's no action defined
if ($("#actionsButton ul").children().length > 0) {
$("#actionsButton").show();
document.addEventListener("DOMContentLoaded", function () {
// Show actions button if there are actions defined
const actionsButtonUl = document.querySelector("#actionsButton ul");
if (actionsButtonUl && actionsButtonUl.children.length > 0) {
document.getElementById("actionsButton").style.display = "";
}
});

View File

@ -1,8 +1,36 @@
{% load static %}
{# Modern DaisyUI Navbar #}
<div class="navbar x-50 bg-neutral-50 text-neutral-950 shadow-lg">
{# Modern DaisyUI Navbar with Mobile Drawer Menu #}
<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">
<a href="{{ meta.server_url }}" class="btn btn-ghost text-xl normal-case">
{# Hamburger menu button - visible on mobile, hidden on desktop #}
{% 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" />
</svg>
@ -10,6 +38,7 @@
</div>
{% if not public_mode %}
{# Desktop menu - hidden on mobile, visible on desktop #}
<div class="navbar-center hidden lg:flex">
<a
href="{{ meta.server_url }}/predict"
@ -18,8 +47,6 @@
id="predictLink"
>Predict</a
>
<!-- <li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li> -->
<!--<li><a href="{{ meta.server_url }}/browse" id="browseLink">Browse</a></li>-->
<div class="dropdown dropdown-center">
<div tabindex="0" role="button" class="btn btn-ghost">Browse</div>
<ul
@ -27,12 +54,18 @@
class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm"
>
<li>
<a href="{{ meta.server_url }}/package" id="packageLink">Package</a>
<a href="{{ meta.server_url }}/package" id="packageLink"
>Package</a
>
</li>
<li>
<a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a>
<a href="{{ meta.server_url }}/pathway" id="pathwayLink"
>Pathway</a
>
</li>
<li>
<a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a>
</li>
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li>
<li>
<a href="{{ meta.server_url }}/compound" id="compoundLink"
>Compound</a
@ -44,7 +77,9 @@
>
</li>
<li>
<a href="{{ meta.server_url }}/model" id="relative-reasoningLink"
<a
href="{{ meta.server_url }}/model"
id="relative-reasoningLink"
>Model</a
>
</li>
@ -114,7 +149,9 @@
tabindex="-1"
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-sm"
>
<li><a href="{{ meta.user.url }}" id="accountbutton">Settings</a></li>
<li>
<a href="{{ meta.user.url }}" id="accountbutton">Settings</a>
</li>
<li>
<form
id="logoutForm"
@ -136,6 +173,96 @@
</div>
{% endif %}
</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>
<script>

View File

@ -9,7 +9,7 @@
>
<div class="hero-overlay"></div>
<!-- Predict Pathway text over the image -->
<div class="absolute bottom-40 left-1/8 z-10 -translate-x-8">
<div class="absolute bottom-40 left-1/8 -translate-x-8">
<h2 class="text-base-100 text-left text-3xl text-shadow-lg">
Predict Your Pathway
</h2>
@ -20,16 +20,68 @@
<div class="bg-base-200 mx-auto max-w-5xl shadow-md">
<!-- Predict Pathway Section -->
<div
class="relative z-20 mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
class="relative mx-auto -mt-32 mb-10 w-full flex-col lg:flex-row-reverse"
>
<div
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="my-4 ml-8 flex h-fit flex-row items-center justify-start">
<div class="flex items-center gap-1">
<label class="swap btn btn-ghost btn-sm p-1" title="Input Mode">
<input type="checkbox" />
<input type="checkbox" x-model="drawMode" />
<span class="swap-on flex items-center gap-1">
<div
class="bg-neutral/50 text-neutral-content flex items-center justify-center rounded-full p-1"
@ -82,16 +134,24 @@
<fieldset
class="fieldset overflow-hidden transition-all duration-300 ease-in-out"
:class="drawMode ? 'p-4' : 'p-8'"
>
<form
id="index-form"
action="{{ meta.current_package.url }}/pathway"
method="POST"
@submit.prevent="submitForm()"
>
{% csrf_token %}
<div
id="text-input-container"
class="scale-100 transform opacity-100 transition-all duration-300 ease-in-out"
x-show="!drawMode"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
>
<div class="join mx-auto w-full">
<input
@ -99,6 +159,7 @@
id="index-form-text-input"
placeholder="canonical SMILES string"
class="input input-md join-item grow"
x-model="smiles"
/>
<button class="btn btn-neutral join-item">Predict!</button>
</div>
@ -107,26 +168,35 @@
<a
href="#"
class="example-link hover:text-primary cursor-pointer"
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
title="load example"
@click.prevent="loadExample('CN1C=NC2=C1C(=O)N(C(=O)N2C)C', $el)"
>Caffeine</a
>
<a
href="#"
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"
@click.prevent="loadExample('CC(C)CC1=CC=C(C=C1)C(C)C(=O)O', $el)"
>Ibuprofen</a
>
</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
>
</div>
</div>
<div
id="ketcher-container"
class="hidden w-full scale-95 transform opacity-0 transition-all duration-300 ease-in-out"
x-show="drawMode"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-300"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="w-full"
>
<iframe
id="index-ketcher"
@ -256,6 +326,31 @@
<script language="javascript">
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
// Function to render Discourse topics into cards
@ -278,16 +373,13 @@
const date = new Date(topic.created_at).toLocaleDateString();
return `
<div class="card bg-white shadow-xs hover:shadow-lg transition-shadow duration-300 h-64 w-75 flex-shrink-0">
<div class="card-body flex flex-col h-full">
<h3 class="card-title leading-tight font-normal tracking-tight h-12 mb-2 line-clamp-2 text-ellipsis wrap-break-word overflow-hidden">
<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 justify-between">
<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">
${topic.title}
</a>
</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 items-center gap-2">
@ -313,141 +405,20 @@
// Make render function globally available
window.renderDiscourseTopics = renderDiscourseTopics;
// Toggle functionality with smooth animations
function toggleInputMode() {
const toggle = $('input[type="checkbox"]');
const textContainer = $("#text-input-container");
const ketcherContainer = $("#ketcher-container");
const formCard = $(".card");
const fieldset = $(".fieldset");
if (toggle.is(":checked")) {
// Draw mode - show Ketcher, hide text input
textContainer.addClass("opacity-0 transform scale-95");
textContainer.removeClass("opacity-100 transform scale-100");
// Adjust fieldset padding for Ketcher mode - reduce padding and make more compact
fieldset.removeClass("p-8");
fieldset.addClass("p-4");
// Wait for fade out to complete, then hide and show new content
setTimeout(() => {
textContainer.addClass("hidden");
ketcherContainer.removeClass("hidden opacity-0 transform scale-95");
ketcherContainer.addClass("opacity-100 transform scale-100");
// Force re-evaluation of iframe size
const iframe = document.getElementById("index-ketcher");
if (iframe) {
iframe.style.height = "400px";
}
}, 300);
} else {
// SMILES mode - show text input, hide Ketcher
ketcherContainer.addClass("opacity-0 transform scale-95");
ketcherContainer.removeClass("opacity-100 transform scale-100");
// Restore fieldset padding for text input mode
fieldset.removeClass("p-4");
fieldset.addClass("p-8");
// Wait for fade out to complete, then hide and show new content
setTimeout(() => {
ketcherContainer.addClass("hidden");
textContainer.removeClass("hidden opacity-0 transform scale-95");
textContainer.addClass("opacity-100 transform scale-100");
}, 300);
// Transfer SMILES from Ketcher to text input if available
if (window.indexKetcher && window.indexKetcher.getSmiles) {
const smiles = window.indexKetcher.getSmiles();
if (smiles && smiles.trim() !== "") {
$("#index-form-text-input").val(smiles);
}
}
}
}
// Ketcher integration
function indexKetcherToTextInput() {
$("#index-form-smiles").val(this.ketcher.getSmiles());
}
$(function () {
// Initialize fieldset with proper padding
$(".fieldset").addClass("p-8");
// Toggle event listener
$('input[type="checkbox"]').on("change", toggleInputMode);
// Ketcher iframe load handler
$("#index-ketcher").on("load", function () {
// Ketcher iframe load handler - set up change event to sync SMILES
document.addEventListener("DOMContentLoaded", function () {
const indexKetcher = document.getElementById("index-ketcher");
indexKetcher.addEventListener("load", function () {
const checkKetcherReady = () => {
const win = this.contentWindow;
if (win.ketcher && "editor" in win.ketcher) {
window.indexKetcher = win.ketcher;
win.ketcher.editor.event.change.handlers.push({
once: false,
priority: 0,
f: indexKetcherToTextInput,
ketcher: win.ketcher,
});
} else {
setTimeout(checkKetcherReady, 100);
}
};
checkKetcherReady();
});
// Handle example link clicks
$(".example-link").on("click", function (e) {
e.preventDefault();
const smiles = $(this).data("smiles");
const title = $(this).attr("title");
// Check if we're in Ketcher mode or text input mode
if ($('input[type="checkbox"]').is(":checked")) {
// In Ketcher mode - set the SMILES in Ketcher
if (window.indexKetcher && window.indexKetcher.setMolecule) {
window.indexKetcher.setMolecule(smiles);
}
} else {
// In text input mode - set the SMILES in the text input
$("#index-form-text-input").val(smiles);
}
// Show a brief feedback
const originalText = $(this).text();
$(this).text(`loaded!`);
setTimeout(() => {
$(this).text(originalText);
}, 1000);
});
// Handle form submission on Enter
$("#index-form").on("submit", function (e) {
e.preventDefault();
var textSmiles = "";
// Check if we're in Ketcher mode and extract SMILES
if ($('input[type="checkbox"]').is(":checked") && window.indexKetcher) {
textSmiles = window.indexKetcher.getSmiles().trim();
} else {
textSmiles = $("#index-form-text-input").val().trim();
}
if (textSmiles === "") {
return;
}
$("#index-form-smiles").val(textSmiles);
$("#index-form").attr("action", currentPackage + "/pathway");
$("#index-form").attr("method", "POST");
this.submit();
});
// Discourse topics are now loaded automatically by discourse-api.js
});
</script>
{% endblock main_content %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,30 +1,66 @@
<div
class="modal fade"
tabindex="-1"
<dialog
id="new_model_modal"
role="dialog"
aria-labelledby="new_model_modal"
aria-hidden="true"
class="modal"
x-data="{
isSubmitting: false,
modelType: '',
buildAppDomain: false,
reset() {
this.isSubmitting = false;
this.modelType = '';
this.buildAppDomain = false;
},
get showMlrr() {
return this.modelType === 'mlrr';
},
get showRbrr() {
return this.modelType === 'rbrr';
},
get showEnviformer() {
return this.modelType === 'enviformer';
},
submit(formId) {
const form = document.getElementById(formId);
if (form && form.checkValidity()) {
this.isSubmitting = true;
form.submit();
} else if (form) {
form.reportValidity();
}
}
}"
@close="reset()"
>
<div class="modal-dialog modal-lg">
<div class="modal-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>
<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>
<h4 class="modal-title">New Model</h4>
</div>
<div class="modal-body">
</form>
<!-- Body -->
<div class="py-4">
<form
id="new_model_form"
accept-charset="UTF-8"
action="{{ meta.current_package.url }}/model"
data-remote="true"
method="post"
>
{% csrf_token %}
<div class="jumbotron">
<div class="alert alert-info mb-4">
<span>
Create a new Model to limit the number of degradation products in
the prediction. You just need to set a name and the packages you
want the object to be based on. There are multiple types of models
@ -32,122 +68,158 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/relative-reasoning"
role="button"
class="link"
>wiki &gt;&gt;</a
>
</span>
</div>
<!-- Name -->
<label for="model-name">Name</label>
<div class="form-control mb-3">
<label class="label" for="model-name">
<span class="label-text">Name</span>
</label>
<input
id="model-name"
name="model-name"
class="form-control"
class="input input-bordered w-full"
placeholder="Name"
required
/>
</div>
<!-- Description -->
<label for="model-description">Description</label>
<div class="form-control mb-3">
<label class="label" for="model-description">
<span class="label-text">Description</span>
</label>
<input
id="model-description"
name="model-description"
class="form-control"
class="input input-bordered w-full"
placeholder="Description"
/>
</div>
<!-- Model Type -->
<label for="model-type">Model Type</label>
<div class="form-control mb-3">
<label class="label" for="model-type">
<span class="label-text">Model Type</span>
</label>
<select
id="model-type"
name="model-type"
class="form-control"
data-width="100%"
class="select select-bordered w-full"
x-model="modelType"
required
>
<option disabled selected>Select Model Type</option>
<option value="" disabled selected>Select Model Type</option>
{% for k, v in model_types.items %}
<option value="{{ v }}">{{ k }}</option>
{% endfor %}
</select>
</div>
<!-- Rule Packages -->
<div id="rule-packages" class="ep-model-param mlrr rbrr">
<label for="model-rule-packages">Rule Packages</label>
<!-- Rule Packages (MLRR, RBRR) -->
<div class="form-control mb-3" x-show="showMlrr || showRbrr" x-cloak>
<label class="label" for="model-rule-packages">
<span class="label-text">Rule Packages</span>
</label>
<select
id="model-rule-packages"
name="model-rule-packages"
data-actions-box="true"
class="form-control"
class="select select-bordered w-full h-32"
multiple
data-width="100%"
>
<option disabled>Reviewed Packages</option>
<optgroup label="Reviewed Packages">
{% for obj in meta.readable_packages %}
{% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
<option disabled>Unreviewed Packages</option>
</optgroup>
<optgroup label="Unreviewed Packages">
{% for obj in meta.readable_packages %}
{% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
</optgroup>
</select>
<label class="label">
<span class="label-text-alt">Hold Ctrl/Cmd to select multiple</span>
</label>
</div>
<!-- Data Packages -->
<div id="data-packages" class="ep-model-param mlrr rbrr enviformer">
<label for="model-data-packages">Data Packages</label>
<!-- Data Packages (MLRR, RBRR, Enviformer) -->
<div
class="form-control mb-3"
x-show="showMlrr || showRbrr || showEnviformer"
x-cloak
>
<label class="label" for="model-data-packages">
<span class="label-text">Data Packages</span>
</label>
<select
id="model-data-packages"
name="model-data-packages"
data-actions-box="true"
class="form-control"
class="select select-bordered w-full h-32"
multiple
data-width="100%"
>
<option disabled>Reviewed Packages</option>
<optgroup label="Reviewed Packages">
{% for obj in meta.readable_packages %}
{% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
<option disabled>Unreviewed Packages</option>
</optgroup>
<optgroup label="Unreviewed Packages">
{% for obj in meta.readable_packages %}
{% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
</optgroup>
</select>
<label class="label">
<span class="label-text-alt">Hold Ctrl/Cmd to select multiple</span>
</label>
</div>
<!-- Fingerprinter -->
<div id="fingerprinter" class="ep-model-param mlrr">
<label for="model-fingerprinter">Fingerprinter</label>
<!-- Fingerprinter (MLRR) -->
<div class="form-control mb-3" x-show="showMlrr" x-cloak>
<label class="label" for="model-fingerprinter">
<span class="label-text">Fingerprinter</span>
</label>
<select
id="model-fingerprinter"
name="model-fingerprinter"
data-actions-box="true"
class="form-control"
class="select select-bordered w-full h-32"
multiple
data-width="100%"
>
<option value="MACCS" selected>MACCS Fingerprinter</option>
{% 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 %}
<option value="{{ v }}">{{ k }}</option>
{% endfor %}
</optgroup>
{% endif %}
</select>
<label class="label">
<span class="label-text-alt">Hold Ctrl/Cmd to select multiple</span>
</label>
</div>
<!-- Threshold -->
<div id="threshold" class="ep-model-param mlrr enviformer">
<label for="model-threshold">Threshold</label>
<!-- Threshold (MLRR, Enviformer) -->
<div
class="form-control mb-3"
x-show="showMlrr || showEnviformer"
x-cloak
>
<label class="label" for="model-threshold">
<span class="label-text">Threshold</span>
</label>
<input
type="number"
min="0"
@ -156,115 +228,110 @@
value="0.5"
id="model-threshold"
name="model-threshold"
class="form-control"
class="input input-bordered w-full"
/>
</div>
<div id="appdomain" class="ep-model-param mlrr">
<!-- Applicability Domain (MLRR) -->
{% if meta.enabled_features.APPLICABILITY_DOMAIN %}
<!-- Build AD? -->
<div class="checkbox">
<label>
<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"
/>Also build an Applicability Domain?
class="checkbox"
x-model="buildAppDomain"
/>
<span class="label-text"
>Also build an Applicability Domain?</span
>
</label>
</div>
<div id="ad-params" style="display:none">
<!-- Num Neighbors -->
<label for="num-neighbors">Number of Neighbors</label>
<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="form-control"
class="input input-bordered w-full"
value="5"
step="1"
min="0"
max="10"
/>
<!-- Local Compatibility -->
<label for="local-compatibility-threshold"
>Local Compatibility Threshold</label
>
</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="form-control"
class="input input-bordered w-full"
value="0.5"
step="0.01"
min="0"
max="1"
/>
<!-- Reliability -->
<label for="reliability-threshold">Reliability Threshold</label>
</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="form-control"
class="input input-bordered w-full"
value="0.5"
step="0.01"
min="0"
max="1"
/>
</div>
{% endif %}
</div>
</div>
{% endif %}
</form>
</div>
<div class="modal-footer">
<a id="new_model_modal_form_submit" class="btn btn-primary" href="#"
>Submit</a
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
<button type="button" class="btn btn-default" data-dismiss="modal">
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>
<script>
$(function () {
// Built in Model Types
var nativeModelTypes = ["mlrr", "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>
<!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

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

View File

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

View File

@ -1,185 +1,260 @@
{% load static %}
<div id="new_prediction_setting_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Create a Prediction Setting</h5>
<dialog
id="new_prediction_setting_modal"
class="modal"
x-data="{
isSubmitting: false,
tpMethod: '',
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
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
:disabled="isSubmitting"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>
</form>
<!-- Body -->
<div class="py-4">
<p class="mb-4">
To create a Prediction Setting fill the form below and click "Create"
</p>
<form
id="new-prediction-setting-modal-form"
accept-charset="UTF-8"
action=""
data-remote="true"
method="post"
>
{% csrf_token %}
<label for="prediction-setting-name">Name</label>
<div class="form-control mb-3">
<label class="label" for="prediction-setting-name">
<span class="label-text">Name</span>
</label>
<input
id="prediction-setting-name"
name="prediction-setting-name"
class="form-control"
class="input input-bordered w-full"
placeholder="Name"
required
/>
<label for="prediction-setting-description">Description</label>
</div>
<div class="form-control mb-3">
<label class="label" for="prediction-setting-description">
<span class="label-text">Description</span>
</label>
<input
id="prediction-setting-description"
name="prediction-setting-description"
class="form-control"
class="input input-bordered w-full"
placeholder="Description"
/>
</div>
<label for="prediction-setting-max-nodes">Max #Nodes</label>
<div class="form-control mb-3">
<label class="label" for="prediction-setting-max-nodes">
<span class="label-text">Max #Nodes</span>
</label>
<input
id="prediction-setting-max-nodes"
type="number"
class="form-control"
class="input input-bordered w-full"
name="prediction-setting-max-nodes"
value="30"
min="1"
max="50"
step="1"
/>
<label for="prediction-setting-max-depth">Max Depth</label>
</div>
<div class="form-control mb-3">
<label class="label" for="prediction-setting-max-depth">
<span class="label-text">Max Depth</span>
</label>
<input
id="prediction-setting-max-depth"
type="number"
class="form-control"
class="input input-bordered w-full"
name="prediction-setting-max-depth"
value="5"
min="1"
max="8"
step="1"
/>
</div>
<label for="tp-generation-method">TP Generation Method</label>
<div class="form-control mb-3">
<label class="label" for="tp-generation-method">
<span class="label-text">TP Generation Method</span>
</label>
<select
id="tp-generation-method"
name="tp-generation-method"
class="form-control"
data-width="100%"
class="select select-bordered w-full"
x-model="tpMethod"
required
>
<option disabled selected>Select how TPs are generated</option>
<option value="" disabled selected>
Select how TPs are generated
</option>
<option value="rule-based-prediction-setting">Rule Based</option>
<option value="model-based-prediction-setting">Model Based</option>
</select>
<div id="rule-based-prediction-setting-specific-form">
<!-- Rule Packages -->
<label>Rule Packages</label><br />
</div>
<!-- Rule Based Settings -->
<div x-show="tpMethod === 'rule-based-prediction-setting'" x-cloak>
<div class="form-control mb-3">
<label class="label">
<span class="label-text">Rule Packages</span>
</label>
<select
id="rule-based-prediction-setting-packages"
name="rule-based-prediction-setting-packages"
data-actions-box="true"
class="form-control"
class="select select-bordered w-full h-32"
multiple
data-width="100%"
>
<option disabled>Reviewed Packages</option>
<optgroup label="Reviewed Packages">
{% for obj in meta.readable_packages %}
{% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
<option disabled>Unreviewed Packages</option>
</optgroup>
<optgroup label="Unreviewed Packages">
{% for obj in meta.readable_packages %}
{% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
</optgroup>
</select>
<label class="label">
<span class="label-text-alt"
>Hold Ctrl/Cmd to select multiple</span
>
</label>
</div>
<div id="model-based-prediction-setting-specific-form">
<label>Select Model</label><br />
</div>
<!-- Model Based Settings -->
<div x-show="tpMethod === 'model-based-prediction-setting'" x-cloak>
<div class="form-control mb-3">
<label class="label" for="model-based-prediction-setting-model">
<span class="label-text">Select Model</span>
</label>
<select
id="model-based-prediction-setting-model"
name="model-based-prediction-setting-model"
class="form-control"
data-width="100%"
class="select select-bordered w-full"
>
<option disabled selected>Select the model</option>
<option value="" disabled selected>Select the model</option>
{% for m in models %}
<option value="{{ m.url }}">{{ m.name|safe }}</option>
{% endfor %}
</select>
<label for="model-based-prediction-setting-threshold"
>Threshold</label
>
</div>
<div class="form-control mb-3">
<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="form-control"
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
class="form-check-input"
type="checkbox"
class="checkbox"
value="on"
id="prediction-setting-new-default"
name="prediction-setting-new-default"
/>
<label class="form-check-label" for="prediction-setting-new-default"
>Set this setting as new default</label
>
<span class="label-text">Set this setting as new default</span>
</label>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<!-- 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"
id="new-prediction-setting-modal-submit"
@click="submit()"
:disabled="isSubmitting"
>
Create
<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>
<script>
$(function () {
// Initially hide all "specific" forms
$("div[id$='-specific-form']").each(function () {
$(this).hide();
});
$("#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>
<!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

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

View File

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

View File

@ -1,30 +1,45 @@
<div
class="modal fade"
tabindex="-1"
{% load static %}
<dialog
id="new_scenario_modal"
role="dialog"
aria-labelledby="new_scenario_modal"
aria-hidden="true"
class="modal"
x-data="{
...modalForm(),
scenarioType: 'empty',
validateYear(el) {
if (el.value && el.value.length < 4) {
el.value = new Date().getFullYear();
}
}
}"
@close="reset()"
>
<div class="modal-dialog modal-lg">
<div class="modal-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>
<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>
<h4 class="modal-title">New Scenario</h4>
</div>
<div class="modal-body">
</form>
<!-- Body -->
<div class="py-4">
<form
id="new_scenario_form"
id="new-scenario-modal-form"
accept-charset="UTF-8"
action="{{ meta.current_package.url }}/scenario"
data-remote="true"
method="post"
>
{% csrf_token %}
<div class="jumbotron">
<div class="alert alert-info mb-4">
<span>
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 date of a study. You can leave all
@ -32,76 +47,95 @@
<a
target="_blank"
href="https://wiki.envipath.org/index.php/scenario"
role="button"
class="link"
>wiki &gt;&gt;</a
>
</span>
</div>
<label for="scenario-name">Name</label>
<div class="form-control mb-3">
<label class="label" for="scenario-name">
<span class="label-text">Name</span>
</label>
<input
id="scenario-name"
name="scenario-name"
class="form-control"
class="input input-bordered w-full"
placeholder="Name"
required
/>
<label for="scenario-description">Description</label>
</div>
<div class="form-control mb-3">
<label class="label" for="scenario-description">
<span class="label-text">Description</span>
</label>
<input
id="scenario-description"
name="scenario-description"
class="form-control"
class="input input-bordered w-full"
placeholder="Description"
/>
<label id="dateField" for="dateYear">Date</label>
<table>
<tr>
<th>
</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="form-control"
class="input input-bordered w-24"
placeholder="YYYY"
max="{% now "Y" %}"
max="{% now 'Y' %}"
@blur="validateYear($el)"
/>
</th>
<th>
<input
type="number"
id="dateMonth"
name="scenario-date-month"
min="1"
max="12"
class="form-control"
class="input input-bordered w-20"
placeholder="MM"
/>
</th>
<th>
<input
type="number"
id="dateDay"
name="scenario-date-day"
min="1"
max="31"
class="form-control"
class="input input-bordered w-20"
placeholder="DD"
/>
</th>
</tr>
</table>
<label for="scenario-type">Scenario Type</label>
</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="form-control"
data-width="100%"
class="select select-bordered w-full"
x-model="scenarioType"
>
<option value="empty" selected>Empty Scenario</option>
{% for k, v in scenario_types.items %}
<option value="{{ v.name }}">{{ k }}</option>
{% endfor %}
</select>
</div>
{% 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 %}
{{ widget|safe }}
{% endfor %}
@ -109,45 +143,35 @@
{% endfor %}
</form>
</div>
<div class="modal-footer">
<a id="new_scenario_modal_form_submit" class="btn btn-primary" href="#"
>Submit</a
<!-- Footer -->
<div class="modal-action">
<button
type="button"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
<button type="button" class="btn btn-default" data-dismiss="modal">
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>
<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 = new Date().getFullYear();
}
});
});
</script>
<!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,53 +1,69 @@
{% load static %}
<!-- Download Pathway -->
<div id="download_pathway_csv_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Download Pathway as CSV</h3>
<dialog
id="download_pathway_csv_modal"
class="modal"
x-data="modalForm()"
@close="reset()"
>
<div class="modal-box">
<!-- Header -->
<h3 class="font-bold text-lg">Download Pathway as CSV</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
:disabled="isSubmitting"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
</form>
<!-- Body -->
<div class="py-4">
<p>
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 }}"
data-remote="true"
method="GET"
>
<input type="hidden" name="download" value="true" />
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<!-- 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"
id="download-pathway-csv-modal-submit"
@click="submit('download-pathway-csv-modal-form'); $el.closest('dialog').close();"
:disabled="isSubmitting"
>
Download
<span x-show="!isSubmitting">Download</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$("#download-pathway-csv-modal-submit").click(function (e) {
e.preventDefault();
$("#download-pathway-csv-modal-form").submit();
$("#download_pathway_csv_modal").modal("hide");
});
});
</script>
<!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

@ -1,43 +1,57 @@
{% load static %}
<!-- Download Pathway -->
<div id="download_pathway_image_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Download Pathway as Image</h3>
<dialog
id="download_pathway_image_modal"
class="modal"
x-data="modalForm()"
@close="reset()"
>
<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="close"
data-dismiss="modal"
aria-label="Close"
class="btn"
onclick="this.closest('dialog').close()"
:disabled="isSubmitting"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
By clicking on Download the Pathway will be saved as SVG.
</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"
@click="isSubmitting = true; downloadSVG(document.getElementById('pwsvg'), '{{ pathway.name.split|join:'_' }}.svg'); $el.closest('dialog').close();"
:disabled="isSubmitting"
>
Download
<span x-show="!isSubmitting">Download</span>
<span
x-show="isSubmitting"
class="loading loading-spinner loading-sm"
></span>
</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$("#download-pathway-image-modal-submit").click(function (e) {
e.preventDefault();
downloadSVG($("#pwsvg")[0], '{{ pathway.name.split|join:"_" }}.svg');
$("#download_pathway_image_modal").modal("hide");
});
});
</script>
<!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,36 +1,43 @@
{% load static %}
<!-- Edit User -->
<div id="edit_user_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Update User Defaults</h5>
<dialog
id="edit_user_modal"
class="modal"
x-data="modalForm()"
@close="reset()"
>
<div class="modal-box">
<!-- Header -->
<h3 class="font-bold text-lg">Update User Defaults</h3>
<!-- Close button (X) -->
<form method="dialog">
<button
type="button"
class="close"
data-dismiss="modal"
aria-label="Close"
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
:disabled="isSubmitting"
>
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p>Edit User Defaults.</p>
</form>
<!-- Body -->
<div class="py-4">
<form
id="edit-user-modal-form"
accept-charset="UTF-8"
action=""
data-remote="true"
method="post"
>
{% csrf_token %}
<p>
<label for="default-package">Default Package</label>
<div class="form-control mb-3">
<label class="label" for="default-package">
<span class="label-text">Default Package</span>
</label>
<select
id="default-package"
name="default-package"
class="form-control"
data-width="100%"
class="select select-bordered w-full"
>
<option disabled>Select a Package</option>
{% for p in meta.writeable_packages %}
@ -42,14 +49,16 @@
</option>
{% endfor %}
</select>
</p>
<p>
<label for="default-group">Default Group</label>
</div>
<div class="form-control mb-3">
<label class="label" for="default-group">
<span class="label-text">Default Group</span>
</label>
<select
id="default-group"
name="default-group"
class="form-control"
data-width="100%"
class="select select-bordered w-full"
>
<option disabled>Select a Group</option>
{% for g in meta.available_groups %}
@ -61,16 +70,16 @@
</option>
{% endfor %}
</select>
</p>
<p>
<label for="default-prediction-setting"
>Default Prediction Setting</label
>
</div>
<div class="form-control mb-3">
<label class="label" for="default-prediction-setting">
<span class="label-text">Default Prediction Setting</span>
</label>
<select
id="default-prediction-setting"
name="default-prediction-setting"
class="form-control"
data-width="100%"
class="select select-bordered w-full"
>
<option disabled>Select a Setting</option>
{% for s in meta.available_settings %}
@ -82,29 +91,38 @@
</option>
{% endfor %}
</select>
</p>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">
<!-- 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"
id="edit-user-modal-submit"
@click="submit('edit-user-modal-form')"
:disabled="isSubmitting"
>
Update
<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>
<script>
$(function () {
$("#edit-user-modal-submit").click(function (e) {
e.preventDefault();
$("#edit-user-modal-form").submit();
});
});
</script>
<!-- Backdrop -->
<form method="dialog" class="modal-backdrop">
<button :disabled="isSubmitting">close</button>
</form>
</dialog>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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