forked from enviPath/enviPy
Compare commits
11 Commits
c6ff97694d
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
| f7c45b8015 | |||
| 68aea97013 | |||
| 3cc7fa9e8b | |||
| 21f3390a43 | |||
| 8cdf91c8fb | |||
| bafbf11322 | |||
| f1a9456d1d | |||
| e0764126e3 | |||
| ef0c45b203 | |||
| b737fc93eb | |||
| d4295c9349 |
@ -92,10 +92,19 @@ if os.environ.get("REGISTRATION_MANDATORY", False) == "True":
|
|||||||
|
|
||||||
ROOT_URLCONF = "envipath.urls"
|
ROOT_URLCONF = "envipath.urls"
|
||||||
|
|
||||||
|
TEMPLATE_DIRS = [
|
||||||
|
os.path.join(BASE_DIR, "templates"),
|
||||||
|
]
|
||||||
|
|
||||||
|
# If we have a non-public tenant, we might need to overwrite some templates
|
||||||
|
# search TENANT folder first...
|
||||||
|
if TENANT != "public":
|
||||||
|
TEMPLATE_DIRS.insert(0, os.path.join(BASE_DIR, TENANT, "templates"))
|
||||||
|
|
||||||
TEMPLATES = [
|
TEMPLATES = [
|
||||||
{
|
{
|
||||||
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
"BACKEND": "django.template.backends.django.DjangoTemplates",
|
||||||
"DIRS": (os.path.join(BASE_DIR, "templates"),),
|
"DIRS": TEMPLATE_DIRS,
|
||||||
"APP_DIRS": True,
|
"APP_DIRS": True,
|
||||||
"OPTIONS": {
|
"OPTIONS": {
|
||||||
"context_processors": [
|
"context_processors": [
|
||||||
@ -399,3 +408,9 @@ if MS_ENTRA_ENABLED:
|
|||||||
|
|
||||||
# Site ID 10 -> beta.envipath.org
|
# Site ID 10 -> beta.envipath.org
|
||||||
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")
|
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")
|
||||||
|
|
||||||
|
# CAP
|
||||||
|
CAP_ENABLED = os.environ.get("CAP_ENABLED", "False") == "True"
|
||||||
|
CAP_API_BASE = os.environ.get("CAP_API_BASE", None)
|
||||||
|
CAP_SITE_KEY = os.environ.get("CAP_SITE_KEY", None)
|
||||||
|
CAP_SECRET_KEY = os.environ.get("CAP_SECRET_KEY", None)
|
||||||
|
|||||||
@ -60,7 +60,7 @@ class ScenarioCreationAPITests(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 404)
|
self.assertEqual(response.status_code, 404)
|
||||||
self.assertIn("Package not found", response.json()["detail"])
|
self.assertIn(f"Package with UUID {fake_uuid} not found", response.json()["detail"])
|
||||||
|
|
||||||
def test_create_scenario_insufficient_permissions(self):
|
def test_create_scenario_insufficient_permissions(self):
|
||||||
"""Test that unauthorized access returns 403."""
|
"""Test that unauthorized access returns 403."""
|
||||||
|
|||||||
@ -41,6 +41,24 @@ def get_package_for_read(user, package_uuid: UUID):
|
|||||||
return package
|
return package
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_for_write(user, package_uuid: UUID):
|
||||||
|
"""
|
||||||
|
Get package by UUID with permission check.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# FIXME: update package manager with custom exceptions to avoid manual checks here
|
||||||
|
try:
|
||||||
|
package = Package.objects.get(uuid=package_uuid)
|
||||||
|
except Package.DoesNotExist:
|
||||||
|
raise EPAPINotFoundError(f"Package with UUID {package_uuid} not found")
|
||||||
|
|
||||||
|
# FIXME: optimize package manager to exclusively work with UUIDs
|
||||||
|
if not user or user.is_anonymous or not PackageManager.writable(user, package):
|
||||||
|
raise EPAPIPermissionDeniedError("Insufficient permissions to access this package.")
|
||||||
|
|
||||||
|
return package
|
||||||
|
|
||||||
|
|
||||||
def get_scenario_for_read(user, scenario_uuid: UUID):
|
def get_scenario_for_read(user, scenario_uuid: UUID):
|
||||||
"""Get scenario by UUID with read permission check."""
|
"""Get scenario by UUID with read permission check."""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@ -9,7 +9,6 @@ import logging
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from epdb.models import Scenario
|
from epdb.models import Scenario
|
||||||
from epdb.logic import PackageManager
|
|
||||||
from epdb.views import _anonymous_or_real
|
from epdb.views import _anonymous_or_real
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
from ..pagination import EnhancedPageNumberPagination
|
||||||
from ..schemas import (
|
from ..schemas import (
|
||||||
@ -17,7 +16,7 @@ from ..schemas import (
|
|||||||
ScenarioOutSchema,
|
ScenarioOutSchema,
|
||||||
ScenarioCreateSchema,
|
ScenarioCreateSchema,
|
||||||
)
|
)
|
||||||
from ..dal import get_user_entities_for_read, get_package_entities_for_read
|
from ..dal import get_user_entities_for_read, get_package_entities_for_read, get_package_for_write
|
||||||
from envipy_additional_information import registry
|
from envipy_additional_information import registry
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -58,7 +57,7 @@ def create_scenario(request, package_uuid: UUID, payload: ScenarioCreateSchema =
|
|||||||
user = _anonymous_or_real(request)
|
user = _anonymous_or_real(request)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_package = PackageManager.get_package_by_id(user, package_uuid)
|
current_package = get_package_for_write(user, package_uuid)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
error_msg = str(e)
|
error_msg = str(e)
|
||||||
if "does not exist" in error_msg:
|
if "does not exist" in error_msg:
|
||||||
|
|||||||
@ -16,6 +16,10 @@ class EPDBConfig(AppConfig):
|
|||||||
model_name = getattr(settings, "EPDB_PACKAGE_MODEL", "epdb.Package")
|
model_name = getattr(settings, "EPDB_PACKAGE_MODEL", "epdb.Package")
|
||||||
logger.info(f"Using Package model: {model_name}")
|
logger.info(f"Using Package model: {model_name}")
|
||||||
|
|
||||||
|
from .autodiscovery import autodiscover
|
||||||
|
|
||||||
|
autodiscover()
|
||||||
|
|
||||||
if settings.PLUGINS_ENABLED:
|
if settings.PLUGINS_ENABLED:
|
||||||
from bridge.contracts import Property
|
from bridge.contracts import Property
|
||||||
from utilities.plugin import discover_plugins
|
from utilities.plugin import discover_plugins
|
||||||
|
|||||||
5
epdb/autodiscovery.py
Normal file
5
epdb/autodiscovery.py
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
from django.utils.module_loading import autodiscover_modules
|
||||||
|
|
||||||
|
|
||||||
|
def autodiscover():
|
||||||
|
autodiscover_modules("epdb_hooks")
|
||||||
@ -1,3 +1,4 @@
|
|||||||
|
from collections import defaultdict
|
||||||
from typing import Any, Dict, List, Optional
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import nh3
|
import nh3
|
||||||
@ -11,8 +12,16 @@ from ninja.security import SessionAuth
|
|||||||
from utilities.chem import FormatConverter
|
from utilities.chem import FormatConverter
|
||||||
from utilities.misc import PackageExporter
|
from utilities.misc import PackageExporter
|
||||||
|
|
||||||
from .logic import GroupManager, PackageManager, SearchManager, SettingManager, UserManager
|
from .logic import (
|
||||||
|
EPDBURLParser,
|
||||||
|
GroupManager,
|
||||||
|
PackageManager,
|
||||||
|
SearchManager,
|
||||||
|
SettingManager,
|
||||||
|
UserManager,
|
||||||
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
|
AdditionalInformation,
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
Edge,
|
||||||
@ -94,6 +103,8 @@ class SimpleObject(Schema):
|
|||||||
return "reviewed" if obj.compound.package.reviewed else "unreviewed"
|
return "reviewed" if obj.compound.package.reviewed else "unreviewed"
|
||||||
elif isinstance(obj, Node) or isinstance(obj, Edge):
|
elif isinstance(obj, Node) or isinstance(obj, Edge):
|
||||||
return "reviewed" if obj.pathway.package.reviewed else "unreviewed"
|
return "reviewed" if obj.pathway.package.reviewed else "unreviewed"
|
||||||
|
elif isinstance(obj, dict) and "review_status" in obj:
|
||||||
|
return "reviewed" if obj.get("review_status") else "unreviewed"
|
||||||
else:
|
else:
|
||||||
raise ValueError("Object has no package")
|
raise ValueError("Object has no package")
|
||||||
|
|
||||||
@ -1327,7 +1338,14 @@ class ScenarioSchema(Schema):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_collection(obj: Scenario):
|
def resolve_collection(obj: Scenario):
|
||||||
return obj.additional_information
|
res = defaultdict(list)
|
||||||
|
|
||||||
|
for ai in obj.get_additional_information(direct_only=False):
|
||||||
|
data = ai.data
|
||||||
|
data["related"] = ai.content_object.simple_json() if ai.content_object else None
|
||||||
|
res[ai.type].append(data)
|
||||||
|
|
||||||
|
return res
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_status(obj: Rule):
|
def resolve_review_status(obj: Rule):
|
||||||
@ -1392,7 +1410,11 @@ def create_package_scenario(request, package_uuid):
|
|||||||
study_type = request.POST.get("type")
|
study_type = request.POST.get("type")
|
||||||
|
|
||||||
ais = []
|
ais = []
|
||||||
types = request.POST.getlist("adInfoTypes[]")
|
types = request.POST.get("adInfoTypes[]", [])
|
||||||
|
|
||||||
|
if types:
|
||||||
|
types = types.split(",")
|
||||||
|
|
||||||
for t in types:
|
for t in types:
|
||||||
ais.append(build_additional_information_from_request(request, t))
|
ais.append(build_additional_information_from_request(request, t))
|
||||||
|
|
||||||
@ -1434,6 +1456,49 @@ def delete_scenario(request, package_uuid, scenario_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post(
|
||||||
|
"/package/{uuid:package_uuid}/additional-information", response={200: str | Any, 403: Error}
|
||||||
|
)
|
||||||
|
def create_package_additional_information(request, package_uuid):
|
||||||
|
from utilities.legacy import build_additional_information_from_request
|
||||||
|
|
||||||
|
try:
|
||||||
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
|
|
||||||
|
scen = request.POST.get("scenario")
|
||||||
|
scenario = Scenario.objects.get(package=p, url=scen)
|
||||||
|
|
||||||
|
url_parser = EPDBURLParser(request.POST.get("attach_obj"))
|
||||||
|
attach_obj = url_parser.get_object()
|
||||||
|
|
||||||
|
if not hasattr(attach_obj, "additional_information"):
|
||||||
|
raise ValueError("Can't attach additional information to this object!")
|
||||||
|
|
||||||
|
if not attach_obj.url.startswith(p.url):
|
||||||
|
raise ValueError(
|
||||||
|
"Additional Information can only be set to objects stored in the same package!"
|
||||||
|
)
|
||||||
|
|
||||||
|
types = request.POST.get("adInfoTypes[]", "").split(",")
|
||||||
|
|
||||||
|
for t in types:
|
||||||
|
ai = build_additional_information_from_request(request, t)
|
||||||
|
|
||||||
|
AdditionalInformation.create(
|
||||||
|
p,
|
||||||
|
ai,
|
||||||
|
scenario=scenario,
|
||||||
|
content_object=attach_obj,
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO implement additional information endpoint ?
|
||||||
|
return redirect(f"{scenario.url}")
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Getting Package with id {package_uuid} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
###########
|
###########
|
||||||
# Pathway #
|
# Pathway #
|
||||||
###########
|
###########
|
||||||
@ -1464,7 +1529,7 @@ class PathwayEdge(Schema):
|
|||||||
|
|
||||||
class PathwayNode(Schema):
|
class PathwayNode(Schema):
|
||||||
atomCount: int = Field(None, alias="atom_count")
|
atomCount: int = Field(None, alias="atom_count")
|
||||||
depth: int = Field(None, alias="depth")
|
depth: float = Field(None, alias="depth")
|
||||||
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
||||||
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
@ -1805,7 +1870,7 @@ class EdgeSchema(Schema):
|
|||||||
startNodes: List["EdgeNode"] = Field([], alias="start_nodes")
|
startNodes: List["EdgeNode"] = Field([], alias="start_nodes")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_status(obj: Node):
|
def resolve_review_status(obj: Edge):
|
||||||
return "reviewed" if obj.pathway.package.reviewed else "unreviewed"
|
return "reviewed" if obj.pathway.package.reviewed else "unreviewed"
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,3 @@
|
|||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, List, Optional, Set, Union, Tuple
|
from typing import Any, Dict, List, Optional, Set, Union, Tuple
|
||||||
@ -11,6 +10,7 @@ from django.db import transaction
|
|||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from epdb.models import (
|
from epdb.models import (
|
||||||
|
AdditionalInformation,
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
Edge,
|
||||||
@ -634,15 +634,30 @@ class PackageManager(object):
|
|||||||
|
|
||||||
# Stores old_id to new_id
|
# Stores old_id to new_id
|
||||||
mapping = {}
|
mapping = {}
|
||||||
# Stores new_scen_id to old_parent_scen_id
|
|
||||||
parent_mapping = {}
|
|
||||||
# Mapping old scen_id to old_obj_id
|
# Mapping old scen_id to old_obj_id
|
||||||
scen_mapping = defaultdict(list)
|
scen_mapping = defaultdict(list)
|
||||||
# Enzymelink Mapping rule_id to enzymelink objects
|
# Enzymelink Mapping rule_id to enzymelink objects
|
||||||
enzyme_mapping = defaultdict(list)
|
enzyme_mapping = defaultdict(list)
|
||||||
|
|
||||||
|
# old_parent_id to child
|
||||||
|
postponed_scens = defaultdict(list)
|
||||||
|
|
||||||
# Store Scenarios
|
# Store Scenarios
|
||||||
for scenario in data["scenarios"]:
|
for scenario in data["scenarios"]:
|
||||||
|
skip_scen = False
|
||||||
|
# Check if parent exists and park this Scenario to convert it later into an
|
||||||
|
# AdditionalInformation object
|
||||||
|
for ex in scenario.get("additionalInformationCollection", {}).get(
|
||||||
|
"additionalInformation", []
|
||||||
|
):
|
||||||
|
if ex["name"] == "referringscenario":
|
||||||
|
postponed_scens[ex["data"]].append(scenario)
|
||||||
|
skip_scen = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if skip_scen:
|
||||||
|
continue
|
||||||
|
|
||||||
scen = Scenario()
|
scen = Scenario()
|
||||||
scen.package = pack
|
scen.package = pack
|
||||||
scen.uuid = UUID(scenario["id"].split("/")[-1]) if keep_ids else uuid4()
|
scen.uuid = UUID(scenario["id"].split("/")[-1]) if keep_ids else uuid4()
|
||||||
@ -655,19 +670,12 @@ class PackageManager(object):
|
|||||||
|
|
||||||
mapping[scenario["id"]] = scen.uuid
|
mapping[scenario["id"]] = scen.uuid
|
||||||
|
|
||||||
new_add_inf = defaultdict(list)
|
|
||||||
# TODO Store AI...
|
|
||||||
for ex in scenario.get("additionalInformationCollection", {}).get(
|
for ex in scenario.get("additionalInformationCollection", {}).get(
|
||||||
"additionalInformation", []
|
"additionalInformation", []
|
||||||
):
|
):
|
||||||
name = ex["name"]
|
name = ex["name"]
|
||||||
addinf_data = ex["data"]
|
addinf_data = ex["data"]
|
||||||
|
|
||||||
# park the parent scen id for now and link it later
|
|
||||||
if name == "referringscenario":
|
|
||||||
parent_mapping[scen.uuid] = addinf_data
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Broken eP Data
|
# Broken eP Data
|
||||||
if name == "initialmasssediment" and addinf_data == "missing data":
|
if name == "initialmasssediment" and addinf_data == "missing data":
|
||||||
continue
|
continue
|
||||||
@ -675,17 +683,11 @@ class PackageManager(object):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = AdditionalInformationConverter.convert(name, addinf_data)
|
ai = AdditionalInformationConverter.convert(name, addinf_data)
|
||||||
res_cls_name = res.__class__.__name__
|
AdditionalInformation.create(pack, ai, scenario=scen)
|
||||||
ai_data = json.loads(res.model_dump_json())
|
|
||||||
ai_data["uuid"] = f"{uuid4()}"
|
|
||||||
new_add_inf[res_cls_name].append(ai_data)
|
|
||||||
except (ValidationError, ValueError):
|
except (ValidationError, ValueError):
|
||||||
logger.error(f"Failed to convert {name} with {addinf_data}")
|
logger.error(f"Failed to convert {name} with {addinf_data}")
|
||||||
|
|
||||||
scen.additional_information = new_add_inf
|
|
||||||
scen.save()
|
|
||||||
|
|
||||||
print("Scenarios imported...")
|
print("Scenarios imported...")
|
||||||
|
|
||||||
# Store compounds and its structures
|
# Store compounds and its structures
|
||||||
@ -925,14 +927,46 @@ class PackageManager(object):
|
|||||||
|
|
||||||
print("Pathways imported...")
|
print("Pathways imported...")
|
||||||
|
|
||||||
# Linking Phase
|
for parent, children in postponed_scens.items():
|
||||||
for child, parent in parent_mapping.items():
|
for child in children:
|
||||||
child_obj = Scenario.objects.get(uuid=child)
|
for ex in child.get("additionalInformationCollection", {}).get(
|
||||||
parent_obj = Scenario.objects.get(uuid=mapping[parent])
|
"additionalInformation", []
|
||||||
child_obj.parent = parent_obj
|
):
|
||||||
child_obj.save()
|
child_id = child["id"]
|
||||||
|
name = ex["name"]
|
||||||
|
addinf_data = ex["data"]
|
||||||
|
|
||||||
|
if name == "referringscenario":
|
||||||
|
continue
|
||||||
|
# Broken eP Data
|
||||||
|
if name == "initialmasssediment" and addinf_data == "missing data":
|
||||||
|
continue
|
||||||
|
if name == "columnheight" and addinf_data == "(2)-(2.5);(6)-(8)":
|
||||||
|
continue
|
||||||
|
|
||||||
|
ai = AdditionalInformationConverter.convert(name, addinf_data)
|
||||||
|
|
||||||
|
if child_id not in scen_mapping:
|
||||||
|
logger.info(
|
||||||
|
f"{child_id} not found in scen_mapping. Seems like its not attached to any object"
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
f"{child_id} not found in scen_mapping. Seems like its not attached to any object"
|
||||||
|
)
|
||||||
|
|
||||||
|
scen = Scenario.objects.get(uuid=mapping[parent])
|
||||||
|
mapping[child_id] = scen.uuid
|
||||||
|
for obj in scen_mapping[child_id]:
|
||||||
|
_ = AdditionalInformation.create(pack, ai, scen, content_object=obj)
|
||||||
|
|
||||||
for scen_id, objects in scen_mapping.items():
|
for scen_id, objects in scen_mapping.items():
|
||||||
|
new_id = mapping.get(scen_id)
|
||||||
|
|
||||||
|
if new_id is None:
|
||||||
|
logger.warning(f"Could not find mapping for {scen_id}")
|
||||||
|
print(f"Could not find mapping for {scen_id}")
|
||||||
|
continue
|
||||||
|
|
||||||
scen = Scenario.objects.get(uuid=mapping[scen_id])
|
scen = Scenario.objects.get(uuid=mapping[scen_id])
|
||||||
for o in objects:
|
for o in objects:
|
||||||
o.scenarios.add(scen)
|
o.scenarios.add(scen)
|
||||||
@ -965,6 +999,7 @@ class PackageManager(object):
|
|||||||
matches = re.findall(r">(R[0-9]+)<", evidence["evidence"])
|
matches = re.findall(r">(R[0-9]+)<", evidence["evidence"])
|
||||||
if not matches or len(matches) != 1:
|
if not matches or len(matches) != 1:
|
||||||
logger.warning(f"Could not find reaction id in {evidence['evidence']}")
|
logger.warning(f"Could not find reaction id in {evidence['evidence']}")
|
||||||
|
print(f"Could not find reaction id in {evidence['evidence']}")
|
||||||
continue
|
continue
|
||||||
|
|
||||||
e.add_kegg_reaction_id(matches[0])
|
e.add_kegg_reaction_id(matches[0])
|
||||||
@ -984,7 +1019,6 @@ class PackageManager(object):
|
|||||||
print("Fixing Node depths...")
|
print("Fixing Node depths...")
|
||||||
total_pws = Pathway.objects.filter(package=pack).count()
|
total_pws = Pathway.objects.filter(package=pack).count()
|
||||||
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
for p, pw in enumerate(Pathway.objects.filter(package=pack)):
|
||||||
print(pw.url)
|
|
||||||
in_count = defaultdict(lambda: 0)
|
in_count = defaultdict(lambda: 0)
|
||||||
out_count = defaultdict(lambda: 0)
|
out_count = defaultdict(lambda: 0)
|
||||||
|
|
||||||
@ -1020,7 +1054,6 @@ class PackageManager(object):
|
|||||||
if str(prod.uuid) not in seen:
|
if str(prod.uuid) not in seen:
|
||||||
old_depth = prod.depth
|
old_depth = prod.depth
|
||||||
if old_depth != i + 1:
|
if old_depth != i + 1:
|
||||||
print(f"updating depth from {old_depth} to {i + 1}")
|
|
||||||
prod.depth = i + 1
|
prod.depth = i + 1
|
||||||
prod.save()
|
prod.save()
|
||||||
|
|
||||||
@ -1031,7 +1064,7 @@ class PackageManager(object):
|
|||||||
if new_level:
|
if new_level:
|
||||||
levels.append(new_level)
|
levels.append(new_level)
|
||||||
|
|
||||||
print(f"{p + 1}/{total_pws} fixed.")
|
print(f"{p + 1}/{total_pws} fixed.", end="\r")
|
||||||
|
|
||||||
return pack
|
return pack
|
||||||
|
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.core.management import call_command
|
from django.core.management import call_command
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
|
|
||||||
@ -45,11 +46,13 @@ class Command(BaseCommand):
|
|||||||
if not os.path.exists(dump_file):
|
if not os.path.exists(dump_file):
|
||||||
raise ValueError(f"Dump file {dump_file} does not exist")
|
raise ValueError(f"Dump file {dump_file} does not exist")
|
||||||
|
|
||||||
print(f"Dropping database {options['name']} y/n: ", end="")
|
db_name = options["name"]
|
||||||
|
|
||||||
|
print(f"Dropping database {db_name} y/n: ", end="")
|
||||||
|
|
||||||
if input() in "yY":
|
if input() in "yY":
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["dropdb", "appdb"],
|
["dropdb", db_name],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
@ -57,20 +60,24 @@ class Command(BaseCommand):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("Aborted")
|
raise ValueError("Aborted")
|
||||||
|
|
||||||
print(f"Creating database {options['name']}")
|
print(f"Creating database {db_name}")
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["createdb", "appdb"],
|
["createdb", db_name],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
print(result.stdout)
|
print(result.stdout)
|
||||||
print(f"Restoring database {options['name']} from {dump_file}")
|
print(f"Restoring database {db_name} from {dump_file}")
|
||||||
|
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["pg_restore", "-d", "appdb", dump_file, "--no-owner"],
|
["pg_restore", "-d", db_name, dump_file, "--no-owner"],
|
||||||
capture_output=True,
|
capture_output=True,
|
||||||
text=True,
|
text=True,
|
||||||
)
|
)
|
||||||
print(result.stdout)
|
print(result.stdout)
|
||||||
call_command("localize_urls", "--old", options["oldurl"], "--new", options["newurl"])
|
|
||||||
|
if db_name == settings.DATABASES["default"]["NAME"]:
|
||||||
|
call_command("localize_urls", "--old", options["oldurl"], "--new", options["newurl"])
|
||||||
|
else:
|
||||||
|
print("Skipping localize_urls as database is not the default one.")
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
# Generated by Django 5.2.7 on 2026-03-09 10:41
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_polymorphic_ctype(apps, schema_editor):
|
||||||
|
ContentType = apps.get_model("contenttypes", "ContentType")
|
||||||
|
Compound = apps.get_model("epdb", "Compound")
|
||||||
|
CompoundStructure = apps.get_model("epdb", "CompoundStructure")
|
||||||
|
|
||||||
|
# Update Compound records
|
||||||
|
compound_ct = ContentType.objects.get_for_model(Compound)
|
||||||
|
Compound.objects.filter(polymorphic_ctype__isnull=True).update(polymorphic_ctype=compound_ct)
|
||||||
|
|
||||||
|
# Update CompoundStructure records
|
||||||
|
compound_structure_ct = ContentType.objects.get_for_model(CompoundStructure)
|
||||||
|
CompoundStructure.objects.filter(polymorphic_ctype__isnull=True).update(
|
||||||
|
polymorphic_ctype=compound_structure_ct
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_populate_polymorphic_ctype(apps, schema_editor):
|
||||||
|
Compound = apps.get_model("epdb", "Compound")
|
||||||
|
CompoundStructure = apps.get_model("epdb", "CompoundStructure")
|
||||||
|
|
||||||
|
Compound.objects.all().update(polymorphic_ctype=None)
|
||||||
|
CompoundStructure.objects.all().update(polymorphic_ctype=None)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("contenttypes", "0002_remove_content_type_name"),
|
||||||
|
("epdb", "0019_remove_scenario_additional_information_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterModelOptions(
|
||||||
|
name="compoundstructure",
|
||||||
|
options={"base_manager_name": "objects"},
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="compound",
|
||||||
|
name="polymorphic_ctype",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="polymorphic_%(app_label)s.%(class)s_set+",
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="compoundstructure",
|
||||||
|
name="polymorphic_ctype",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
editable=False,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
related_name="polymorphic_%(app_label)s.%(class)s_set+",
|
||||||
|
to="contenttypes.contenttype",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.RunPython(populate_polymorphic_ctype, reverse_populate_polymorphic_ctype),
|
||||||
|
]
|
||||||
@ -765,7 +765,12 @@ class Package(EnviPathModel):
|
|||||||
|
|
||||||
|
|
||||||
class Compound(
|
class Compound(
|
||||||
EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin
|
PolymorphicModel,
|
||||||
|
EnviPathModel,
|
||||||
|
AliasMixin,
|
||||||
|
ScenarioMixin,
|
||||||
|
ChemicalIdentifierMixin,
|
||||||
|
AdditionalInformationMixin,
|
||||||
):
|
):
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||||
@ -1095,7 +1100,12 @@ class Compound(
|
|||||||
|
|
||||||
|
|
||||||
class CompoundStructure(
|
class CompoundStructure(
|
||||||
EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin, AdditionalInformationMixin
|
PolymorphicModel,
|
||||||
|
EnviPathModel,
|
||||||
|
AliasMixin,
|
||||||
|
ScenarioMixin,
|
||||||
|
ChemicalIdentifierMixin,
|
||||||
|
AdditionalInformationMixin,
|
||||||
):
|
):
|
||||||
compound = models.ForeignKey("epdb.Compound", on_delete=models.CASCADE, db_index=True)
|
compound = models.ForeignKey("epdb.Compound", on_delete=models.CASCADE, db_index=True)
|
||||||
smiles = models.TextField(blank=False, null=False, verbose_name="SMILES")
|
smiles = models.TextField(blank=False, null=False, verbose_name="SMILES")
|
||||||
@ -1775,9 +1785,9 @@ class Reaction(
|
|||||||
edges = Edge.objects.filter(edge_label=self)
|
edges = Edge.objects.filter(edge_label=self)
|
||||||
for e in edges:
|
for e in edges:
|
||||||
for scen in e.scenarios.all():
|
for scen in e.scenarios.all():
|
||||||
for ai in scen.additional_information.keys():
|
for ai in scen.get_additional_information():
|
||||||
if ai == "Enzyme":
|
if ai.type == "Enzyme":
|
||||||
res.extend(scen.additional_information[ai])
|
res.append(ai.get())
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
|
||||||
@ -2334,7 +2344,10 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
"reaction_probability": self.kv.get("probability"),
|
"reaction_probability": self.kv.get("probability"),
|
||||||
"start_node_urls": [x.url for x in self.start_nodes.all()],
|
"start_node_urls": [x.url for x in self.start_nodes.all()],
|
||||||
"end_node_urls": [x.url for x in self.end_nodes.all()],
|
"end_node_urls": [x.url for x in self.end_nodes.all()],
|
||||||
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
|
"scenarios": [
|
||||||
|
{"name": s.get_name(), "url": s.url, "review_status": s.package.reviewed}
|
||||||
|
for s in self.scenarios.all()
|
||||||
|
],
|
||||||
}
|
}
|
||||||
|
|
||||||
for n in self.start_nodes.all():
|
for n in self.start_nodes.all():
|
||||||
@ -3458,9 +3471,7 @@ class EnviFormer(PackageBasedModel):
|
|||||||
def predict_batch(self, smiles: List[str], *args, **kwargs):
|
def predict_batch(self, smiles: List[str], *args, **kwargs):
|
||||||
# Standardizer removes all but one compound from a raw SMILES string, so they need to be processed separately
|
# Standardizer removes all but one compound from a raw SMILES string, so they need to be processed separately
|
||||||
canon_smiles = [
|
canon_smiles = [
|
||||||
".".join(
|
".".join([FormatConverter.standardize(s, remove_stereo=True) for s in smi.split(".")])
|
||||||
[FormatConverter.standardize(s, remove_stereo=True) for s in smiles.split(".")]
|
|
||||||
)
|
|
||||||
for smi in smiles
|
for smi in smiles
|
||||||
]
|
]
|
||||||
logger.info(f"Submitting {canon_smiles} to {self.get_name()}")
|
logger.info(f"Submitting {canon_smiles} to {self.get_name()}")
|
||||||
@ -4138,7 +4149,7 @@ class Scenario(EnviPathModel):
|
|||||||
ais = AdditionalInformation.objects.filter(scenario=self)
|
ais = AdditionalInformation.objects.filter(scenario=self)
|
||||||
|
|
||||||
if direct_only:
|
if direct_only:
|
||||||
return ais.filter(content_object__isnull=True)
|
return ais.filter(object_id__isnull=True)
|
||||||
else:
|
else:
|
||||||
return ais
|
return ais
|
||||||
|
|
||||||
@ -4180,7 +4191,6 @@ class AdditionalInformation(models.Model):
|
|||||||
ai: "EnviPyModel",
|
ai: "EnviPyModel",
|
||||||
scenario=None,
|
scenario=None,
|
||||||
content_object=None,
|
content_object=None,
|
||||||
skip_cleaning=False,
|
|
||||||
):
|
):
|
||||||
add_inf = AdditionalInformation()
|
add_inf = AdditionalInformation()
|
||||||
add_inf.package = package
|
add_inf.package = package
|
||||||
|
|||||||
17
epdb/template_registry.py
Normal file
17
epdb/template_registry.py
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
from collections import defaultdict
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
|
_registry = defaultdict(list)
|
||||||
|
_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
|
def register_template(slot: str, template_name: str, *, order: int = 100):
|
||||||
|
item = (order, template_name)
|
||||||
|
with _lock:
|
||||||
|
if item not in _registry[slot]:
|
||||||
|
_registry[slot].append(item)
|
||||||
|
_registry[slot].sort(key=lambda x: x[0])
|
||||||
|
|
||||||
|
|
||||||
|
def get_templates(slot: str):
|
||||||
|
return [template_name for _, template_name in _registry.get(slot, [])]
|
||||||
@ -2,6 +2,8 @@ from django import template
|
|||||||
from pydantic import AnyHttpUrl, ValidationError
|
from pydantic import AnyHttpUrl, ValidationError
|
||||||
from pydantic.type_adapter import TypeAdapter
|
from pydantic.type_adapter import TypeAdapter
|
||||||
|
|
||||||
|
from epdb.template_registry import get_templates
|
||||||
|
|
||||||
register = template.Library()
|
register = template.Library()
|
||||||
|
|
||||||
url_adapter = TypeAdapter(AnyHttpUrl)
|
url_adapter = TypeAdapter(AnyHttpUrl)
|
||||||
@ -19,3 +21,8 @@ def is_url(value):
|
|||||||
return True
|
return True
|
||||||
except ValidationError:
|
except ValidationError:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
@register.simple_tag
|
||||||
|
def epdb_slot_templates(slot):
|
||||||
|
return get_templates(slot)
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import logging
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Iterable
|
from typing import Any, Dict, List, Iterable
|
||||||
|
|
||||||
|
import requests
|
||||||
import nh3
|
import nh3
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
@ -147,6 +148,11 @@ def handler500(request):
|
|||||||
def login(request):
|
def login(request):
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
|
|
||||||
|
if s.CAP_ENABLED:
|
||||||
|
context["CAP_ENABLED"] = s.CAP_ENABLED
|
||||||
|
context["CAP_API_BASE"] = s.CAP_API_BASE
|
||||||
|
context["CAP_SITE_KEY"] = s.CAP_SITE_KEY
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context["title"] = "enviPath"
|
context["title"] = "enviPath"
|
||||||
context["next"] = request.GET.get("next", "")
|
context["next"] = request.GET.get("next", "")
|
||||||
@ -224,6 +230,11 @@ def logout(request):
|
|||||||
def register(request):
|
def register(request):
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
|
|
||||||
|
if s.CAP_ENABLED:
|
||||||
|
context["CAP_ENABLED"] = s.CAP_ENABLED
|
||||||
|
context["CAP_API_BASE"] = s.CAP_API_BASE
|
||||||
|
context["CAP_SITE_KEY"] = s.CAP_SITE_KEY
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
# Redirect to unified login page with signup tab
|
# Redirect to unified login page with signup tab
|
||||||
next_url = request.GET.get("next", "")
|
next_url = request.GET.get("next", "")
|
||||||
@ -238,6 +249,33 @@ def register(request):
|
|||||||
if next := request.POST.get("next"):
|
if next := request.POST.get("next"):
|
||||||
context["next"] = next
|
context["next"] = next
|
||||||
|
|
||||||
|
# Catpcha
|
||||||
|
if s.CAP_ENABLED:
|
||||||
|
cap_token = request.POST.get("cap-token")
|
||||||
|
|
||||||
|
if not cap_token:
|
||||||
|
context["message"] = "Missing CAP Token."
|
||||||
|
return render(request, "static/login.html", context)
|
||||||
|
|
||||||
|
verify_url = f"{s.CAP_API_BASE}/{s.CAP_SITE_KEY}/siteverify"
|
||||||
|
payload = {
|
||||||
|
"secret": s.CAP_SECRET_KEY,
|
||||||
|
"response": cap_token,
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
resp = requests.post(verify_url, json=payload, timeout=10)
|
||||||
|
resp.raise_for_status()
|
||||||
|
verify_data = resp.json()
|
||||||
|
except requests.RequestException:
|
||||||
|
context["message"] = "Captcha verification failed."
|
||||||
|
return render(request, "static/login.html", context)
|
||||||
|
|
||||||
|
if not verify_data.get("success"):
|
||||||
|
context["message"] = "Captcha check failed. Please try again."
|
||||||
|
return render(request, "static/login.html", context)
|
||||||
|
# End Captcha
|
||||||
|
|
||||||
username = request.POST.get("username", "").strip()
|
username = request.POST.get("username", "").strip()
|
||||||
email = request.POST.get("email", "").strip()
|
email = request.POST.get("email", "").strip()
|
||||||
password = request.POST.get("password", "").strip()
|
password = request.POST.get("password", "").strip()
|
||||||
@ -917,7 +955,7 @@ def package_models(request, package_uuid):
|
|||||||
params["threshold"] = threshold
|
params["threshold"] = threshold
|
||||||
|
|
||||||
mod = EnviFormer.create(**params)
|
mod = EnviFormer.create(**params)
|
||||||
elif model_type == "mlrr":
|
elif model_type == "ml-relative-reasoning":
|
||||||
# ML Specific
|
# ML Specific
|
||||||
threshold = float(request.POST.get("model-threshold", 0.5))
|
threshold = float(request.POST.get("model-threshold", 0.5))
|
||||||
# TODO handle additional fingerprinter
|
# TODO handle additional fingerprinter
|
||||||
@ -941,7 +979,7 @@ def package_models(request, package_uuid):
|
|||||||
params["app_domain_local_compatibility_threshold"] = local_compatibility_threshold
|
params["app_domain_local_compatibility_threshold"] = local_compatibility_threshold
|
||||||
|
|
||||||
mod = MLRelativeReasoning.create(**params)
|
mod = MLRelativeReasoning.create(**params)
|
||||||
elif model_type == "rbrr":
|
elif model_type == "rule-based-relative-reasoning":
|
||||||
params["rule_packages"] = [
|
params["rule_packages"] = [
|
||||||
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
||||||
]
|
]
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -79,9 +79,9 @@ class PepperPrediction(PredictedProperty):
|
|||||||
dist = stats.lognorm(s=sigma_ln, scale=np.exp(mu_ln))
|
dist = stats.lognorm(s=sigma_ln, scale=np.exp(mu_ln))
|
||||||
|
|
||||||
# Exact probabilities
|
# Exact probabilities
|
||||||
p_green = dist.cdf(p) # P(X < a)
|
p_green = dist.cdf(p) # P(X < p) prob not persistent
|
||||||
p_yellow = dist.cdf(vp) - p_green # P(a <= X <= b)
|
p_yellow = 1.0 - dist.cdf(p) # P (X > p) prob persistent
|
||||||
p_red = 1.0 - dist.cdf(vp) # P(X > b)
|
p_red = 1.0 - dist.cdf(vp) # P(X > vp) prob very persistent
|
||||||
|
|
||||||
# Plotting range
|
# Plotting range
|
||||||
q_low, q_high = dist.ppf(quantiles)
|
q_low, q_high = dist.ppf(quantiles)
|
||||||
|
|||||||
@ -88,6 +88,7 @@ document.addEventListener("alpine:init", () => {
|
|||||||
options.debugErrors ??
|
options.debugErrors ??
|
||||||
(typeof window !== "undefined" &&
|
(typeof window !== "undefined" &&
|
||||||
window.location?.search?.includes("debugErrors=1")),
|
window.location?.search?.includes("debugErrors=1")),
|
||||||
|
attach_object: options.attach_object || null,
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
if (options.schemaUrl) {
|
if (options.schemaUrl) {
|
||||||
|
|||||||
@ -1,21 +1,34 @@
|
|||||||
{% extends "collections/paginated_base.html" %}
|
{% extends "collections/paginated_base.html" %}
|
||||||
|
{% load envipytags %}
|
||||||
|
|
||||||
{% block page_title %}Compounds{% endblock %}
|
{% block page_title %}Compounds{% endblock %}
|
||||||
|
|
||||||
{% block action_button %}
|
{% block action_button %}
|
||||||
{% if meta.can_edit %}
|
<div class="flex items-center gap-2">
|
||||||
<button
|
{% if meta.can_edit %}
|
||||||
type="button"
|
<button
|
||||||
class="btn btn-primary btn-sm"
|
type="button"
|
||||||
onclick="document.getElementById('new_compound_modal').showModal(); return false;"
|
class="btn btn-primary btn-sm"
|
||||||
>
|
onclick="document.getElementById('new_compound_modal').showModal(); return false;"
|
||||||
New Compound
|
>
|
||||||
</button>
|
New Compound
|
||||||
{% endif %}
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% epdb_slot_templates "epdb.actions.collections.compound" as action_button_templates %}
|
||||||
|
|
||||||
|
{% for tpl in action_button_templates %}
|
||||||
|
{% include tpl %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% endblock action_button %}
|
{% endblock action_button %}
|
||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
{% include "modals/collections/new_compound_modal.html" %}
|
{% include "modals/collections/new_compound_modal.html" %}
|
||||||
|
{% epdb_slot_templates "modals.collections.compound" as action_modals_templates %}
|
||||||
|
|
||||||
|
{% for tpl in action_modals_templates %}
|
||||||
|
{% include tpl %}
|
||||||
|
{% endfor %}
|
||||||
{% endblock action_modals %}
|
{% endblock action_modals %}
|
||||||
|
|
||||||
{% block description %}
|
{% block description %}
|
||||||
|
|||||||
@ -18,8 +18,25 @@
|
|||||||
<!-- Schema form -->
|
<!-- Schema form -->
|
||||||
<template x-if="schema && !loading">
|
<template x-if="schema && !loading">
|
||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
|
<template x-if="attach_object">
|
||||||
|
<div>
|
||||||
|
<h4>
|
||||||
|
<span
|
||||||
|
class="text-lg font-semibold"
|
||||||
|
x-text="schema['x-title'] + ' attached to'"
|
||||||
|
></span>
|
||||||
|
<a
|
||||||
|
class="text-lg font-semibold underline text-blue-600 hover:text-blue-800"
|
||||||
|
:href="attach_object.url"
|
||||||
|
x-text="attach_object.name"
|
||||||
|
target="_blank"
|
||||||
|
></a>
|
||||||
|
</h4>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
<!-- Title from schema -->
|
<!-- Title from schema -->
|
||||||
<template x-if="schema['x-title'] || schema.title">
|
<template x-if="(schema['x-title'] || schema.title) && !attach_object">
|
||||||
<h4
|
<h4
|
||||||
class="text-lg font-semibold"
|
class="text-lg font-semibold"
|
||||||
x-text="data.name || schema['x-title'] || schema.title"
|
x-text="data.name || schema['x-title'] || schema.title"
|
||||||
|
|||||||
@ -71,24 +71,129 @@
|
|||||||
<label class="label">
|
<label class="label">
|
||||||
<span class="label-text">User or Group</span>
|
<span class="label-text">User or Group</span>
|
||||||
</label>
|
</label>
|
||||||
<select
|
<div
|
||||||
id="select_grantee"
|
class="relative"
|
||||||
name="grantee"
|
x-data="{
|
||||||
class="select select-bordered w-full select-sm"
|
searchQuery: '',
|
||||||
required
|
selectedItem: null,
|
||||||
|
showResults: false,
|
||||||
|
filteredResults: [],
|
||||||
|
allItems: [
|
||||||
|
{% for u in users %}
|
||||||
|
{ type: 'user', name: '{{ u.username }}', url: '{{ u.url }}',
|
||||||
|
display: '{{ u.username }}' },
|
||||||
|
{% endfor %}
|
||||||
|
{% for g in groups %}
|
||||||
|
{ type: 'group', name: '{{ g.name|safe }}', url: '{{ g.url }}',
|
||||||
|
display: '{{ g.name|safe }}' },
|
||||||
|
{% endfor %}
|
||||||
|
],
|
||||||
|
init() {
|
||||||
|
this.filteredResults = this.allItems;
|
||||||
|
},
|
||||||
|
search() {
|
||||||
|
if (this.searchQuery.length === 0) {
|
||||||
|
this.filteredResults = this.allItems;
|
||||||
|
} else {
|
||||||
|
this.filteredResults = this.allItems.filter(item =>
|
||||||
|
item.name.toLowerCase().includes(this.searchQuery.toLowerCase())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
this.showResults = true;
|
||||||
|
},
|
||||||
|
selectItem(item) {
|
||||||
|
this.selectedItem = item;
|
||||||
|
this.searchQuery = item.display;
|
||||||
|
this.showResults = false;
|
||||||
|
},
|
||||||
|
clearSelection() {
|
||||||
|
this.selectedItem = null;
|
||||||
|
this.searchQuery = '';
|
||||||
|
this.showResults = false;
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
@click.away="showResults = false"
|
||||||
>
|
>
|
||||||
<optgroup label="Users">
|
<input
|
||||||
{% for u in users %}
|
type="text"
|
||||||
<option value="{{ u.url }}">{{ u.username }}</option>
|
x-model="searchQuery"
|
||||||
{% endfor %}
|
@input="search()"
|
||||||
</optgroup>
|
@focus="showResults = true; search()"
|
||||||
<optgroup label="Groups">
|
@keydown.escape="showResults = false"
|
||||||
{% for g in groups %}
|
@keydown.arrow-down.prevent="$refs.resultsList?.children[0]?.focus()"
|
||||||
<option value="{{ g.url }}">{{ g.name|safe }}</option>
|
class="input input-bordered w-full input-sm"
|
||||||
{% endfor %}
|
placeholder="Search users or groups..."
|
||||||
</optgroup>
|
autocomplete="off"
|
||||||
</select>
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Clear button -->
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
x-show="searchQuery.length > 0"
|
||||||
|
@click="clearSelection()"
|
||||||
|
class="absolute right-2 top-1/2 transform -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<!-- Hidden input for form submission -->
|
||||||
|
<input
|
||||||
|
type="hidden"
|
||||||
|
name="grantee"
|
||||||
|
x-bind:value="selectedItem?.url || ''"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Search results dropdown -->
|
||||||
|
<div
|
||||||
|
x-show="showResults && filteredResults.length > 0"
|
||||||
|
x-transition
|
||||||
|
class="absolute z-50 w-full mt-1 bg-base-100 border border-base-300 rounded-lg shadow-lg max-h-60 overflow-y-auto"
|
||||||
|
>
|
||||||
|
<ul x-ref="resultsList" id="resultsList" class="py-1">
|
||||||
|
<template
|
||||||
|
x-for="(item, index) in filteredResults"
|
||||||
|
:key="item.url"
|
||||||
|
>
|
||||||
|
<li>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
@keydown.enter="selectItem(item)"
|
||||||
|
@keydown.escape="showResults = false"
|
||||||
|
@keydown.arrow-up.prevent="index > 0 ? $event.target.parentElement.previousElementSibling?.children[0]?.focus() : null"
|
||||||
|
@keydown.arrow-down.prevent="index < filteredResults.length - 1 ? $event.target.parentElement.nextElementSibling?.children[0]?.focus() : null"
|
||||||
|
class="w-full px-4 py-2 text-left hover:bg-base-200 focus:bg-base-200 focus:outline-none flex items-center space-x-2"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
x-text="item.type === 'user' ? '👤' : '👥'"
|
||||||
|
class="text-sm opacity-60"
|
||||||
|
></span>
|
||||||
|
<span x-text="item.display"></span>
|
||||||
|
<span
|
||||||
|
x-text="item.type === 'user' ? '(User)' : '(Group)'"
|
||||||
|
class="text-xs opacity-50 ml-auto"
|
||||||
|
></span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</template>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- No results message -->
|
||||||
|
<div
|
||||||
|
x-show="showResults && filteredResults.length === 0 && searchQuery.length > 0"
|
||||||
|
x-transition
|
||||||
|
class="absolute z-50 w-full mt-1 bg-base-100 border border-base-300 rounded-lg shadow-lg"
|
||||||
|
>
|
||||||
|
<div class="px-4 py-2 text-gray-500 text-sm">
|
||||||
|
No users or groups found
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-span-2 text-center">
|
<div class="col-span-2 text-center">
|
||||||
<label class="label justify-center">
|
<label class="label justify-center">
|
||||||
<span class="label-text">Read</span>
|
<span class="label-text">Read</span>
|
||||||
|
|||||||
@ -189,7 +189,8 @@
|
|||||||
x-data="schemaRenderer({
|
x-data="schemaRenderer({
|
||||||
rjsf: schemas[item.type.toLowerCase()],
|
rjsf: schemas[item.type.toLowerCase()],
|
||||||
data: item.data,
|
data: item.data,
|
||||||
mode: 'view'
|
mode: 'view',
|
||||||
|
attach_object: item.attach_object
|
||||||
})"
|
})"
|
||||||
x-init="init()"
|
x-init="init()"
|
||||||
>
|
>
|
||||||
|
|||||||
@ -218,6 +218,12 @@
|
|||||||
|
|
||||||
<input type="hidden" name="next" value="{{ next }}" />
|
<input type="hidden" name="next" value="{{ next }}" />
|
||||||
|
|
||||||
|
{% if CAP_ENABLED %}
|
||||||
|
<cap-widget
|
||||||
|
data-cap-api-endpoint="{{ CAP_API_BASE }}/{{ CAP_SITE_KEY }}/"
|
||||||
|
></cap-widget>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- ToS and Academic Use Notice -->
|
<!-- ToS and Academic Use Notice -->
|
||||||
<div class="text-xs text-base-content/70 mt-2">
|
<div class="text-xs text-base-content/70 mt-2">
|
||||||
<p>
|
<p>
|
||||||
@ -233,7 +239,6 @@
|
|||||||
enviPath is free for academic and non-commercial use only.
|
enviPath is free for academic and non-commercial use only.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button type="submit" name="confirmsignup" class="btn btn-success w-full">
|
<button type="submit" name="confirmsignup" class="btn btn-success w-full">
|
||||||
Sign Up
|
Sign Up
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -19,7 +19,16 @@
|
|||||||
type="text/css"
|
type="text/css"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{% block extra_styles %}{% endblock %}
|
{% if CAP_ENABLED %}
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/@cap.js/widget@0.1.41/cap.min.js"></script>
|
||||||
|
<link
|
||||||
|
rel="stylesheet"
|
||||||
|
href="https://cdn.jsdelivr.net/npm/@cap.js/widget@0.1.41/src/cap.min.css"
|
||||||
|
/>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% block extra_styles %}
|
||||||
|
{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body class="bg-base-100">
|
<body class="bg-base-100">
|
||||||
<div class="flex h-screen">
|
<div class="flex h-screen">
|
||||||
|
|||||||
@ -20,7 +20,16 @@ class TestPackagePage(EnviPyStaticLiveServerTestCase):
|
|||||||
page.get_by_role("button", name="Actions").click()
|
page.get_by_role("button", name="Actions").click()
|
||||||
page.get_by_role("button", name="Edit Permissions").click()
|
page.get_by_role("button", name="Edit Permissions").click()
|
||||||
# Add read and write permission to enviPath Users group
|
# Add read and write permission to enviPath Users group
|
||||||
page.locator("#select_grantee").select_option(label="enviPath Users")
|
search_input = page.locator('input[placeholder="Search users or groups..."]')
|
||||||
|
search_input.fill("enviPath")
|
||||||
|
|
||||||
|
# Wait for the results list to appear and be populated
|
||||||
|
page.wait_for_selector("#resultsList", state="visible")
|
||||||
|
|
||||||
|
# Click the first button in the results list
|
||||||
|
first_button = page.locator("#resultsList button").first
|
||||||
|
first_button.click()
|
||||||
|
|
||||||
page.locator("#read_new").check()
|
page.locator("#read_new").check()
|
||||||
page.locator("#write_new").check()
|
page.locator("#write_new").check()
|
||||||
page.get_by_role("button", name="+", exact=True).click()
|
page.get_by_role("button", name="+", exact=True).click()
|
||||||
|
|||||||
2
uv.lock
generated
2
uv.lock
generated
@ -841,7 +841,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "envipy-additional-information"
|
name = "envipy-additional-information"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#40459366648a03b01432998b32fdabd5556a1bae" }
|
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#04f6a01b8c5cd1342464e004e0cfaec9abc13ac5" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user