This commit is contained in:
Tim Lorsbach
2025-11-11 10:54:36 +01:00
56 changed files with 560 additions and 230 deletions

116
.gitea/workflows/ci.yaml Normal file
View File

@ -0,0 +1,116 @@
name: CI
on:
pull_request:
branches:
- develop
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16
env:
POSTGRES_USER: ${{ vars.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ vars.POSTGRES_DB }}
ports:
- ${{ vars.POSTGRES_PORT}}:5432
options: >-
--health-cmd="pg_isready -U postgres"
--health-interval=10s
--health-timeout=5s
--health-retries=5
#redis:
# image: redis:7
# ports:
# - 6379:6379
# options: >-
# --health-cmd "redis-cli ping"
# --health-interval=10s
# --health-timeout=5s
# --health-retries=5
env:
RUNNER_TOOL_CACHE: /toolcache
EP_DATA_DIR: /opt/enviPy/
ALLOWED_HOSTS: 127.0.0.1,localhost
DEBUG: True
LOG_LEVEL: DEBUG
MODEL_BUILDING_ENABLED: True
APPLICABILITY_DOMAIN_ENABLED: True
ENVIFORMER_PRESENT: True
ENVIFORMER_DEVICE: cpu
FLAG_CELERY_PRESENT: False
PLUGINS_ENABLED: True
SERVER_URL: http://localhost:8000
ADMIN_APPROVAL_REQUIRED: True
REGISTRATION_MANDATORY: True
LOG_DIR: ''
# DB
POSTGRES_SERVICE_NAME: postgres
POSTGRES_DB: ${{ vars.POSTGRES_DB }}
POSTGRES_USER: ${{ vars.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
POSTGRES_PORT: 5432
# SENTRY
SENTRY_ENABLED: False
# MS ENTRA
MS_ENTRA_ENABLED: False
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install system tools via apt
run: |
sudo apt-get update
sudo apt-get install -y postgresql-client redis-tools openjdk-11-jre-headless
- name: Setup ssh
run: |
echo "${{ secrets.ENVIPY_CI_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan git.envipath.com >> ~/.ssh/known_hosts
eval $(ssh-agent -s)
ssh-add ~/.ssh/id_ed25519
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 20
cache: "pnpm"
- name: Install uv
uses: astral-sh/setup-uv@v6
with:
enable-cache: true
- name: Setup venv
run: |
uv sync --locked --all-extras --dev
- name: Wait for services
run: |
until pg_isready -h postgres -U postgres; do sleep 2; done
# until redis-cli -h redis ping; do sleep 2; done
- name: Run Django migrations
run: |
source .venv/bin/activate
python manage.py migrate --noinput
- name: Run Django tests
run: |
source .venv/bin/activate
python manage.py test tests --exclude-tag slow

View File

@ -92,6 +92,8 @@ TEMPLATES = [
}, },
] ]
ALLOWED_HTML_TAGS = {"b", "i", "u", "br", "em", "mark", "p", "s", "strong"}
WSGI_APPLICATION = "envipath.wsgi.application" WSGI_APPLICATION = "envipath.wsgi.application"
# Database # Database

View File

@ -4,6 +4,7 @@ import json
from typing import Union, List, Optional, Set, Dict, Any from typing import Union, List, Optional, Set, Dict, Any
from uuid import UUID from uuid import UUID
import nh3
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db import transaction from django.db import transaction
from django.conf import settings as s from django.conf import settings as s
@ -185,6 +186,12 @@ class UserManager(object):
def create_user( def create_user(
username, email, password, set_setting=True, add_to_group=True, *args, **kwargs username, email, password, set_setting=True, add_to_group=True, *args, **kwargs
): ):
# Clean for potential XSS
clean_username = nh3.clean(username).strip()
clean_email = nh3.clean(email).strip()
if clean_username != username or clean_email != email:
# This will be caught by the try in view.py/register
raise ValueError("Invalid username or password")
# avoid circular import :S # avoid circular import :S
from .tasks import send_registration_mail from .tasks import send_registration_mail
@ -262,8 +269,9 @@ class GroupManager(object):
@staticmethod @staticmethod
def create_group(current_user, name, description): def create_group(current_user, name, description):
g = Group() g = Group()
g.name = name # Clean for potential XSS
g.description = description g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
g.owner = current_user g.owner = current_user
g.save() g.save()
@ -518,8 +526,13 @@ class PackageManager(object):
@transaction.atomic @transaction.atomic
def create_package(current_user, name: str, description: str = None): def create_package(current_user, name: str, description: str = None):
p = Package() p = Package()
p.name = name
p.description = description # Clean for potential XSS
p.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None and description.strip() != "":
p.description = nh3.clean(description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
p.save() p.save()
up = UserPackagePermission() up = UserPackagePermission()
@ -1094,28 +1107,29 @@ class SettingManager(object):
model: EPModel = None, model: EPModel = None,
model_threshold: float = None, model_threshold: float = None,
): ):
s = Setting() new_s = Setting()
s.name = name # Clean for potential XSS
s.description = description new_s.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
s.max_nodes = max_nodes new_s.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
s.max_depth = max_depth new_s.max_nodes = max_nodes
s.model = model new_s.max_depth = max_depth
s.model_threshold = model_threshold new_s.model = model
new_s.model_threshold = model_threshold
s.save() new_s.save()
if rule_packages is not None: if rule_packages is not None:
for r in rule_packages: for r in rule_packages:
s.rule_packages.add(r) new_s.rule_packages.add(r)
s.save() new_s.save()
usp = UserSettingPermission() usp = UserSettingPermission()
usp.user = user usp.user = user
usp.setting = s usp.setting = new_s
usp.permission = Permission.ALL[0] usp.permission = Permission.ALL[0]
usp.save() usp.save()
return s return new_s
@staticmethod @staticmethod
def get_default_setting(user: User): def get_default_setting(user: User):
@ -1542,7 +1556,9 @@ class SPathway(object):
if sub.app_domain_assessment is None: if sub.app_domain_assessment is None:
if self.prediction_setting.model: if self.prediction_setting.model:
if self.prediction_setting.model.app_domain: if self.prediction_setting.model.app_domain:
app_domain_assessment = self.prediction_setting.model.app_domain.assess(sub.smiles) app_domain_assessment = self.prediction_setting.model.app_domain.assess(
sub.smiles
)
if self.persist is not None: if self.persist is not None:
n = self.snode_persist_lookup[sub] n = self.snode_persist_lookup[sub]
@ -1574,7 +1590,9 @@ class SPathway(object):
app_domain_assessment = None app_domain_assessment = None
if self.prediction_setting.model: if self.prediction_setting.model:
if self.prediction_setting.model.app_domain: if self.prediction_setting.model.app_domain:
app_domain_assessment = (self.prediction_setting.model.app_domain.assess(c)) app_domain_assessment = (
self.prediction_setting.model.app_domain.assess(c)
)
self.smiles_to_node[c] = SNode( self.smiles_to_node[c] = SNode(
c, sub.depth + 1, app_domain_assessment c, sub.depth + 1, app_domain_assessment

View File

@ -11,6 +11,7 @@ from typing import Union, List, Optional, Dict, Tuple, Set, Any
from uuid import uuid4 from uuid import uuid4
import math import math
import joblib import joblib
import nh3
import numpy as np import numpy as np
from django.conf import settings as s from django.conf import settings as s
from django.contrib.auth.models import AbstractUser from django.contrib.auth.models import AbstractUser
@ -28,8 +29,14 @@ from sklearn.metrics import precision_score, recall_score, jaccard_score
from sklearn.model_selection import ShuffleSplit from sklearn.model_selection import ShuffleSplit
from utilities.chem import FormatConverter, ProductSet, PredictionResult, IndigoUtils from utilities.chem import FormatConverter, ProductSet, PredictionResult, IndigoUtils
from utilities.ml import RuleBasedDataset, ApplicabilityDomainPCA, EnsembleClassifierChain, RelativeReasoning, \ from utilities.ml import (
EnviFormerDataset, Dataset RuleBasedDataset,
ApplicabilityDomainPCA,
EnsembleClassifierChain,
RelativeReasoning,
EnviFormerDataset,
Dataset,
)
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -803,14 +810,16 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
c = Compound() c = Compound()
c.package = package c.package = package
if name is None or name.strip() == "": if name is not None:
# Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Compound {Compound.objects.filter(package=package).count() + 1}" name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
c.name = name c.name = name
# We have a default here only set the value if it carries some payload # We have a default here only set the value if it carries some payload
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
c.description = description.strip() c.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
c.save() c.save()
@ -982,11 +991,11 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
raise ValueError("Unpersisted Compound! Persist compound first!") raise ValueError("Unpersisted Compound! Persist compound first!")
cs = CompoundStructure() cs = CompoundStructure()
# Clean for potential XSS
if name is not None: if name is not None:
cs.name = name cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None: if description is not None:
cs.description = description cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
cs.smiles = smiles cs.smiles = smiles
cs.compound = compound cs.compound = compound
@ -1188,21 +1197,29 @@ class SimpleAmbitRule(SimpleRule):
r = SimpleAmbitRule() r = SimpleAmbitRule()
r.package = package r.package = package
if name is None or name.strip() == "": 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}" name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
r.name = name r.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
r.description = description r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r.smirks = smirks r.smirks = smirks
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "": if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
r.reactant_filter_smarts = reactant_filter_smarts 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 product_filter_smarts is not None and product_filter_smarts.strip() != "":
r.product_filter_smarts = product_filter_smarts 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() r.save()
return r return r
@ -1403,12 +1420,11 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
r = Reaction() r = Reaction()
r.package = package r.package = package
# Clean for potential XSS
if name is not None and name.strip() != "": if name is not None and name.strip() != "":
r.name = name r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None and name.strip() != "": if description is not None and name.strip() != "":
r.description = description r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r.multi_step = multi_step r.multi_step = multi_step
@ -1716,14 +1732,15 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
): ):
pw = Pathway() pw = Pathway()
pw.package = package pw.package = package
if name is not None:
if name is None or name.strip() == "": # Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Pathway {Pathway.objects.filter(package=package).count() + 1}" name = f"Pathway {Pathway.objects.filter(package=package).count() + 1}"
pw.name = name pw.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
pw.description = description pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
pw.save() pw.save()
try: try:
@ -2018,11 +2035,16 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
for node in end_nodes: for node in end_nodes:
e.end_nodes.add(node) e.end_nodes.add(node)
if name is None: # Clean for potential XSS
# Cleaning technically not needed as it is also done in Reaction.create, including it here for consistency
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Reaction {pathway.package.reactions.count() + 1}" name = f"Reaction {pathway.package.reactions.count() + 1}"
if description is None: if description is None:
description = s.DEFAULT_VALUES["description"] description = s.DEFAULT_VALUES["description"]
description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r = Reaction.create( r = Reaction.create(
pathway.package, pathway.package,
@ -2344,7 +2366,9 @@ class PackageBasedModel(EPModel):
eval_reactions = list( eval_reactions = list(
Reaction.objects.filter(package__in=self.eval_packages.all()).distinct() Reaction.objects.filter(package__in=self.eval_packages.all()).distinct()
) )
ds = RuleBasedDataset.generate_dataset(eval_reactions, self.applicable_rules, educts_only=True) ds = RuleBasedDataset.generate_dataset(
eval_reactions, self.applicable_rules, educts_only=True
)
if isinstance(self, RuleBasedRelativeReasoning): if isinstance(self, RuleBasedRelativeReasoning):
X = ds.X(exclude_id_col=False, na_replacement=None).to_numpy() X = ds.X(exclude_id_col=False, na_replacement=None).to_numpy()
y = ds.y(na_replacement=np.nan).to_numpy() y = ds.y(na_replacement=np.nan).to_numpy()
@ -2542,14 +2566,15 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
): ):
rbrr = RuleBasedRelativeReasoning() rbrr = RuleBasedRelativeReasoning()
rbrr.package = package rbrr.package = package
if name is not None:
if name is None or name.strip() == "": # Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"RuleBasedRelativeReasoning {RuleBasedRelativeReasoning.objects.filter(package=package).count() + 1}" name = f"RuleBasedRelativeReasoning {RuleBasedRelativeReasoning.objects.filter(package=package).count() + 1}"
rbrr.name = name rbrr.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
rbrr.description = description rbrr.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if threshold is None or (threshold <= 0 or 1 <= threshold): if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.") raise ValueError("Threshold must be a float between 0 and 1.")
@ -2646,14 +2671,15 @@ class MLRelativeReasoning(PackageBasedModel):
): ):
mlrr = MLRelativeReasoning() mlrr = MLRelativeReasoning()
mlrr.package = package mlrr.package = package
if name is not None:
if name is None or name.strip() == "": # Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"MLRelativeReasoning {MLRelativeReasoning.objects.filter(package=package).count() + 1}" name = f"MLRelativeReasoning {MLRelativeReasoning.objects.filter(package=package).count() + 1}"
mlrr.name = name mlrr.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
mlrr.description = description mlrr.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if threshold is None or (threshold <= 0 or 1 <= threshold): if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.") raise ValueError("Threshold must be a float between 0 and 1.")
@ -2807,7 +2833,9 @@ class ApplicabilityDomain(EnviPathModel):
else: else:
smiles.append(structures) smiles.append(structures)
assessment_ds, assessment_prods = ds.classification_dataset(structures, self.model.applicable_rules) assessment_ds, assessment_prods = ds.classification_dataset(
structures, self.model.applicable_rules
)
# qualified_neighbours_per_rule is a nested dictionary structured as: # qualified_neighbours_per_rule is a nested dictionary structured as:
# { # {
@ -2823,12 +2851,16 @@ class ApplicabilityDomain(EnviPathModel):
qualified_neighbours_per_rule: Dict = {} qualified_neighbours_per_rule: Dict = {}
import polars as pl import polars as pl
# Select only the triggered columns # Select only the triggered columns
for i, row in enumerate(assessment_ds[:, assessment_ds.triggered()].iter_rows(named=True)): for i, row in enumerate(assessment_ds[:, assessment_ds.triggered()].iter_rows(named=True)):
# Find the rules the structure triggers. For each rule, filter the training dataset to rows that also # Find the rules the structure triggers. For each rule, filter the training dataset to rows that also
# trigger that rule. # trigger that rule.
train_trig = {trig_uuid.split("_")[-1]: ds.filter(pl.col(trig_uuid).eq(1)) train_trig = {
for trig_uuid, value in row.items() if value == 1} trig_uuid.split("_")[-1]: ds.filter(pl.col(trig_uuid).eq(1))
for trig_uuid, value in row.items()
if value == 1
}
qualified_neighbours_per_rule[i] = train_trig qualified_neighbours_per_rule[i] = train_trig
rule_to_i = {str(r.uuid): i for i, r in enumerate(self.model.applicable_rules)} rule_to_i = {str(r.uuid): i for i, r in enumerate(self.model.applicable_rules)}
preds = self.model.combine_products_and_probs( preds = self.model.combine_products_and_probs(
@ -2848,18 +2880,28 @@ class ApplicabilityDomain(EnviPathModel):
# loop through rule indices together with the collected neighbours indices from train dataset # loop through rule indices together with the collected neighbours indices from train dataset
for rule_uuid, train_instances in qualified_neighbours_per_rule[i].items(): for rule_uuid, train_instances in qualified_neighbours_per_rule[i].items():
# compute tanimoto distance for all neighbours and add to dataset # compute tanimoto distance for all neighbours and add to dataset
dists = self._compute_distances(assessment_ds[i, assessment_ds.struct_features()].to_numpy()[0], dists = self._compute_distances(
train_instances[:, train_instances.struct_features()].to_numpy()) assessment_ds[i, assessment_ds.struct_features()].to_numpy()[0],
train_instances[:, train_instances.struct_features()].to_numpy(),
)
train_instances = train_instances.with_columns(dist=pl.Series(dists)) train_instances = train_instances.with_columns(dist=pl.Series(dists))
# sort them in a descending way and take at most `self.num_neighbours` # sort them in a descending way and take at most `self.num_neighbours`
# TODO: Should this be descending? If we want the most similar then we want values close to zero (ascending) # TODO: Should this be descending? If we want the most similar then we want values close to zero (ascending)
train_instances = train_instances.sort("dist", descending=True)[:self.num_neighbours] train_instances = train_instances.sort("dist", descending=True)[
: self.num_neighbours
]
# compute average distance # compute average distance
rule_reliabilities[rule_uuid] = train_instances.select(pl.mean("dist")).fill_nan(0.0).item() rule_reliabilities[rule_uuid] = (
train_instances.select(pl.mean("dist")).fill_nan(0.0).item()
)
# for local_compatibility we'll need the datasets for the indices having the highest similarity # for local_compatibility we'll need the datasets for the indices having the highest similarity
local_compatibilities[rule_uuid] = self._compute_compatibility(rule_uuid, train_instances) local_compatibilities[rule_uuid] = self._compute_compatibility(
neighbours_per_rule[rule_uuid] = list(CompoundStructure.objects.filter(uuid__in=train_instances["structure_id"])) rule_uuid, train_instances
)
neighbours_per_rule[rule_uuid] = list(
CompoundStructure.objects.filter(uuid__in=train_instances["structure_id"])
)
neighbor_probs_per_rule[rule_uuid] = train_instances[f"prob_{rule_uuid}"].to_list() neighbor_probs_per_rule[rule_uuid] = train_instances[f"prob_{rule_uuid}"].to_list()
ad_res = { ad_res = {
@ -2933,8 +2975,11 @@ class ApplicabilityDomain(EnviPathModel):
def _compute_compatibility(self, rule_idx: int, neighbours: "RuleBasedDataset"): def _compute_compatibility(self, rule_idx: int, neighbours: "RuleBasedDataset"):
accuracy = 0.0 accuracy = 0.0
import polars as pl import polars as pl
obs_pred = neighbours.select(obs=pl.col(f"obs_{rule_idx}").cast(pl.Boolean),
pred=pl.col(f"prob_{rule_idx}") >= self.model.threshold) obs_pred = neighbours.select(
obs=pl.col(f"obs_{rule_idx}").cast(pl.Boolean),
pred=pl.col(f"prob_{rule_idx}") >= self.model.threshold,
)
# Compute tp, tn, fp, fn using polars expressions # Compute tp, tn, fp, fn using polars expressions
tp = obs_pred.filter((pl.col("obs")) & (pl.col("pred"))).height tp = obs_pred.filter((pl.col("obs")) & (pl.col("pred"))).height
tn = obs_pred.filter((~pl.col("obs")) & (~pl.col("pred"))).height tn = obs_pred.filter((~pl.col("obs")) & (~pl.col("pred"))).height
@ -2961,14 +3006,15 @@ class EnviFormer(PackageBasedModel):
): ):
mod = EnviFormer() mod = EnviFormer()
mod.package = package mod.package = package
if name is not None:
if name is None or name.strip() == "": # Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"EnviFormer {EnviFormer.objects.filter(package=package).count() + 1}" name = f"EnviFormer {EnviFormer.objects.filter(package=package).count() + 1}"
mod.name = name mod.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
mod.description = description mod.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if threshold is None or (threshold <= 0 or 1 <= threshold): if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.") raise ValueError("Threshold must be a float between 0 and 1.")
@ -3103,7 +3149,7 @@ class EnviFormer(PackageBasedModel):
pred_dict = {} pred_dict = {}
for k, pred in enumerate(predictions): for k, pred in enumerate(predictions):
pred_smiles, pred_proba = zip(*pred.items()) pred_smiles, pred_proba = zip(*pred.items())
reactant, true_product = test_ds[k, "educts"], test_ds[k, "products"] reactant, _ = test_ds[k, "educts"], test_ds[k, "products"]
pred_dict.setdefault(reactant, {"predict": [], "scores": []}) pred_dict.setdefault(reactant, {"predict": [], "scores": []})
for smiles, proba in zip(pred_smiles, pred_proba): for smiles, proba in zip(pred_smiles, pred_proba):
smiles = set(smiles.split(".")) smiles = set(smiles.split("."))
@ -3217,8 +3263,9 @@ class EnviFormer(PackageBasedModel):
# If there are eval packages perform single generation evaluation on them instead of random splits # If there are eval packages perform single generation evaluation on them instead of random splits
if self.eval_packages.count() > 0: if self.eval_packages.count() > 0:
ds = EnviFormerDataset.generate_dataset(Reaction.objects.filter( ds = EnviFormerDataset.generate_dataset(
package__in=self.eval_packages.all()).distinct()) Reaction.objects.filter(package__in=self.eval_packages.all()).distinct()
)
test_result = self.model.predict_batch(ds.X()) test_result = self.model.predict_batch(ds.X())
single_gen_result = evaluate_sg(ds, test_result, self.threshold) single_gen_result = evaluate_sg(ds, test_result, self.threshold)
self.eval_results = self.compute_averages([single_gen_result]) self.eval_results = self.compute_averages([single_gen_result])
@ -3236,7 +3283,9 @@ class EnviFormer(PackageBasedModel):
train = ds[train_index] train = ds[train_index]
test = ds[test_index] test = ds[test_index]
start = datetime.now() start = datetime.now()
model = fine_tune(train.X(), train.y(), s.MODEL_DIR, str(split_id), device=s.ENVIFORMER_DEVICE) model = fine_tune(
train.X(), train.y(), s.MODEL_DIR, str(split_id), device=s.ENVIFORMER_DEVICE
)
end = datetime.now() end = datetime.now()
logger.debug( logger.debug(
f"EnviFormer finetuning took {(end - start).total_seconds():.2f} seconds" f"EnviFormer finetuning took {(end - start).total_seconds():.2f} seconds"
@ -3313,7 +3362,12 @@ class EnviFormer(PackageBasedModel):
for pathway in train_pathways: for pathway in train_pathways:
for reaction in pathway.edges: for reaction in pathway.edges:
reaction = reaction.edge_label reaction = reaction.edge_label
if any([educt in test_educts for educt in reaction_to_educts[str(reaction.uuid)]]): if any(
[
educt in test_educts
for educt in reaction_to_educts[str(reaction.uuid)]
]
):
overlap += 1 overlap += 1
continue continue
train_reactions.append(reaction) train_reactions.append(reaction)
@ -3370,41 +3424,44 @@ class Scenario(EnviPathModel):
scenario_type: str, scenario_type: str,
additional_information: List["EnviPyModel"], additional_information: List["EnviPyModel"],
): ):
s = Scenario() new_s = Scenario()
s.package = package new_s.package = package
if name is not None:
if name is None or name.strip() == "": # Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}" name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}"
new_s.name = name
s.name = name
if description is not None and description.strip() != "": if description is not None and description.strip() != "":
s.description = description new_s.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if scenario_date is not None and scenario_date.strip() != "": if scenario_date is not None and scenario_date.strip() != "":
s.scenario_date = scenario_date new_s.scenario_date = nh3.clean(scenario_date).strip()
if scenario_type is not None and scenario_type.strip() != "": if scenario_type is not None and scenario_type.strip() != "":
s.scenario_type = scenario_type new_s.scenario_type = scenario_type
add_inf = defaultdict(list) add_inf = defaultdict(list)
for info in additional_information: for info in additional_information:
cls_name = info.__class__.__name__ cls_name = info.__class__.__name__
ai_data = json.loads(info.model_dump_json()) # Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(info.model_dump_json()).strip())
ai_data["uuid"] = f"{uuid4()}" ai_data["uuid"] = f"{uuid4()}"
add_inf[cls_name].append(ai_data) add_inf[cls_name].append(ai_data)
s.additional_information = add_inf new_s.additional_information = add_inf
s.save() new_s.save()
return s return new_s
@transaction.atomic @transaction.atomic
def add_additional_information(self, data: "EnviPyModel"): def add_additional_information(self, data: "EnviPyModel"):
cls_name = data.__class__.__name__ cls_name = data.__class__.__name__
ai_data = json.loads(data.model_dump_json()) # Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
ai_data["uuid"] = f"{uuid4()}" ai_data["uuid"] = f"{uuid4()}"
if cls_name not in self.additional_information: if cls_name not in self.additional_information:
@ -3439,7 +3496,8 @@ class Scenario(EnviPathModel):
new_ais = defaultdict(list) new_ais = defaultdict(list)
for k, vals in data.items(): for k, vals in data.items():
for v in vals: for v in vals:
ai_data = json.loads(v.model_dump_json()) # Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(v.model_dump_json()).strip())
if hasattr(v, "uuid"): if hasattr(v, "uuid"):
ai_data["uuid"] = str(v.uuid) ai_data["uuid"] = str(v.uuid)
else: else:

View File

@ -10,6 +10,7 @@ from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from envipy_additional_information import NAME_MAPPING from envipy_additional_information import NAME_MAPPING
from oauth2_provider.decorators import protected_resource from oauth2_provider.decorators import protected_resource
import nh3
from utilities.chem import FormatConverter, IndigoUtils from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required from utilities.decorators import package_permission_required
@ -85,7 +86,10 @@ def login(request):
from django.contrib.auth import authenticate from django.contrib.auth import authenticate
from django.contrib.auth import login from django.contrib.auth import login
username = request.POST.get("username") username = request.POST.get("username").strip()
if username != request.POST.get("username"):
context["message"] = "Login failed!"
return render(request, "static/login.html", context)
password = request.POST.get("password") password = request.POST.get("password")
# Get email for username and check if the account is active # Get email for username and check if the account is active
@ -673,7 +677,8 @@ def search(request):
if request.method == "GET": if request.method == "GET":
package_urls = request.GET.getlist("packages") package_urls = request.GET.getlist("packages")
searchterm = request.GET.get("search") searchterm = request.GET.get("search").strip()
mode = request.GET.get("mode") mode = request.GET.get("mode")
# add HTTP_ACCEPT check to differentiate between index and ajax call # add HTTP_ACCEPT check to differentiate between index and ajax call
@ -774,7 +779,6 @@ def package_models(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
log_post_params(request) log_post_params(request)
name = request.POST.get("model-name") name = request.POST.get("model-name")
description = request.POST.get("model-description") description = request.POST.get("model-description")
@ -939,8 +943,14 @@ def package_model(request, package_uuid, model_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
else: else:
name = request.POST.get("model-name", "").strip() # TODO: Move cleaning to property updater
description = request.POST.get("model-description", "").strip() name = request.POST.get("model-name")
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
description = request.POST.get("model-description")
if description is not None:
description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if any([name, description]): if any([name, description]):
if name: if name:
@ -1042,8 +1052,16 @@ def package(request, package_uuid):
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
# TODO: Move cleaning to property updater
new_package_name = request.POST.get("package-name") new_package_name = request.POST.get("package-name")
if new_package_name is not None:
new_package_name = nh3.clean(new_package_name, tags=s.ALLOWED_HTML_TAGS).strip()
new_package_description = request.POST.get("package-description") new_package_description = request.POST.get("package-description")
if new_package_description is not None:
new_package_description = nh3.clean(
new_package_description, tags=s.ALLOWED_HTML_TAGS
).strip()
grantee_url = request.POST.get("grantee") grantee_url = request.POST.get("grantee")
read = request.POST.get("read") == "on" read = request.POST.get("read") == "on"
@ -1152,7 +1170,7 @@ def package_compounds(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
compound_name = request.POST.get("compound-name") compound_name = request.POST.get("compound-name")
compound_smiles = request.POST.get("compound-smiles") compound_smiles = request.POST.get("compound-smiles").strip()
compound_description = request.POST.get("compound-description") compound_description = request.POST.get("compound-description")
c = Compound.create(current_package, compound_smiles, compound_name, compound_description) c = Compound.create(current_package, compound_smiles, compound_name, compound_description)
@ -1205,8 +1223,16 @@ def package_compound(request, package_uuid, compound_uuid):
return JsonResponse({"success": current_compound.url}) return JsonResponse({"success": current_compound.url})
new_compound_name = request.POST.get("compound-name", "").strip() # TODO: Move cleaning to property updater
new_compound_description = request.POST.get("compound-description", "").strip() new_compound_name = request.POST.get("compound-name")
if new_compound_name is not None:
new_compound_name = nh3.clean(new_compound_name, tags=s.ALLOWED_HTML_TAGS).strip()
new_compound_description = request.POST.get("compound-description")
if new_compound_description is not None:
new_compound_description = nh3.clean(
new_compound_description, tags=s.ALLOWED_HTML_TAGS
).strip()
if new_compound_name: if new_compound_name:
current_compound.name = new_compound_name current_compound.name = new_compound_name
@ -1271,7 +1297,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
elif request.method == "POST": elif request.method == "POST":
structure_name = request.POST.get("structure-name") structure_name = request.POST.get("structure-name")
structure_smiles = request.POST.get("structure-smiles") structure_smiles = request.POST.get("structure-smiles").strip()
structure_description = request.POST.get("structure-description") structure_description = request.POST.get("structure-description")
try: try:
@ -1342,8 +1368,16 @@ def package_compound_structure(request, package_uuid, compound_uuid, structure_u
else: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
new_structure_name = request.POST.get("compound-structure-name", "").strip() # TODO: Move cleaning to property updater
new_structure_description = request.POST.get("compound-structure-description", "").strip() new_structure_name = request.POST.get("compound-structure-name")
if new_structure_name is not None:
new_structure_name = nh3.clean(new_structure_name, tags=s.ALLOWED_HTML_TAGS).strip()
new_structure_description = request.POST.get("compound-structure-description")
if new_structure_description is not None:
new_structure_description = nh3.clean(
new_structure_description, tags=s.ALLOWED_HTML_TAGS
).strip()
if new_structure_name: if new_structure_name:
current_structure.name = new_structure_name current_structure.name = new_structure_name
@ -1445,11 +1479,11 @@ def package_rules(request, package_uuid):
# Obtain parameters as required by rule type # Obtain parameters as required by rule type
if rule_type == "SimpleAmbitRule": if rule_type == "SimpleAmbitRule":
params["smirks"] = request.POST.get("rule-smirks") params["smirks"] = request.POST.get("rule-smirks").strip()
params["reactant_filter_smarts"] = request.POST.get("rule-reactant-smarts") params["reactant_filter_smarts"] = request.POST.get("rule-reactant-smarts")
params["product_filter_smarts"] = request.POST.get("rule-product-smarts") params["product_filter_smarts"] = request.POST.get("rule-product-smarts")
elif rule_type == "SimpleRDKitRule": elif rule_type == "SimpleRDKitRule":
params["reaction_smarts"] = request.POST.get("rule-reaction-smarts") params["reaction_smarts"] = request.POST.get("rule-reaction-smarts").strip()
elif rule_type == "ParallelRule": elif rule_type == "ParallelRule":
pass pass
elif rule_type == "SequentialRule": elif rule_type == "SequentialRule":
@ -1550,8 +1584,14 @@ def package_rule(request, package_uuid, rule_uuid):
return JsonResponse({"success": current_rule.url}) return JsonResponse({"success": current_rule.url})
rule_name = request.POST.get("rule-name", "").strip() # TODO: Move cleaning to property updater
rule_description = request.POST.get("rule-description", "").strip() rule_name = request.POST.get("rule-name")
if rule_name is not None:
rule_name = nh3.clean(rule_name, tags=s.ALLOWED_HTML_TAGS).strip()
rule_description = request.POST.get("rule-description")
if rule_description is not None:
rule_description = nh3.clean(rule_description, tags=s.ALLOWED_HTML_TAGS).strip()
if rule_name: if rule_name:
current_rule.name = rule_name current_rule.name = rule_name
@ -1640,8 +1680,8 @@ def package_reactions(request, package_uuid):
elif request.method == "POST": elif request.method == "POST":
reaction_name = request.POST.get("reaction-name") reaction_name = request.POST.get("reaction-name")
reaction_description = request.POST.get("reaction-description") reaction_description = request.POST.get("reaction-description")
reactions_smirks = request.POST.get("reaction-smirks")
reactions_smirks = request.POST.get("reaction-smirks").strip()
educts = reactions_smirks.split(">>")[0].split(".") educts = reactions_smirks.split(">>")[0].split(".")
products = reactions_smirks.split(">>")[1].split(".") products = reactions_smirks.split(">>")[1].split(".")
@ -1702,8 +1742,16 @@ def package_reaction(request, package_uuid, reaction_uuid):
return JsonResponse({"success": current_reaction.url}) return JsonResponse({"success": current_reaction.url})
new_reaction_name = request.POST.get("reaction-name", "").strip() # TODO: Move cleaning to property updater
new_reaction_description = request.POST.get("reaction-description", "").strip() new_reaction_name = request.POST.get("reaction-name")
if new_reaction_name is not None:
new_reaction_name = nh3.clean(new_reaction_name, tags=s.ALLOWED_HTML_TAGS).strip()
new_reaction_description = request.POST.get("reaction-description")
if new_reaction_description is not None:
new_reaction_description = nh3.clean(
new_reaction_description, tags=s.ALLOWED_HTML_TAGS
).strip()
if new_reaction_name: if new_reaction_name:
current_reaction.name = new_reaction_name current_reaction.name = new_reaction_name
@ -1780,8 +1828,9 @@ def package_pathways(request, package_uuid):
name = request.POST.get("name") name = request.POST.get("name")
description = request.POST.get("description") description = request.POST.get("description")
pw_mode = request.POST.get("predict", "predict").strip()
smiles = request.POST.get("smiles", "").strip() smiles = request.POST.get("smiles", "").strip()
pw_mode = request.POST.get("predict", "predict").strip()
if "smiles" in request.POST and smiles == "": if "smiles" in request.POST and smiles == "":
return error( return error(
@ -1790,8 +1839,6 @@ def package_pathways(request, package_uuid):
"Pathway prediction failed due to missing or empty SMILES", "Pathway prediction failed due to missing or empty SMILES",
) )
smiles = smiles.strip()
try: try:
stand_smiles = FormatConverter.standardize(smiles) stand_smiles = FormatConverter.standardize(smiles)
except ValueError: except ValueError:
@ -1950,8 +1997,14 @@ def package_pathway(request, package_uuid, pathway_uuid):
return JsonResponse({"success": current_pathway.url}) return JsonResponse({"success": current_pathway.url})
# TODO: Move cleaning to property updater
pathway_name = request.POST.get("pathway-name") pathway_name = request.POST.get("pathway-name")
if pathway_name is not None:
pathway_name = nh3.clean(pathway_name, tags=s.ALLOWED_HTML_TAGS).strip()
pathway_description = request.POST.get("pathway-description") pathway_description = request.POST.get("pathway-description")
if pathway_description is not None:
pathway_description = nh3.clean(pathway_description, tags=s.ALLOWED_HTML_TAGS).strip()
if any([pathway_name, pathway_description]): if any([pathway_name, pathway_description]):
if pathway_name is not None and pathway_name.strip() != "": if pathway_name is not None and pathway_name.strip() != "":
@ -2039,8 +2092,8 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
elif request.method == "POST": elif request.method == "POST":
node_name = request.POST.get("node-name") node_name = request.POST.get("node-name")
node_description = request.POST.get("node-description") node_description = request.POST.get("node-description")
node_smiles = request.POST.get("node-smiles")
node_smiles = request.POST.get("node-smiles").strip()
current_pathway.add_node(node_smiles, name=node_name, description=node_description) current_pathway.add_node(node_smiles, name=node_name, description=node_description)
return redirect(current_pathway.url) return redirect(current_pathway.url)
@ -2202,9 +2255,9 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
elif request.method == "POST": elif request.method == "POST":
log_post_params(request) log_post_params(request)
edge_name = request.POST.get("edge-name") edge_name = request.POST.get("edge-name")
edge_description = request.POST.get("edge-description") edge_description = request.POST.get("edge-description")
edge_substrates = request.POST.getlist("edge-substrates") edge_substrates = request.POST.getlist("edge-substrates")
edge_products = request.POST.getlist("edge-products") edge_products = request.POST.getlist("edge-products")
@ -2291,7 +2344,7 @@ def package_scenarios(request, package_uuid):
"all", False "all", False
): ):
scens = Scenario.objects.filter(package=current_package).order_by("name") scens = Scenario.objects.filter(package=current_package).order_by("name")
res = [{"name": s.name, "url": s.url, "uuid": s.uuid} for s in scens] res = [{"name": s_.name, "url": s_.url, "uuid": s_.uuid} for s_ in scens]
return JsonResponse(res, safe=False) return JsonResponse(res, safe=False)
context = get_base_context(request) context = get_base_context(request)
@ -2339,21 +2392,21 @@ def package_scenarios(request, package_uuid):
"name": "soil", "name": "soil",
"widgets": [ "widgets": [
HTMLGenerator.generate_html(ai, prefix=f"soil_{0}") HTMLGenerator.generate_html(ai, prefix=f"soil_{0}")
for ai in [x for s in SOIL_ADDITIONAL_INFORMATION.values() for x in s] for ai in [x for sv in SOIL_ADDITIONAL_INFORMATION.values() for x in sv]
], ],
}, },
"Sludge Data": { "Sludge Data": {
"name": "sludge", "name": "sludge",
"widgets": [ "widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sludge_{0}") HTMLGenerator.generate_html(ai, prefix=f"sludge_{0}")
for ai in [x for s in SLUDGE_ADDITIONAL_INFORMATION.values() for x in s] for ai in [x for sv in SLUDGE_ADDITIONAL_INFORMATION.values() for x in sv]
], ],
}, },
"Water-Sediment System Data": { "Water-Sediment System Data": {
"name": "sediment", "name": "sediment",
"widgets": [ "widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sediment_{0}") HTMLGenerator.generate_html(ai, prefix=f"sediment_{0}")
for ai in [x for s in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in s] for ai in [x for sv in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in sv]
], ],
}, },
} }
@ -2368,6 +2421,7 @@ def package_scenarios(request, package_uuid):
scenario_name = request.POST.get("scenario-name") scenario_name = request.POST.get("scenario-name")
scenario_description = request.POST.get("scenario-description") scenario_description = request.POST.get("scenario-description")
scenario_date_year = request.POST.get("scenario-date-year") scenario_date_year = request.POST.get("scenario-date-year")
scenario_date_month = request.POST.get("scenario-date-month") scenario_date_month = request.POST.get("scenario-date-month")
scenario_date_day = request.POST.get("scenario-date-day") scenario_date_day = request.POST.get("scenario-date-day")
@ -2381,9 +2435,9 @@ def package_scenarios(request, package_uuid):
scenario_type = request.POST.get("scenario-type") scenario_type = request.POST.get("scenario-type")
additional_information = HTMLGenerator.build_models(request.POST.dict()) additional_information = HTMLGenerator.build_models(request.POST.dict())
additional_information = [x for s in additional_information.values() for x in s] additional_information = [x for sv in additional_information.values() for x in sv]
s = Scenario.create( new_scen = Scenario.create(
current_package, current_package,
name=scenario_name, name=scenario_name,
description=scenario_description, description=scenario_description,
@ -2392,7 +2446,7 @@ def package_scenarios(request, package_uuid):
additional_information=additional_information, additional_information=additional_information,
) )
return redirect(s.url) return redirect(new_scen.url)
else: else:
return HttpResponseNotAllowed( return HttpResponseNotAllowed(
[ [
@ -2692,6 +2746,7 @@ def settings(request):
name = request.POST.get("prediction-setting-name") name = request.POST.get("prediction-setting-name")
description = request.POST.get("prediction-setting-description") description = request.POST.get("prediction-setting-description")
new_default = request.POST.get("prediction-setting-new-default", "off") == "on" new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
max_nodes = min( max_nodes = min(

View File

@ -27,6 +27,7 @@ dependencies = [
"scikit-learn>=1.6.1", "scikit-learn>=1.6.1",
"sentry-sdk[django]>=2.32.0", "sentry-sdk[django]>=2.32.0",
"setuptools>=80.8.0", "setuptools>=80.8.0",
"nh3==0.3.2",
"polars==1.35.1", "polars==1.35.1",
] ]

View File

@ -1,6 +1,5 @@
{% extends "framework.html" %} {% extends "framework.html" %}
{% load static %} {% load static %}
{% load envipytags %}
{% block content %} {% block content %}
<div class="panel-group" id="reviewListAccordion"> <div class="panel-group" id="reviewListAccordion">

View File

@ -192,7 +192,7 @@
<div class="panel-body list-group-item" id="ReviewedContent"> <div class="panel-body list-group-item" id="ReviewedContent">
{% if object_type == 'package' %} {% if object_type == 'package' %}
{% for obj in reviewed_objects %} {% for obj in reviewed_objects %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }} <a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}
<span class="glyphicon glyphicon-star" aria-hidden="true" <span class="glyphicon glyphicon-star" aria-hidden="true"
style="float:right" data-toggle="tooltip" style="float:right" data-toggle="tooltip"
data-placement="top" title="" data-original-title="Reviewed"> data-placement="top" title="" data-original-title="Reviewed">
@ -201,7 +201,7 @@
{% endfor %} {% endfor %}
{% else %} {% else %}
{% for obj in reviewed_objects|slice:":50" %} {% for obj in reviewed_objects|slice:":50" %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}{# <i>({{ obj.package.name }})</i> #} <a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}{# <i>({{ obj.package.name }})</i> #}
<span class="glyphicon glyphicon-star" aria-hidden="true" <span class="glyphicon glyphicon-star" aria-hidden="true"
style="float:right" data-toggle="tooltip" style="float:right" data-toggle="tooltip"
data-placement="top" title="" data-original-title="Reviewed"> data-placement="top" title="" data-original-title="Reviewed">
@ -221,11 +221,11 @@
<div class="panel-body list-group-item" id="UnreviewedContent"> <div class="panel-body list-group-item" id="UnreviewedContent">
{% if object_type == 'package' %} {% if object_type == 'package' %}
{% for obj in unreviewed_objects %} {% for obj in unreviewed_objects %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a> <a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a>
{% endfor %} {% endfor %}
{% else %} {% else %}
{% for obj in unreviewed_objects|slice:":50" %} {% for obj in unreviewed_objects|slice:":50" %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a> <a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a>
{% endfor %} {% endfor %}
{% endif %} {% endif %}
</div> </div>
@ -236,9 +236,9 @@
<ul class='list-group'> <ul class='list-group'>
{% for obj in objects %} {% for obj in objects %}
{% if object_type == 'user' %} {% if object_type == 'user' %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.username }}</a> <a class="list-group-item" href="{{ obj.url }}">{{ obj.username|safe }}</a>
{% else %} {% else %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a> <a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</ul> </ul>

View File

@ -26,12 +26,12 @@
{% endif %} {% endif %}
<h4 class="panel-title"> <h4 class="panel-title">
<a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail" <a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail"
href="#{{ obj.id }}">{{ obj.name }}</a> href="#{{ obj.id }}">{{ obj.name|safe }}</a>
</h4> </h4>
</div> </div>
<div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}"> <div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
<a class="list-group-item" href="{{ obj.detail_url }}">{{ obj.name }} Migration Detail Page</a> <a class="list-group-item" href="{{ obj.detail_url }}">{{ obj.name|safe }} Migration Detail Page</a>
</div> </div>
</div> </div>
{% endfor %} {% endfor %}

View File

@ -27,7 +27,7 @@
{% endif %} {% endif %}
<h4 class="panel-title"> <h4 class="panel-title">
<a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail" <a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail"
href="#{{ obj.id }}">{{ obj.name }}</a> href="#{{ obj.id }}">{{ obj.name|safe }}</a>
</h4> </h4>
</div> </div>
<div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}"> <div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}">

View File

@ -1,3 +1,4 @@
<div class="modal fade" tabindex="-1" id="new_model_modal" role="dialog" aria-labelledby="new_model_modal" <div class="modal fade" tabindex="-1" id="new_model_modal" role="dialog" aria-labelledby="new_model_modal"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
@ -47,14 +48,14 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
@ -68,14 +69,14 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<div class="modal fade" tabindex="-1" id="new_pathway_modal" role="dialog" aria-labelledby="new_pathway_modal" <div class="modal fade" tabindex="-1" id="new_pathway_modal" role="dialog" aria-labelledby="new_pathway_modal"
aria-hidden="true" style="overflow-y: auto;"> aria-hidden="true" style="overflow-y: auto;">
@ -111,7 +112,7 @@
<select id="settingSelect" name="settingSelect" class="form-control"> <select id="settingSelect" name="settingSelect" class="form-control">
{% for setting in available_settings %} {% for setting in available_settings %}
<option value="{{ setting.id }}">{{ setting.name }}</option> <option value="{{ setting.id }}">{{ setting.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
<p></p> <p></p>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<div id="new_prediction_setting_modal" class="modal" tabindex="-1"> <div id="new_prediction_setting_modal" class="modal" tabindex="-1">
@ -40,14 +41,14 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>
@ -57,7 +58,7 @@
<select id="model-based-prediction-setting-model" name="model-based-prediction-setting-model" class="form-control" data-width='100%'> <select id="model-based-prediction-setting-model" name="model-based-prediction-setting-model" class="form-control" data-width='100%'>
<option disabled selected>Select the model</option> <option disabled selected>Select the model</option>
{% for m in models %} {% for m in models %}
<option value="{{ m.url }}">{{ m.name }}</option> <option value="{{ m.url }}">{{ m.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
<label for="model-based-prediction-setting-threshold">Threshold</label> <label for="model-based-prediction-setting-threshold">Threshold</label>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<div class="modal fade bs-modal-lg" id="add_pathway_edge_modal" tabindex="-1" aria-labelledby="add_pathway_edge_modal" <div class="modal fade bs-modal-lg" id="add_pathway_edge_modal" tabindex="-1" aria-labelledby="add_pathway_edge_modal"
aria-modal="true" aria-modal="true"
@ -36,7 +37,7 @@
<select id="add_pathway_edge_substrates" name="edge-substrates" <select id="add_pathway_edge_substrates" name="edge-substrates"
data-actions-box='true' class="form-control" multiple data-width='100%'> data-actions-box='true' class="form-control" multiple data-width='100%'>
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name }}</option> <option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -47,7 +48,7 @@
<select id="add_pathway_edge_products" name="edge-products" <select id="add_pathway_edge_products" name="edge-products"
data-actions-box='true' class="form-control" multiple data-width='100%'> data-actions-box='true' class="form-control" multiple data-width='100%'>
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name }}</option> <option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Delete Edge --> <!-- Delete Edge -->
<div id="delete_pathway_edge_modal" class="modal" tabindex="-1"> <div id="delete_pathway_edge_modal" class="modal" tabindex="-1">
@ -19,7 +20,7 @@
data-actions-box='true' class="form-control" data-width='100%'> data-actions-box='true' class="form-control" data-width='100%'>
<option value="" disabled selected>Select Reaction to delete</option> <option value="" disabled selected>Select Reaction to delete</option>
{% for e in pathway.edges %} {% for e in pathway.edges %}
<option value="{{ e.url }}">{{ e.edge_label.name }}</option> <option value="{{ e.url }}">{{ e.edge_label.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" id="hidden" name="hidden" value="delete"/> <input type="hidden" id="hidden" name="hidden" value="delete"/>

View File

@ -1,4 +1,5 @@
{% load static %} {% load static %}
<!-- Delete Node --> <!-- Delete Node -->
<div id="delete_pathway_node_modal" class="modal" tabindex="-1"> <div id="delete_pathway_node_modal" class="modal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
@ -19,7 +20,7 @@
data-actions-box='true' class="form-control" data-width='100%'> data-actions-box='true' class="form-control" data-width='100%'>
<option value="" disabled selected>Select Compound to delete</option> <option value="" disabled selected>Select Compound to delete</option>
{% for n in pathway.nodes %} {% for n in pathway.nodes %}
<option value="{{ n.url }}">{{ n.default_node_label.name }}</option> <option value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" id="hidden" name="hidden" value="delete"/> <input type="hidden" id="hidden" name="hidden" value="delete"/>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Compound --> <!-- Edit Compound -->
<div id="edit_compound_modal" class="modal" tabindex="-1"> <div id="edit_compound_modal" class="modal" tabindex="-1">
@ -15,12 +16,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="compound-name">Name</label> <label for="compound-name">Name</label>
<input id="compound-name" class="form-control" name="compound-name" value="{{ compound.name}}"> <input id="compound-name" class="form-control" name="compound-name" value="{{ compound.name|safe}}">
</p> </p>
<p> <p>
<label for="compound-description">Description</label> <label for="compound-description">Description</label>
<input id="compound-description" type="text" class="form-control" <input id="compound-description" type="text" class="form-control"
value="{{ compound.description }}" value="{{ compound.description|safe }}"
name="compound-description"> name="compound-description">
</p> </p>
</form> </form>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Compound --> <!-- Edit Compound -->
<div id="edit_compound_structure_modal" class="modal" tabindex="-1"> <div id="edit_compound_structure_modal" class="modal" tabindex="-1">
@ -15,12 +16,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="compound-structure-name">Name</label> <label for="compound-structure-name">Name</label>
<input id="compound-structure-name" class="form-control" name="compound-structure-name" value="{{ compound_structure.name }}"> <input id="compound-structure-name" class="form-control" name="compound-structure-name" value="{{ compound_structure.name|safe }}">
</p> </p>
<p> <p>
<label for="compound-structure-description">Description</label> <label for="compound-structure-description">Description</label>
<input id="compound-structure-description" type="text" class="form-control" <input id="compound-structure-description" type="text" class="form-control"
value="{{ compound_structure.description }}" name="compound-structure-description"> value="{{ compound_structure.description|safe }}" name="compound-structure-description">
</p> </p>
</form> </form>
</div> </div>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Package Permission --> <!-- Edit Package Permission -->
<div id="edit_group_member_modal" class="modal" tabindex="-1"> <div id="edit_group_member_modal" class="modal" tabindex="-1">
@ -39,7 +40,7 @@
{% endfor %} {% endfor %}
<option disabled>Groups</option> <option disabled>Groups</option>
{% for g in groups %} {% for g in groups %}
<option value="{{ g.url }}">{{ g.name }}</option> <option value="{{ g.url }}">{{ g.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" name="action" value="add"> <input type="hidden" name="action" value="add">
@ -81,7 +82,7 @@
accept-charset="UTF-8" action="" data-remote="true" method="post"> accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %} {% csrf_token %}
<div class="col-xs-8"> <div class="col-xs-8">
{{ g.name }} {{ g.name|safe }}
<input type="hidden" name="member" value="{{ g.url }}"/> <input type="hidden" name="member" value="{{ g.url }}"/>
<input type="hidden" name="action" value="remove"> <input type="hidden" name="action" value="remove">
</div> </div>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Model --> <!-- Edit Model -->
<div id="edit_model_modal" class="modal" tabindex="-1"> <div id="edit_model_modal" class="modal" tabindex="-1">
@ -16,12 +17,12 @@
<p> <p>
<label for="model-name">Name</label> <label for="model-name">Name</label>
<input id="model-name" type="text" class="form-control" name="model-name" <input id="model-name" type="text" class="form-control" name="model-name"
value="{{ model.name }}"> value="{{ model.name|safe }}">
</p> </p>
<p> <p>
<label for="model-description">Description</label> <label for="model-description">Description</label>
<input id="model-description" type="text" class="form-control" name="model-description" <input id="model-description" type="text" class="form-control" name="model-description"
value="{{ model.description }}"> value="{{ model.description|safe }}">
</p> </p>
</form> </form>
</div> </div>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Node --> <!-- Edit Node -->
<div id="edit_node_modal" class="modal" tabindex="-1"> <div id="edit_node_modal" class="modal" tabindex="-1">
@ -15,12 +16,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="node-name">Name</label> <label for="node-name">Name</label>
<input id="node-name" class="form-control" name="node-name" value="{{ node.name}}"> <input id="node-name" class="form-control" name="node-name" value="{{ node.name|safe}}">
</p> </p>
<p> <p>
<label for="node-description">Description</label> <label for="node-description">Description</label>
<input id="node-description" type="text" class="form-control" <input id="node-description" type="text" class="form-control"
value="{{ node.description }}" value="{{ node.description|safe }}"
name="node-description"> name="node-description">
</p> </p>
</form> </form>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Package --> <!-- Edit Package -->
<div id="edit_package_modal" class="modal" tabindex="-1"> <div id="edit_package_modal" class="modal" tabindex="-1">
@ -15,12 +16,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="package-name">Name</label> <label for="package-name">Name</label>
<input id="package-name" class="form-control" name="package-name" value="{{ package.name}}"> <input id="package-name" class="form-control" name="package-name" value="{{ package.name|safe}}">
</p> </p>
<p> <p>
<label for="package-description">Description</label> <label for="package-description">Description</label>
<input id="package-description" type="text" class="form-control" <input id="package-description" type="text" class="form-control"
value="{{ package.description }}" value="{{ package.description|safe }}"
name="package-description"> name="package-description">
</p> </p>
</form> </form>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Package Permission --> <!-- Edit Package Permission -->
<div id="edit_package_permissions_modal" class="modal" tabindex="-1"> <div id="edit_package_permissions_modal" class="modal" tabindex="-1">
@ -46,7 +47,7 @@
{% endfor %} {% endfor %}
<option disabled>Groups</option> <option disabled>Groups</option>
{% for g in groups %} {% for g in groups %}
<option value="{{ g.url }}">{{ g.name }}</option> <option value="{{ g.url }}">{{ g.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>
@ -100,7 +101,7 @@
accept-charset="UTF-8" action="" data-remote="true" method="post"> accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %} {% csrf_token %}
<div class="col-xs-4"> <div class="col-xs-4">
{{ gp.group.name }} {{ gp.group.name|safe }}
<input type="hidden" name="grantee" value="{{ gp.group.url }}"/> <input type="hidden" name="grantee" value="{{ gp.group.url }}"/>
</div> </div>
<div class="col-xs-2"> <div class="col-xs-2">

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Pathway --> <!-- Edit Pathway -->
<div id="edit_pathway_modal" class="modal" tabindex="-1"> <div id="edit_pathway_modal" class="modal" tabindex="-1">
@ -15,12 +16,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="pathway-name">Name</label> <label for="pathway-name">Name</label>
<input id="pathway-name" class="form-control" name="pathway-name" value="{{ pathway.name }}"> <input id="pathway-name" class="form-control" name="pathway-name" value="{{ pathway.name|safe }}">
</p> </p>
<p> <p>
<label for="pathway-description">Description</label> <label for="pathway-description">Description</label>
<textarea id="pathway-description" type="text" class="form-control" name="pathway-description" <textarea id="pathway-description" type="text" class="form-control" name="pathway-description"
rows="10">{{ pathway.description }}</textarea> rows="10">{{ pathway.description|safe }}</textarea>
</p> </p>
</form> </form>
</div> </div>

View File

@ -32,7 +32,7 @@
<td colspan="2"> <td colspan="2">
<select id="model" name="model" class="form-control" data-width='100%'> <select id="model" name="model" class="form-control" data-width='100%'>
{% for m in models %} {% for m in models %}
<option value="{{ m.id }}" {% if user.prediction_settings.model.url == m.url %}selected{% endif %}>{{ m.name }}</option> <option value="{{ m.id }}" {% if user.prediction_settings.model.url == m.url %}selected{% endif %}>{{ m.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
</td> </td>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Reaction --> <!-- Edit Reaction -->
<div id="edit_reaction_modal" class="modal" tabindex="-1"> <div id="edit_reaction_modal" class="modal" tabindex="-1">
@ -14,12 +15,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="reaction-name">Name</label> <label for="reaction-name">Name</label>
<input id="reaction-name" class="form-control" name="reaction-name" value="{{ reaction.name }}"> <input id="reaction-name" class="form-control" name="reaction-name" value="{{ reaction.name|safe }}">
</p> </p>
<p> <p>
<label for="reaction-description">Description</label> <label for="reaction-description">Description</label>
<input id="reaction-description" type="text" class="form-control" <input id="reaction-description" type="text" class="form-control"
value="{{ reaction.description }}" name="reaction-description"> value="{{ reaction.description|safe }}" name="reaction-description">
</p> </p>
</form> </form>
</div> </div>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit Rule --> <!-- Edit Rule -->
<div id="edit_rule_modal" class="modal" tabindex="-1"> <div id="edit_rule_modal" class="modal" tabindex="-1">
@ -14,12 +15,12 @@
{% csrf_token %} {% csrf_token %}
<p> <p>
<label for="rule-name">Name</label> <label for="rule-name">Name</label>
<input id="rule-name" class="form-control" name="rule-name" value="{{ rule.name }}"> <input id="rule-name" class="form-control" name="rule-name" value="{{ rule.name|safe }}">
</p> </p>
<p> <p>
<label for="rule-description">Description</label> <label for="rule-description">Description</label>
<input id="rule-description" type="text" class="form-control" <input id="rule-description" type="text" class="form-control"
value="{{ rule.description }}" name="rule-description"> value="{{ rule.description|safe }}" name="rule-description">
</p> </p>
</form> </form>
</div> </div>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Edit User --> <!-- Edit User -->
<div id="edit_user_modal" class="modal" tabindex="-1"> <div id="edit_user_modal" class="modal" tabindex="-1">
@ -18,7 +19,7 @@
<select id="default-package" name="default-package" class="form-control" data-width='100%'> <select id="default-package" name="default-package" class="form-control" data-width='100%'>
<option disabled>Select a Package</option> <option disabled>Select a Package</option>
{% for p in meta.writeable_packages %} {% for p in meta.writeable_packages %}
<option value="{{ p.url }}" {% if p.id == meta.user.default_package.id %}selected{% endif %}>{{ p.name }}</option> <option value="{{ p.url }}" {% if p.id == meta.user.default_package.id %}selected{% endif %}>{{ p.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
</p> </p>
@ -27,7 +28,7 @@
<select id="default-group" name="default-group" class="form-control" data-width='100%'> <select id="default-group" name="default-group" class="form-control" data-width='100%'>
<option disabled>Select a Group</option> <option disabled>Select a Group</option>
{% for g in meta.available_groups %} {% for g in meta.available_groups %}
<option value="{{ g.url }}" {% if g.id == meta.user.default_group.id %}selected{% endif %}>{{ g.name }}</option> <option value="{{ g.url }}" {% if g.id == meta.user.default_group.id %}selected{% endif %}>{{ g.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
</p> </p>
@ -36,7 +37,7 @@
<select id="default-prediction-setting" name="default-prediction-setting" class="form-control" data-width='100%'> <select id="default-prediction-setting" name="default-prediction-setting" class="form-control" data-width='100%'>
<option disabled>Select a Setting</option> <option disabled>Select a Setting</option>
{% for s in meta.available_settings %} {% for s in meta.available_settings %}
<option value="{{ s.url }}" {% if s.id == meta.user.default_setting.id %}selected{% endif %}>{{ s.name }}</option> <option value="{{ s.url }}" {% if s.id == meta.user.default_setting.id %}selected{% endif %}>{{ s.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
</p> </p>

View File

@ -1,3 +1,4 @@
<div class="modal fade" tabindex="-1" id="evaluate_model_modal" role="dialog" aria-labelledby="evaluate_model_modal" <div class="modal fade" tabindex="-1" id="evaluate_model_modal" role="dialog" aria-labelledby="evaluate_model_modal"
aria-hidden="true"> aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
@ -24,14 +25,14 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if obj.reviewed %} {% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %} {% for obj in meta.readable_packages %}
{% if not obj.reviewed %} {% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</select> </select>

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Copy Object --> <!-- Copy Object -->
<div id="generic_copy_object_modal" class="modal" tabindex="-1"> <div id="generic_copy_object_modal" class="modal" tabindex="-1">
@ -18,7 +19,7 @@
data-width='100%'> data-width='100%'>
<option disabled selected>Select Target Package</option> <option disabled selected>Select Target Package</option>
{% for p in meta.writeable_packages %} {% for p in meta.writeable_packages %}
<option value="{{ p.url }}">{{ p.name }}</option>` <option value="{{ p.url }}">{{ p.name|safe }}</option>`
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" name="hidden" value="copy"> <input type="hidden" name="hidden" value="copy">

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<style> <style>
@ -55,7 +56,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
<h4 class="modal-title">Set Aliases for {{ current_object.name }}</h4> <h4 class="modal-title">Set Aliases for {{ current_object.name|safe }}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form id="set_aliases_modal_form" accept-charset="UTF-8" action="{{ current_object.url }}" <form id="set_aliases_modal_form" accept-charset="UTF-8" action="{{ current_object.url }}"

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<!-- Delete Object --> <!-- Delete Object -->
<div id="generic_set_external_reference_modal" class="modal" tabindex="-1"> <div id="generic_set_external_reference_modal" class="modal" tabindex="-1">
@ -23,7 +24,7 @@
{% if entity == object_type %} {% if entity == object_type %}
{% for db in databases %} {% for db in databases %}
<option id="db-select-{{ db.database.pk }}" data-input-placeholder="{{ db.placeholder }}" <option id="db-select-{{ db.database.pk }}" data-input-placeholder="{{ db.placeholder }}"
value="{{ db.database.id }}">{{ db.database.name }}</option>` value="{{ db.database.id }}">{{ db.database.name|safe }}</option>`
{% endfor %} {% endfor %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}

View File

@ -1,3 +1,4 @@
{% load static %} {% load static %}
<div class="modal fade bs-modal-lg" id="set_scenario_modal" tabindex="-1" aria-labelledby="set_scenario_modal" <div class="modal fade bs-modal-lg" id="set_scenario_modal" tabindex="-1" aria-labelledby="set_scenario_modal"
aria-modal="true" role="dialog"> aria-modal="true" role="dialog">
@ -7,7 +8,7 @@
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
<h4 class="modal-title">Set Scenarios for {{ current_object.name }}</h4> <h4 class="modal-title">Set Scenarios for {{ current_object.name|safe }}</h4>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div id="loading_scenario_div" class="text-center"></div> <div id="loading_scenario_div" class="text-center"></div>

View File

@ -1,3 +1,4 @@
<div class="modal fade" <div class="modal fade"
tabindex="-1" tabindex="-1"
id="manage_api_token_modal" id="manage_api_token_modal"
@ -41,7 +42,7 @@
<div class="input-group"> <div class="input-group">
<input type="hidden" name="hidden" value="delete"> <input type="hidden" name="hidden" value="delete">
<input type="hidden" name="token-id" value="{{ t.pk }}"> <input type="hidden" name="token-id" value="{{ t.pk }}">
<input type="text" class="form-control" value="{{ t.name }}" disabled> <input type="text" class="form-control" value="{{ t.name|safe }}" disabled>
<span class="input-group-btn"> <span class="input-group-btn">
<button type="submit" class="btn btn-danger">Delete</button> <button type="submit" class="btn btn-danger">Delete</button>
</span> </span>

View File

@ -56,7 +56,7 @@
<option disabled>Select a Setting</option> <option disabled>Select a Setting</option>
{% for s in meta.available_settings %} {% for s in meta.available_settings %}
<option value="{{ s.url }}"{% if s.id == meta.user.default_setting.id %}selected{% endif %}> <option value="{{ s.url }}"{% if s.id == meta.user.default_setting.id %}selected{% endif %}>
{{ s.name }}{% if s.id == meta.user.default_setting.id %} <i>(User default)</i>{% endif %} {{ s.name|safe }}{% if s.id == meta.user.default_setting.id %} <i>(User default)</i>{% endif %}
</option> </option>
{% endfor %} {% endfor %}
</select> </select>

View File

@ -13,7 +13,7 @@
<div class="panel-group" id="rule-detail"> <div class="panel-group" id="rule-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ rule.name }} {{ rule.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -60,7 +60,7 @@
<div id="rule-reaction-patterns" class="panel-collapse collapse in"> <div id="rule-reaction-patterns" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in rule.srs %} {% for r in rule.srs %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a>
<div align="center"> <div align="center">
<p> <p>
{{r.as_svg|safe}} {{r.as_svg|safe}}
@ -81,7 +81,7 @@
<div id="rule-scenario" class="panel-collapse collapse in"> <div id="rule-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in rule.scenarios.all %} {% for s in rule.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -15,7 +15,7 @@
<div class="panel-group" id="compound-detail"> <div class="panel-group" id="compound-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ compound.name }} {{ compound.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -64,7 +64,7 @@
</div> </div>
<div id="compound-desc" class="panel-collapse collapse in"> <div id="compound-desc" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{{ compound.description }} {{ compound.description|safe }}
</div> </div>
</div> </div>
@ -133,7 +133,7 @@
<div id="compound-reaction" class="panel-collapse collapse in"> <div id="compound-reaction" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in compound.related_reactions %} {% for r in compound.related_reactions %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a> <a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }} <i>({{ r.package.name|safe }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -150,7 +150,7 @@
<div id="compound-pathway" class="panel-collapse collapse in"> <div id="compound-pathway" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in compound.related_pathways %} {% for r in compound.related_pathways %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a> <a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }} <i>({{ r.package.name|safe }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -167,7 +167,7 @@
<div id="compound-scenario" class="panel-collapse collapse in"> <div id="compound-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in compound.scenarios.all %} {% for s in compound.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -13,7 +13,7 @@
<div class="panel-group" id="compound-structure-detail"> <div class="panel-group" id="compound-structure-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ compound_structure.name }} {{ compound_structure.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -28,7 +28,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> {{ compound_structure.description }} </p> <p> {{ compound_structure.description|safe }} </p>
</div> </div>
<!-- Image --> <!-- Image -->
@ -86,7 +86,7 @@
<div id="compound-structure-scenario" class="panel-collapse collapse in"> <div id="compound-structure-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in compound_structure.scenarios.all %} {% for s in compound_structure.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -12,7 +12,7 @@
<div class="panel-group" id="edge-detail"> <div class="panel-group" id="edge-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ edge.edge_label.name }} {{ edge.edge_label.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -36,7 +36,7 @@
</div> </div>
<div id="edge-desc" class="panel-collapse collapse in"> <div id="edge-desc" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{{ edge.description }} {{ edge.description|safe }}
</div> </div>
</div> </div>
@ -82,12 +82,12 @@
<div id="edge-description-smiles" class="panel-collapse collapse in"> <div id="edge-description-smiles" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for educt in edge.start_nodes.all %} {% for educt in edge.start_nodes.all %}
<a class="btn btn-default" href="{{ educt.url }}">{{ educt.name }}</a> <a class="btn btn-default" href="{{ educt.url }}">{{ educt.name|safe }}</a>
{% endfor %} {% endfor %}
<span class="glyphicon glyphicon-arrow-right" style="margin-left:5em;margin-right:5em;" <span class="glyphicon glyphicon-arrow-right" style="margin-left:5em;margin-right:5em;"
aria-hidden="true"></span> aria-hidden="true"></span>
{% for product in edge.end_nodes.all %} {% for product in edge.end_nodes.all %}
<a class="btn btn-default" href="{{ product.url }}">{{ product.name }}</a> <a class="btn btn-default" href="{{ product.url }}">{{ product.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -116,7 +116,7 @@
<div id="edge-rules" class="panel-collapse collapse in"> <div id="edge-rules" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in edge.edge_label.rules.all %} {% for r in edge.edge_label.rules.all %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -132,7 +132,7 @@
<div id="edge-scenario" class="panel-collapse collapse in"> <div id="edge-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in edge.scenarios.all %} {% for s in edge.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -11,7 +11,7 @@
<div class="panel-group" id="package-detail"> <div class="panel-group" id="package-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ group.name }} {{ group.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -26,7 +26,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> {{ group.description }} </p> <p> {{ group.description|safe }} </p>
</div> </div>
</div> </div>
@ -39,10 +39,10 @@
</div> </div>
<ul class="list-group"> <ul class="list-group">
{% for um in group.user_member.all %} {% for um in group.user_member.all %}
<a class="list-group-item" href="{{ um.url }}">{{ um.username }}</a> <a class="list-group-item" href="{{ um.url }}">{{ um.username|safe }}</a>
{% endfor %} {% endfor %}
{% for gm in group.group_member.all %} {% for gm in group.group_member.all %}
<a class="list-group-item" href="{{ gm.url }}">{{ gm.name }}</a> <a class="list-group-item" href="{{ gm.url }}">{{ gm.name|safe }}</a>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>
@ -56,7 +56,7 @@
</div> </div>
<ul class="list-group"> <ul class="list-group">
{% for p in packages %} {% for p in packages %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a> <a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a>
{% endfor %} {% endfor %}
</ul> </ul>
</div> </div>

View File

@ -1,6 +1,5 @@
{% extends "framework.html" %} {% extends "framework.html" %}
{% load static %} {% load static %}
{% load envipytags %}
{% block content %} {% block content %}
{% block action_modals %} {% block action_modals %}
@ -18,7 +17,7 @@
<div class="panel-group" id="model-detail"> <div class="panel-group" id="model-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ model.name }} {{ model.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -33,7 +32,7 @@
</div> </div>
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> {{ model.description }} </p> <p> {{ model.description|safe }} </p>
</div> </div>
{% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning'%} {% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning'%}
<!-- Rule Packages --> <!-- Rule Packages -->
@ -46,7 +45,7 @@
<div id="rule-package" class="panel-collapse collapse in"> <div id="rule-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for p in model.rule_packages.all %} {% for p in model.rule_packages.all %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a> <a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -60,7 +59,7 @@
<div id="reaction-package" class="panel-collapse collapse in"> <div id="reaction-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for p in model.data_packages.all %} {% for p in model.data_packages.all %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a> <a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -75,7 +74,7 @@
<div id="eval-package" class="panel-collapse collapse in"> <div id="eval-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for p in model.eval_packages.all %} {% for p in model.eval_packages.all %}
<a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a> <a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -12,7 +12,7 @@
<div class="panel-group" id="node-detail"> <div class="panel-group" id="node-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ node.name }} {{ node.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -39,7 +39,7 @@
</div> </div>
<div id="node-desc" class="panel-collapse collapse in"> <div id="node-desc" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{{ node.description }} {{ node.description|safe }}
</div> </div>
</div> </div>
@ -98,7 +98,7 @@
<div id="node-scenario" class="panel-collapse collapse in"> <div id="node-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in node.scenarios.all %} {% for s in node.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -14,7 +14,7 @@
<div class="panel-group" id="package-detail"> <div class="panel-group" id="package-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ package.name }} {{ package.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"

View File

@ -98,7 +98,7 @@
<div class="panel-group" id="pwAccordion"> <div class="panel-group" id="pwAccordion">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ pathway.name }} {{ pathway.name|safe }}
</div> </div>
</div> </div>
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
@ -236,7 +236,7 @@
<div id="pathway-scenario" class="panel-collapse collapse in"> <div id="pathway-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in pathway.scenarios.all %} {% for s in pathway.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -266,7 +266,7 @@
<td colspan="2"> <td colspan="2">
<li class="list-group-item"> <li class="list-group-item">
<a href="{{ pathway.setting.model.url }}"> <a href="{{ pathway.setting.model.url }}">
{{ pathway.setting.model.name }} {{ pathway.setting.model.name|safe }}
</a> </a>
</li> </li>
</td> </td>
@ -299,7 +299,7 @@
{% for p in pathway.setting.rule_packages.all %} {% for p in pathway.setting.rule_packages.all %}
<li class="list-group-item"> <li class="list-group-item">
<a href="{{ p.url }}"> <a href="{{ p.url }}">
{{ p.name }} {{ p.name|safe }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -14,7 +14,7 @@
<div class="panel-group" id="reaction-detail"> <div class="panel-group" id="reaction-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ reaction.name }} {{ reaction.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -38,7 +38,7 @@
</div> </div>
<div id="reaction-desc" class="panel-collapse collapse in"> <div id="reaction-desc" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{{ reaction.description }} {{ reaction.description|safe }}
</div> </div>
</div> </div>
@ -84,12 +84,12 @@
<div id="reaction-description-smiles" class="panel-collapse collapse in"> <div id="reaction-description-smiles" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for educt in reaction.educts.all %} {% for educt in reaction.educts.all %}
<a class="btn btn-default" href="{{ educt.url }}">{{ educt.name }}</a> <a class="btn btn-default" href="{{ educt.url }}">{{ educt.name|safe }}</a>
{% endfor %} {% endfor %}
<span class="glyphicon glyphicon-arrow-right" style="margin-left:5em;margin-right:5em;" <span class="glyphicon glyphicon-arrow-right" style="margin-left:5em;margin-right:5em;"
aria-hidden="true"></span> aria-hidden="true"></span>
{% for product in reaction.products.all %} {% for product in reaction.products.all %}
<a class="btn btn-default" href="{{ product.url }}">{{ product.name }}</a> <a class="btn btn-default" href="{{ product.url }}">{{ product.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -118,7 +118,7 @@
<div id="reaction-rules" class="panel-collapse collapse in"> <div id="reaction-rules" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in reaction.rules.all %} {% for r in reaction.rules.all %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -152,7 +152,7 @@
<div id="reaction-pathway" class="panel-collapse collapse in"> <div id="reaction-pathway" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in reaction.related_pathways %} {% for r in reaction.related_pathways %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -168,7 +168,7 @@
<div id="reaction-scenario" class="panel-collapse collapse in"> <div id="reaction-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in reaction.scenarios.all %} {% for s in reaction.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a> <a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -10,7 +10,7 @@
<div class="panel-group" id="scenario-detail"> <div class="panel-group" id="scenario-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ scenario.name }} {{ scenario.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -30,7 +30,7 @@
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading">Description</div> <div class="panel-heading">Description</div>
<div class="panel-body"> <div class="panel-body">
{{ scenario.description }} {{ scenario.description|safe }}
<br> <br>
{{ scenario.scenario_type }} {{ scenario.scenario_type }}
<br> <br>

View File

@ -13,7 +13,7 @@
<div class="panel-group" id="rule-detail"> <div class="panel-group" id="rule-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ rule.name }} {{ rule.name|safe }}
<div id="actionsButton" <div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;" style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
@ -29,7 +29,7 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> <p>
{{ rule.description }} {{ rule.description|safe }}
</p> </p>
</div> </div>
@ -145,7 +145,7 @@
<div id="rule-composite-rule" class="panel-collapse collapse in"> <div id="rule-composite-rule" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for cr in rule.parallelrule_set.all %} {% for cr in rule.parallelrule_set.all %}
<a class="list-group-item" href="{{ cr.url }}">{{ cr.name }}</a> <a class="list-group-item" href="{{ cr.url }}">{{ cr.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -162,7 +162,7 @@
<div id="rule-scenario" class="panel-collapse collapse in"> <div id="rule-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for s in rule.scenarios.all %} {% for s in rule.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }}</a> <a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -179,7 +179,7 @@
<div id="rule-reaction" class="panel-collapse collapse"> <div id="rule-reaction" class="panel-collapse collapse">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in rule.related_reactions %} {% for r in rule.related_reactions %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>
@ -196,7 +196,7 @@
<div id="rule-pathway" class="panel-collapse collapse"> <div id="rule-pathway" class="panel-collapse collapse">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for r in rule.related_pathways %} {% for r in rule.related_pathways %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }}</a> <a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</a>
{% endfor %} {% endfor %}
</div> </div>
</div> </div>

View File

@ -41,7 +41,7 @@
<div id="default-package" class="panel-collapse collapse in"> <div id="default-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
<li class="list-group-item"> <li class="list-group-item">
<a href="{{ user.default_package.url }}"> {{ user.default_package.name }}</a> <a href="{{ user.default_package.url }}"> {{ user.default_package.name|safe }}</a>
</li> </li>
</div> </div>
</div> </div>
@ -58,7 +58,7 @@
<div class="panel-body list-group-item"> <div class="panel-body list-group-item">
{% for g in meta.available_groups %} {% for g in meta.available_groups %}
<li class="list-group-item"> <li class="list-group-item">
<a href="{{ g.url }}"> {{ g.name }}</a> <a href="{{ g.url }}"> {{ g.name|safe }}</a>
</li> </li>
{% endfor %} {% endfor %}
</div> </div>
@ -90,7 +90,7 @@
<td colspan="2"> <td colspan="2">
<li class="list-group-item"> <li class="list-group-item">
<a href="{{user.default_setting.model.url}}"> <a href="{{user.default_setting.model.url}}">
{{ user.default_setting.model.name }} {{ user.default_setting.model.name|safe }}
</a> </a>
</li> </li>
</td> </td>
@ -123,7 +123,7 @@
{% for p in user.default_setting.rule_packages.all %} {% for p in user.default_setting.rule_packages.all %}
<li class="list-group-item"> <li class="list-group-item">
<a href="{{p.url}}"> <a href="{{p.url}}">
{{ p.name }} {{ p.name|safe }}
</a> </a>
</li> </li>
{% endfor %} {% endfor %}

View File

@ -18,7 +18,7 @@
</head> </head>
<body> <body>
<p></p> <p></p>
{{ pathway.name }} {{ pathway.name|safe }}
<div id="viz"> <div id="viz">
<svg width="2000" height="2000"> <!-- Sehr großes SVG für Zoom --> <svg width="2000" height="2000"> <!-- Sehr großes SVG für Zoom -->
<defs> <defs>

View File

@ -11,13 +11,13 @@
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
{% endif %} {% endif %}
{% for obj in reviewed_objects %} {% for obj in reviewed_objects %}
<option value="{{ obj.url }}" selected>{{ obj.name }}</option> <option value="{{ obj.url }}" selected>{{ obj.name|safe }}</option>
{% endfor %} {% endfor %}
{% if unreviewed_objects %} {% if unreviewed_objects %}
<option disabled>Unreviewed Packages</option> <option disabled>Unreviewed Packages</option>
{% endif %} {% endif %}
{% for obj in unreviewed_objects %} {% for obj in unreviewed_objects %}
<option value="{{ obj.url }}">{{ obj.name }}</option> <option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endfor %} {% endfor %}
</select> </select>
</div> </div>

View File

@ -29,7 +29,7 @@ class RuleTest(TestCase):
self.assertEqual(r.name, "bt0022-2833") self.assertEqual(r.name, "bt0022-2833")
self.assertEqual( self.assertEqual(
r.description, r.description,
"Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative", "Dihalomethyl derivative + Halomethyl derivative &gt; 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative",
) )
def test_smirks_are_trimmed(self): def test_smirks_are_trimmed(self):

View File

@ -255,6 +255,30 @@ class FormatConverter(object):
except Exception: except Exception:
return False return False
@staticmethod
def is_valid_smarts(smarts: str) -> bool:
"""
Checks whether a given string is a valid SMARTS pattern.
Parameters
----------
smarts : str
The SMARTS string to validate.
Returns
-------
bool
True if the SMARTS string is valid, False otherwise.
"""
if not isinstance(smarts, str) or not smarts.strip():
return False
try:
mol = Chem.MolFromSmarts(smarts)
return mol is not None
except Exception:
return False
@staticmethod @staticmethod
def apply( def apply(
smiles: str, smiles: str,

41
uv.lock generated
View File

@ -571,6 +571,7 @@ dependencies = [
{ name = "epam-indigo" }, { name = "epam-indigo" },
{ name = "gunicorn" }, { name = "gunicorn" },
{ name = "networkx" }, { name = "networkx" },
{ name = "nh3" },
{ name = "polars" }, { name = "polars" },
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "python-dotenv" }, { name = "python-dotenv" },
@ -613,6 +614,7 @@ requires-dist = [
{ name = "gunicorn", specifier = ">=23.0.0" }, { name = "gunicorn", specifier = ">=23.0.0" },
{ name = "msal", marker = "extra == 'ms-login'", specifier = ">=1.33.0" }, { name = "msal", marker = "extra == 'ms-login'", specifier = ">=1.33.0" },
{ name = "networkx", specifier = ">=3.4.2" }, { name = "networkx", specifier = ">=3.4.2" },
{ name = "nh3", specifier = "==0.3.2" },
{ name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.37.0" }, { name = "poethepoet", marker = "extra == 'dev'", specifier = ">=0.37.0" },
{ name = "polars", specifier = "==1.35.1" }, { name = "polars", specifier = "==1.35.1" },
{ name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.3.0" },
@ -1147,6 +1149,39 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" },
] ]
[[package]]
name = "nh3"
version = "0.3.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ca/a5/34c26015d3a434409f4d2a1cd8821a06c05238703f49283ffeb937bef093/nh3-0.3.2.tar.gz", hash = "sha256:f394759a06df8b685a4ebfb1874fb67a9cbfd58c64fc5ed587a663c0e63ec376", size = 19288, upload-time = "2025-10-30T11:17:45.948Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5b/01/a1eda067c0ba823e5e2bb033864ae4854549e49fb6f3407d2da949106bfb/nh3-0.3.2-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:d18957a90806d943d141cc5e4a0fefa1d77cf0d7a156878bf9a66eed52c9cc7d", size = 1419839, upload-time = "2025-10-30T11:17:09.956Z" },
{ url = "https://files.pythonhosted.org/packages/30/57/07826ff65d59e7e9cc789ef1dc405f660cabd7458a1864ab58aefa17411b/nh3-0.3.2-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45c953e57028c31d473d6b648552d9cab1efe20a42ad139d78e11d8f42a36130", size = 791183, upload-time = "2025-10-30T11:17:11.99Z" },
{ url = "https://files.pythonhosted.org/packages/af/2f/e8a86f861ad83f3bb5455f596d5c802e34fcdb8c53a489083a70fd301333/nh3-0.3.2-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2c9850041b77a9147d6bbd6dbbf13eeec7009eb60b44e83f07fcb2910075bf9b", size = 829127, upload-time = "2025-10-30T11:17:13.192Z" },
{ url = "https://files.pythonhosted.org/packages/d8/97/77aef4daf0479754e8e90c7f8f48f3b7b8725a3b8c0df45f2258017a6895/nh3-0.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:403c11563e50b915d0efdb622866d1d9e4506bce590ef7da57789bf71dd148b5", size = 997131, upload-time = "2025-10-30T11:17:14.677Z" },
{ url = "https://files.pythonhosted.org/packages/41/ee/fd8140e4df9d52143e89951dd0d797f5546004c6043285289fbbe3112293/nh3-0.3.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:0dca4365db62b2d71ff1620ee4f800c4729849906c5dd504ee1a7b2389558e31", size = 1068783, upload-time = "2025-10-30T11:17:15.861Z" },
{ url = "https://files.pythonhosted.org/packages/87/64/bdd9631779e2d588b08391f7555828f352e7f6427889daf2fa424bfc90c9/nh3-0.3.2-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:0fe7ee035dd7b2290715baf29cb27167dddd2ff70ea7d052c958dbd80d323c99", size = 994732, upload-time = "2025-10-30T11:17:17.155Z" },
{ url = "https://files.pythonhosted.org/packages/79/66/90190033654f1f28ca98e3d76b8be1194505583f9426b0dcde782a3970a2/nh3-0.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a40202fd58e49129764f025bbaae77028e420f1d5b3c8e6f6fd3a6490d513868", size = 975997, upload-time = "2025-10-30T11:17:18.77Z" },
{ url = "https://files.pythonhosted.org/packages/34/30/ebf8e2e8d71fdb5a5d5d8836207177aed1682df819cbde7f42f16898946c/nh3-0.3.2-cp314-cp314t-win32.whl", hash = "sha256:1f9ba555a797dbdcd844b89523f29cdc90973d8bd2e836ea6b962cf567cadd93", size = 583364, upload-time = "2025-10-30T11:17:20.286Z" },
{ url = "https://files.pythonhosted.org/packages/94/ae/95c52b5a75da429f11ca8902c2128f64daafdc77758d370e4cc310ecda55/nh3-0.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:dce4248edc427c9b79261f3e6e2b3ecbdd9b88c267012168b4a7b3fc6fd41d13", size = 589982, upload-time = "2025-10-30T11:17:21.384Z" },
{ url = "https://files.pythonhosted.org/packages/b4/bd/c7d862a4381b95f2469704de32c0ad419def0f4a84b7a138a79532238114/nh3-0.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:019ecbd007536b67fdf76fab411b648fb64e2257ca3262ec80c3425c24028c80", size = 577126, upload-time = "2025-10-30T11:17:22.755Z" },
{ url = "https://files.pythonhosted.org/packages/b6/3e/f5a5cc2885c24be13e9b937441bd16a012ac34a657fe05e58927e8af8b7a/nh3-0.3.2-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7064ccf5ace75825bd7bf57859daaaf16ed28660c1c6b306b649a9eda4b54b1e", size = 1431980, upload-time = "2025-10-30T11:17:25.457Z" },
{ url = "https://files.pythonhosted.org/packages/7f/f7/529a99324d7ef055de88b690858f4189379708abae92ace799365a797b7f/nh3-0.3.2-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8745454cdd28bbbc90861b80a0111a195b0e3961b9fa2e672be89eb199fa5d8", size = 820805, upload-time = "2025-10-30T11:17:26.98Z" },
{ url = "https://files.pythonhosted.org/packages/3d/62/19b7c50ccd1fa7d0764822d2cea8f2a320f2fd77474c7a1805cb22cf69b0/nh3-0.3.2-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72d67c25a84579f4a432c065e8b4274e53b7cf1df8f792cf846abfe2c3090866", size = 803527, upload-time = "2025-10-30T11:17:28.284Z" },
{ url = "https://files.pythonhosted.org/packages/4a/ca/f022273bab5440abff6302731a49410c5ef66b1a9502ba3fbb2df998d9ff/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:13398e676a14d6233f372c75f52d5ae74f98210172991f7a3142a736bd92b131", size = 1051674, upload-time = "2025-10-30T11:17:29.909Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f7/5728e3b32a11daf5bd21cf71d91c463f74305938bc3eb9e0ac1ce141646e/nh3-0.3.2-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03d617e5c8aa7331bd2659c654e021caf9bba704b109e7b2b28b039a00949fe5", size = 1004737, upload-time = "2025-10-30T11:17:31.205Z" },
{ url = "https://files.pythonhosted.org/packages/53/7f/f17e0dba0a99cee29e6cee6d4d52340ef9cb1f8a06946d3a01eb7ec2fb01/nh3-0.3.2-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2f55c4d2d5a207e74eefe4d828067bbb01300e06e2a7436142f915c5928de07", size = 911745, upload-time = "2025-10-30T11:17:32.945Z" },
{ url = "https://files.pythonhosted.org/packages/42/0f/c76bf3dba22c73c38e9b1113b017cf163f7696f50e003404ec5ecdb1e8a6/nh3-0.3.2-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb18403f02b655a1bbe4e3a4696c2ae1d6ae8f5991f7cacb684b1ae27e6c9f7", size = 797184, upload-time = "2025-10-30T11:17:34.226Z" },
{ url = "https://files.pythonhosted.org/packages/08/a1/73d8250f888fb0ddf1b119b139c382f8903d8bb0c5bd1f64afc7e38dad1d/nh3-0.3.2-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d66f41672eb4060cf87c037f760bdbc6847852ca9ef8e9c5a5da18f090abf87", size = 838556, upload-time = "2025-10-30T11:17:35.875Z" },
{ url = "https://files.pythonhosted.org/packages/d1/09/deb57f1fb656a7a5192497f4a287b0ade5a2ff6b5d5de4736d13ef6d2c1f/nh3-0.3.2-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f97f8b25cb2681d25e2338148159447e4d689aafdccfcf19e61ff7db3905768a", size = 1006695, upload-time = "2025-10-30T11:17:37.071Z" },
{ url = "https://files.pythonhosted.org/packages/b6/61/8f4d41c4ccdac30e4b1a4fa7be4b0f9914d8314a5058472f84c8e101a418/nh3-0.3.2-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:2ab70e8c6c7d2ce953d2a58102eefa90c2d0a5ed7aa40c7e29a487bc5e613131", size = 1075471, upload-time = "2025-10-30T11:17:38.225Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c6/966aec0cb4705e69f6c3580422c239205d5d4d0e50fac380b21e87b6cf1b/nh3-0.3.2-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:1710f3901cd6440ca92494ba2eb6dc260f829fa8d9196b659fa10de825610ce0", size = 1002439, upload-time = "2025-10-30T11:17:39.553Z" },
{ url = "https://files.pythonhosted.org/packages/e2/c8/97a2d5f7a314cce2c5c49f30c6f161b7f3617960ade4bfc2fd1ee092cb20/nh3-0.3.2-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:91e9b001101fb4500a2aafe3e7c92928d85242d38bf5ac0aba0b7480da0a4cd6", size = 987439, upload-time = "2025-10-30T11:17:40.81Z" },
{ url = "https://files.pythonhosted.org/packages/0d/95/2d6fc6461687d7a171f087995247dec33e8749a562bfadd85fb5dbf37a11/nh3-0.3.2-cp38-abi3-win32.whl", hash = "sha256:169db03df90da63286e0560ea0efa9b6f3b59844a9735514a1d47e6bb2c8c61b", size = 589826, upload-time = "2025-10-30T11:17:42.239Z" },
{ url = "https://files.pythonhosted.org/packages/64/9a/1a1c154f10a575d20dd634e5697805e589bbdb7673a0ad00e8da90044ba7/nh3-0.3.2-cp38-abi3-win_amd64.whl", hash = "sha256:562da3dca7a17f9077593214a9781a94b8d76de4f158f8c895e62f09573945fe", size = 596406, upload-time = "2025-10-30T11:17:43.773Z" },
{ url = "https://files.pythonhosted.org/packages/9e/7e/a96255f63b7aef032cbee8fc4d6e37def72e3aaedc1f72759235e8f13cb1/nh3-0.3.2-cp38-abi3-win_arm64.whl", hash = "sha256:cf5964d54edd405e68583114a7cba929468bcd7db5e676ae38ee954de1cfc104", size = 584162, upload-time = "2025-10-30T11:17:44.96Z" },
]
[[package]] [[package]]
name = "nodeenv" name = "nodeenv"
version = "1.9.1" version = "1.9.1"
@ -1854,11 +1889,11 @@ wheels = [
[[package]] [[package]]
name = "redis" name = "redis"
version = "7.0.1" version = "6.4.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/57/8f/f125feec0b958e8d22c8f0b492b30b1991d9499a4315dfde466cf4289edc/redis-7.0.1.tar.gz", hash = "sha256:c949df947dca995dc68fdf5a7863950bf6df24f8d6022394585acc98e81624f1", size = 4755322, upload-time = "2025-10-27T14:34:00.33Z" } sdist = { url = "https://files.pythonhosted.org/packages/0d/d6/e8b92798a5bd67d659d51a18170e91c16ac3b59738d91894651ee255ed49/redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010", size = 4647399, upload-time = "2025-08-07T08:10:11.441Z" }
wheels = [ wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/97/9f22a33c475cda519f20aba6babb340fb2f2254a02fb947816960d1e669a/redis-7.0.1-py3-none-any.whl", hash = "sha256:4977af3c7d67f8f0eb8b6fec0dafc9605db9343142f634041fb0235f67c0588a", size = 339938, upload-time = "2025-10-27T14:33:58.553Z" }, { url = "https://files.pythonhosted.org/packages/e8/02/89e2ed7e85db6c93dfa9e8f691c5087df4e3551ab39081a4d7c6d1f90e05/redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f", size = 279847, upload-time = "2025-08-07T08:10:09.84Z" },
] ]
[[package]] [[package]]