4 Commits

Author SHA1 Message Date
f7c45b8015 [Feature] Add legacy api endpoint to mimic ReferringScenarios (#362)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#362
2026-03-17 19:44:47 +13:00
68aea97013 [Feature] Simple template extension mechanism (#361)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#361
2026-03-16 21:06:20 +13:00
3cc7fa9e8b [Fix] Add Captcha vars to Template (#359)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#359
2026-03-13 11:46:34 +13:00
21f3390a43 [Feature] Add Captchas to avoid spam registrations (#358)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#358
2026-03-13 11:36:48 +13:00
14 changed files with 202 additions and 17 deletions

View File

@ -408,3 +408,9 @@ if MS_ENTRA_ENABLED:
# Site ID 10 -> beta.envipath.org
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)

View File

@ -16,6 +16,10 @@ class EPDBConfig(AppConfig):
model_name = getattr(settings, "EPDB_PACKAGE_MODEL", "epdb.Package")
logger.info(f"Using Package model: {model_name}")
from .autodiscovery import autodiscover
autodiscover()
if settings.PLUGINS_ENABLED:
from bridge.contracts import Property
from utilities.plugin import discover_plugins

5
epdb/autodiscovery.py Normal file
View File

@ -0,0 +1,5 @@
from django.utils.module_loading import autodiscover_modules
def autodiscover():
autodiscover_modules("epdb_hooks")

View File

@ -1,3 +1,4 @@
from collections import defaultdict
from typing import Any, Dict, List, Optional
import nh3
@ -11,8 +12,16 @@ from ninja.security import SessionAuth
from utilities.chem import FormatConverter
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 (
AdditionalInformation,
Compound,
CompoundStructure,
Edge,
@ -1329,7 +1338,14 @@ class ScenarioSchema(Schema):
@staticmethod
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
def resolve_review_status(obj: Rule):
@ -1394,7 +1410,11 @@ def create_package_scenario(request, package_uuid):
study_type = request.POST.get("type")
ais = []
types = request.POST.get("adInfoTypes[]", "").split(",")
types = request.POST.get("adInfoTypes[]", [])
if types:
types = types.split(",")
for t in types:
ais.append(build_additional_information_from_request(request, t))
@ -1436,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 #
###########

View File

@ -4191,7 +4191,6 @@ class AdditionalInformation(models.Model):
ai: "EnviPyModel",
scenario=None,
content_object=None,
skip_cleaning=False,
):
add_inf = AdditionalInformation()
add_inf.package = package

17
epdb/template_registry.py Normal file
View 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, [])]

View File

@ -2,6 +2,8 @@ from django import template
from pydantic import AnyHttpUrl, ValidationError
from pydantic.type_adapter import TypeAdapter
from epdb.template_registry import get_templates
register = template.Library()
url_adapter = TypeAdapter(AnyHttpUrl)
@ -19,3 +21,8 @@ def is_url(value):
return True
except ValidationError:
return False
@register.simple_tag
def epdb_slot_templates(slot):
return get_templates(slot)

View File

@ -3,6 +3,7 @@ import logging
from datetime import datetime
from typing import Any, Dict, List, Iterable
import requests
import nh3
from django.conf import settings as s
from django.contrib.auth import get_user_model
@ -147,6 +148,11 @@ def handler500(request):
def login(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":
context["title"] = "enviPath"
context["next"] = request.GET.get("next", "")
@ -224,6 +230,11 @@ def logout(request):
def register(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":
# Redirect to unified login page with signup tab
next_url = request.GET.get("next", "")
@ -238,6 +249,33 @@ def register(request):
if next := request.POST.get("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()
email = request.POST.get("email", "").strip()
password = request.POST.get("password", "").strip()

View File

@ -88,6 +88,7 @@ document.addEventListener("alpine:init", () => {
options.debugErrors ??
(typeof window !== "undefined" &&
window.location?.search?.includes("debugErrors=1")),
attach_object: options.attach_object || null,
async init() {
if (options.schemaUrl) {

View File

@ -1,8 +1,10 @@
{% extends "collections/paginated_base.html" %}
{% load envipytags %}
{% block page_title %}Compounds{% endblock %}
{% block action_button %}
<div class="flex items-center gap-2">
{% if meta.can_edit %}
<button
type="button"
@ -12,10 +14,21 @@
New Compound
</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 %}
{% block action_modals %}
{% 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 %}
{% block description %}

View File

@ -18,8 +18,25 @@
<!-- Schema form -->
<template x-if="schema && !loading">
<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 -->
<template x-if="schema['x-title'] || schema.title">
<template x-if="(schema['x-title'] || schema.title) && !attach_object">
<h4
class="text-lg font-semibold"
x-text="data.name || schema['x-title'] || schema.title"

View File

@ -189,7 +189,8 @@
x-data="schemaRenderer({
rjsf: schemas[item.type.toLowerCase()],
data: item.data,
mode: 'view'
mode: 'view',
attach_object: item.attach_object
})"
x-init="init()"
>

View File

@ -218,6 +218,12 @@
<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 -->
<div class="text-xs text-base-content/70 mt-2">
<p>
@ -233,7 +239,6 @@
enviPath is free for academic and non-commercial use only.
</p>
</div>
<button type="submit" name="confirmsignup" class="btn btn-success w-full">
Sign Up
</button>

View File

@ -19,7 +19,16 @@
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>
<body class="bg-base-100">
<div class="flex h-screen">