2 Commits

Author SHA1 Message Date
62e890245e ... 2025-10-29 20:00:41 +01:00
138846d84d ... 2025-10-29 19:46:20 +01:00
130 changed files with 2154 additions and 5638 deletions

View File

@ -16,5 +16,3 @@ POSTGRES_PORT=
# MAIL
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
# MATOMO
MATOMO_SITE_ID

View File

@ -1,116 +0,0 @@
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

5
.gitignore vendored
View File

@ -10,8 +10,3 @@ scratches/
data/
.DS_Store
node_modules/
static/css/output.css
*.code-workspace

View File

@ -8,7 +8,6 @@ repos:
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
exclude: ^static/images/
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.3
@ -21,15 +20,6 @@ repos:
- id: ruff-format
types_or: [python, pyi]
- repo: local
hooks:
- id: prettier-jinja-templates
name: Format Jinja templates with Prettier
entry: pnpm exec prettier --plugin=prettier-plugin-jinja-template --parser=jinja-template --write
language: system
types: [file]
files: ^templates/.*\.html$
# - repo: local
# hooks:
# - id: django-check

View File

@ -1,11 +0,0 @@
{
"plugins": ["prettier-plugin-jinja-template", "prettier-plugin-tailwindcss"],
"overrides": [
{
"files": "templates/**/*.html",
"options": {
"parser": "jinja-template"
}
}
]
}

View File

@ -7,13 +7,11 @@ These instructions will guide you through setting up the project for local devel
### Prerequisites
- Python 3.11 or later
- [uv](https://github.com/astral-sh/uv) - Python package manager
- **Docker and Docker Compose** - Required for running PostgreSQL database
- [uv](https://github.com/astral-sh/uv) - A fast Python package installer and resolver.
- **Docker and Docker Compose** - Required for running the PostgreSQL database.
- Git
- Make
> **Note:** This application requires PostgreSQL (uses `ArrayField`). Docker is the easiest way to run PostgreSQL locally.
> **Note:** This application requires PostgreSQL, which uses `ArrayField`. Docker is the recommended way to run PostgreSQL locally.
### 1. Install Dependencies
@ -25,12 +23,7 @@ Then, sync the project dependencies. This will create a virtual environment in `
uv sync --dev
```
Note on RDkit installation: if you have rdkit installed on your system globally with a different version of python, the installation will try to link against that and subsequent calls fail. Only option remove global rdkit and rerun sync.
---
The frontend requires `pnpm` to correctly display in development.
[Install it here](https://pnpm.io/installation).
> **Note on RDkit:** If you have a different version of rdkit installed globally, the dependency installation may fail. If this happens, please uninstall the global version and run `uv sync` again.
### 2. Set Up Environment File
@ -51,7 +44,6 @@ uv run poe setup
```
This single command will:
1. Start the PostgreSQL database using Docker Compose.
2. Run database migrations.
3. Bootstrap initial data (anonymous user, default packages, models).
@ -62,12 +54,9 @@ After setup, start the development server:
uv run poe dev
```
This will start the css-watcher as well as the django-development server,
The application will be available at `http://localhost:8000`.
**Note:** The development server automatically starts a CSS watcher (`pnpm run dev`) alongside the Django server to rebuild CSS files when changes are detected. This ensures your styles are always up-to-date during development.
#### Other useful Poe commands
#### Other useful Poe commands:
You can list all available commands by running `uv run poe --help`.
@ -77,7 +66,6 @@ uv run poe db-down # Stop PostgreSQL
uv run poe migrate # Run migrations only
uv run poe bootstrap # Bootstrap data only
uv run poe shell # Open the Django shell
uv run poe build # Build frontend assets and collect static files
uv run poe clean # Remove database volumes (WARNING: destroys all data)
```

View File

@ -52,6 +52,28 @@ INSTALLED_APPS = [
"migration",
]
# Add the TENANT providing implementations for
# Required
# - Package
# - Compound (TODO)
# - CompoundStructure (TODO)
# Optional
# - PackageManager (TODO)
# - GroupManager (TODO)
# - SettingManager (TODO)
TENANT = os.environ.get("TENANT", "public")
INSTALLED_APPS.append(TENANT)
PACKAGE_IMPLEMENTATION = f"{TENANT}.Package"
PACKAGE_MODULE_PATH = f"{TENANT}.models.Package"
def GET_PACKAGE_MODEL():
from django.apps import apps
return apps.get_model(TENANT, "Package")
AUTHENTICATION_BACKENDS = [
"django.contrib.auth.backends.ModelBackend",
]
@ -92,8 +114,6 @@ TEMPLATES = [
},
]
ALLOWED_HTML_TAGS = {"b", "i", "u", "br", "em", "mark", "p", "s", "strong"}
WSGI_APPLICATION = "envipath.wsgi.application"
# Database
@ -245,7 +265,6 @@ LOGGING = {
ENVIFORMER_PRESENT = os.environ.get("ENVIFORMER_PRESENT", "False") == "True"
ENVIFORMER_DEVICE = os.environ.get("ENVIFORMER_DEVICE", "cpu")
# If celery is not present set always eager to true which will cause delayed tasks to block until finished
FLAG_CELERY_PRESENT = os.environ.get("FLAG_CELERY_PRESENT", "False") == "True"
if not FLAG_CELERY_PRESENT:
@ -346,14 +365,6 @@ LOGIN_EXEMPT_URLS = [
"/password_reset/",
"/reset/",
"/microsoft/",
"/terms",
"/privacy",
"/cookie-policy",
"/about",
"/contact",
"/jobs",
"/cite",
"/legal",
]
# MS AD/Entra
@ -368,6 +379,3 @@ if MS_ENTRA_ENABLED:
MS_ENTRA_AUTHORITY = f"https://login.microsoftonline.com/{MS_ENTRA_TENANT_ID}"
MS_ENTRA_REDIRECT_URI = os.environ["MS_REDIRECT_URI"]
MS_ENTRA_SCOPES = os.environ.get("MS_SCOPES", "").split(",")
# Site ID 10 -> beta.envipath.org
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")

View File

@ -1,11 +1,11 @@
from django.contrib import admin
from django.conf import settings as s
from .models import (
User,
UserPackagePermission,
Group,
GroupPackagePermission,
Package,
MLRelativeReasoning,
EnviFormer,
Compound,
@ -24,6 +24,9 @@ from .models import (
)
Package = s.GET_PACKAGE_MODEL()
class UserAdmin(admin.ModelAdmin):
list_display = ["username", "email", "is_active"]

View File

@ -1,5 +1,6 @@
from typing import List, Dict, Optional, Any
from django.conf import settings as s
from django.contrib.auth import get_user_model
from django.http import HttpResponse
from django.shortcuts import redirect
@ -10,7 +11,6 @@ from .logic import PackageManager, UserManager, SettingManager
from .models import (
Compound,
CompoundStructure,
Package,
User,
UserPackagePermission,
Rule,
@ -23,6 +23,9 @@ from .models import (
)
Package = s.GET_PACKAGE_MODEL()
def _anonymous_or_real(request):
if request.user.is_authenticated and not request.user.is_anonymous:
return request.user

View File

@ -4,7 +4,6 @@ import json
from typing import Union, List, Optional, Set, Dict, Any
from uuid import UUID
import nh3
from django.contrib.auth import get_user_model
from django.db import transaction
from django.conf import settings as s
@ -12,7 +11,6 @@ from pydantic import ValidationError
from epdb.models import (
User,
Package,
UserPackagePermission,
GroupPackagePermission,
Permission,
@ -34,6 +32,8 @@ from utilities.misc import PackageImporter, PackageExporter
logger = logging.getLogger(__name__)
Package = s.GET_PACKAGE_MODEL()
class EPDBURLParser:
UUID_PATTERN = r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
@ -186,12 +186,6 @@ class UserManager(object):
def create_user(
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
from .tasks import send_registration_mail
@ -269,9 +263,8 @@ class GroupManager(object):
@staticmethod
def create_group(current_user, name, description):
g = Group()
# Clean for potential XSS
g.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
g.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
g.name = name
g.description = description
g.owner = current_user
g.save()
@ -526,13 +519,8 @@ class PackageManager(object):
@transaction.atomic
def create_package(current_user, name: str, description: str = None):
p = Package()
# 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.name = name
p.description = description
p.save()
up = UserPackagePermission()
@ -1107,29 +1095,28 @@ class SettingManager(object):
model: EPModel = None,
model_threshold: float = None,
):
new_s = Setting()
# Clean for potential XSS
new_s.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
new_s.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
new_s.max_nodes = max_nodes
new_s.max_depth = max_depth
new_s.model = model
new_s.model_threshold = model_threshold
s = Setting()
s.name = name
s.description = description
s.max_nodes = max_nodes
s.max_depth = max_depth
s.model = model
s.model_threshold = model_threshold
new_s.save()
s.save()
if rule_packages is not None:
for r in rule_packages:
new_s.rule_packages.add(r)
new_s.save()
s.rule_packages.add(r)
s.save()
usp = UserSettingPermission()
usp.user = user
usp.setting = new_s
usp.setting = s
usp.permission = Permission.ALL[0]
usp.save()
return new_s
return s
@staticmethod
def get_default_setting(user: User):
@ -1558,7 +1545,7 @@ class SPathway(object):
if self.prediction_setting.model.app_domain:
app_domain_assessment = self.prediction_setting.model.app_domain.assess(
sub.smiles
)
)[0]
if self.persist is not None:
n = self.snode_persist_lookup[sub]
@ -1591,7 +1578,9 @@ class SPathway(object):
if self.prediction_setting.model:
if self.prediction_setting.model.app_domain:
app_domain_assessment = (
self.prediction_setting.model.app_domain.assess(c)
self.prediction_setting.model.app_domain.assess(c)[
0
]
)
self.smiles_to_node[c] = SNode(

View File

@ -2,7 +2,9 @@ from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import MLRelativeReasoning, EnviFormer, Package
from epdb.models import EnviFormer, MLRelativeReasoning
Package = s.GET_PACKAGE_MODEL()
class Command(BaseCommand):
@ -75,11 +77,13 @@ class Command(BaseCommand):
return packages
# Iteratively create models in options["model_names"]
print(f"Creating models: {options['model_names']}\n"
print(
f"Creating models: {options['model_names']}\n"
f"Data packages: {options['data_packages']}\n"
f"Rule Packages (only for MLRR): {options['rule_packages']}\n"
f"Eval Packages: {options['eval_packages']}\n"
f"Threshold: {options['threshold']:.2f}")
f"Threshold: {options['threshold']:.2f}"
)
data_packages = decode_packages(options["data_packages"])
eval_packages = decode_packages(options["eval_packages"])
rule_packages = decode_packages(options["rule_packages"])
@ -90,7 +94,7 @@ class Command(BaseCommand):
pack,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=options['threshold'],
threshold=options["threshold"],
name=f"EnviFormer - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description=f"EnviFormer transformer trained on {options['data_packages']} "
f"evaluated on {options['eval_packages']}.",
@ -101,7 +105,7 @@ class Command(BaseCommand):
rule_packages=rule_packages,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=options['threshold'],
threshold=options["threshold"],
name=f"ECC - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
description=f"ML Relative Reasoning trained on {options['data_packages']} with rules from "
f"{options['rule_packages']} and evaluated on {options['eval_packages']}.",

View File

@ -8,7 +8,9 @@ from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import EnviFormer, Package
from epdb.models import EnviFormer
Package = s.GET_PACKAGE_MODEL()
class Command(BaseCommand):

View File

@ -1,8 +1,8 @@
from django.apps import apps
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db.models import F, Value, TextField, JSONField
from django.db.models.functions import Replace, Cast
from django.db.models import F, JSONField, TextField, Value
from django.db.models.functions import Cast, Replace
from epdb.models import EnviPathModel
@ -23,10 +23,13 @@ class Command(BaseCommand):
)
def handle(self, *args, **options):
Package = s.GET_PACKAGE_MODEL()
print("Localizing urls for Package")
Package.objects.update(url=Replace(F("url"), Value(options["old"]), Value(options["new"])))
MODELS = [
"User",
"Group",
"Package",
"Compound",
"CompoundStructure",
"Pathway",

View File

@ -0,0 +1,190 @@
# Generated by Django 5.2.7 on 2025-10-29 13:32
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0009_joblog"),
("public", "0001_initial"),
]
operations = [
migrations.AlterField(
model_name="userpackagepermission",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Permission on",
),
),
migrations.AlterField(
model_name="grouppackagepermission",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Permission on",
),
),
migrations.AlterField(
model_name="epmodel",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="rule",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="compound",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="scenario",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="pathway",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="reaction",
name="package",
field=models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="public.package",
verbose_name="Package",
),
),
migrations.AlterField(
model_name="user",
name="default_package",
field=models.ForeignKey(
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="public.package",
verbose_name="Default Package",
),
),
migrations.AlterField(
model_name="enviformer",
name="data_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_data_packages",
to="public.package",
verbose_name="Data Packages",
),
),
migrations.AlterField(
model_name="enviformer",
name="eval_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_eval_packages",
to="public.package",
verbose_name="Evaluation Packages",
),
),
migrations.AlterField(
model_name="enviformer",
name="rule_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_rule_packages",
to="public.package",
verbose_name="Rule Packages",
),
),
migrations.AlterField(
model_name="mlrelativereasoning",
name="data_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_data_packages",
to="public.package",
verbose_name="Data Packages",
),
),
migrations.AlterField(
model_name="mlrelativereasoning",
name="eval_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_eval_packages",
to="public.package",
verbose_name="Evaluation Packages",
),
),
migrations.AlterField(
model_name="mlrelativereasoning",
name="rule_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_rule_packages",
to="public.package",
verbose_name="Rule Packages",
),
),
migrations.AlterField(
model_name="rulebasedrelativereasoning",
name="data_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_data_packages",
to="public.package",
verbose_name="Data Packages",
),
),
migrations.AlterField(
model_name="rulebasedrelativereasoning",
name="eval_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_eval_packages",
to="public.package",
verbose_name="Evaluation Packages",
),
),
migrations.AlterField(
model_name="rulebasedrelativereasoning",
name="rule_packages",
field=models.ManyToManyField(
related_name="%(app_label)s_%(class)s_rule_packages",
to="public.package",
verbose_name="Rule Packages",
),
),
migrations.AlterField(
model_name="setting",
name="rule_packages",
field=models.ManyToManyField(
blank=True,
related_name="setting_rule_packages",
to="public.package",
verbose_name="Setting Rule Packages",
),
),
migrations.DeleteModel(
name="Package",
),
]

View File

@ -7,11 +7,10 @@ import secrets
from abc import abstractmethod
from collections import defaultdict
from datetime import datetime
from typing import Union, List, Optional, Dict, Tuple, Set, Any
from typing import Union, List, Optional, Dict, Tuple, Set, Any, TYPE_CHECKING
from uuid import uuid4
import math
import joblib
import nh3
import numpy as np
from django.conf import settings as s
from django.contrib.auth.models import AbstractUser
@ -29,17 +28,12 @@ from sklearn.metrics import precision_score, recall_score, jaccard_score
from sklearn.model_selection import ShuffleSplit
from utilities.chem import FormatConverter, ProductSet, PredictionResult, IndigoUtils
from utilities.ml import (
RuleBasedDataset,
ApplicabilityDomainPCA,
EnsembleClassifierChain,
RelativeReasoning,
EnviFormerDataset,
Dataset,
)
from utilities.ml import Dataset, ApplicabilityDomainPCA, EnsembleClassifierChain, RelativeReasoning
logger = logging.getLogger(__name__)
if TYPE_CHECKING:
Package = s.GET_PACKAGE_MODEL()
##########################
# User/Groups/Permission #
@ -53,7 +47,10 @@ class User(AbstractUser):
)
url = models.TextField(blank=False, null=True, verbose_name="URL", unique=True)
default_package = models.ForeignKey(
"epdb.Package", verbose_name="Default Package", null=True, on_delete=models.SET_NULL
s.PACKAGE_IMPLEMENTATION,
verbose_name="Default Package",
null=True,
on_delete=models.SET_NULL,
)
default_group = models.ForeignKey(
"Group",
@ -243,7 +240,7 @@ class UserPackagePermission(Permission):
)
user = models.ForeignKey("User", verbose_name="Permission to", on_delete=models.CASCADE)
package = models.ForeignKey(
"epdb.Package", verbose_name="Permission on", on_delete=models.CASCADE
s.PACKAGE_IMPLEMENTATION, verbose_name="Permission on", on_delete=models.CASCADE
)
class Meta:
@ -259,7 +256,7 @@ class GroupPackagePermission(Permission):
)
group = models.ForeignKey("Group", verbose_name="Permission to", on_delete=models.CASCADE)
package = models.ForeignKey(
"epdb.Package", verbose_name="Permission on", on_delete=models.CASCADE
s.PACKAGE_IMPLEMENTATION, verbose_name="Permission on", on_delete=models.CASCADE
)
class Meta:
@ -659,7 +656,7 @@ class License(models.Model):
image_link = models.URLField(blank=False, null=False, verbose_name="Image link")
class Package(EnviPathModel):
class AbstractPackage(EnviPathModel):
reviewed = models.BooleanField(verbose_name="Reviewstatus", default=False)
license = models.ForeignKey(
"epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License"
@ -727,10 +724,13 @@ class Package(EnviPathModel):
rules = sorted(rules, key=lambda x: x.url)
return rules
class Meta:
abstract = True
class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin):
package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
s.PACKAGE_IMPLEMENTATION, verbose_name="Package", on_delete=models.CASCADE, db_index=True
)
default_structure = models.ForeignKey(
"CompoundStructure",
@ -780,7 +780,7 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
@staticmethod
@transaction.atomic
def create(
package: Package, smiles: str, name: str = None, description: str = None, *args, **kwargs
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
) -> "Compound":
if smiles is None or smiles.strip() == "":
raise ValueError("SMILES is required")
@ -810,16 +810,14 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
c = Compound()
c.package = package
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 == "":
if name is None or name.strip() == "":
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
c.name = name
# We have a default here only set the value if it carries some payload
if description is not None and description.strip() != "":
c.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
c.description = description.strip()
c.save()
@ -991,11 +989,11 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
raise ValueError("Unpersisted Compound! Persist compound first!")
cs = CompoundStructure()
# Clean for potential XSS
if name is not None:
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
cs.name = name
if description is not None:
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
cs.description = description
cs.smiles = smiles
cs.compound = compound
@ -1060,7 +1058,7 @@ class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
s.PACKAGE_IMPLEMENTATION, verbose_name="Package", on_delete=models.CASCADE, db_index=True
)
# # https://github.com/django-polymorphic/django-polymorphic/issues/229
@ -1166,7 +1164,7 @@ class SimpleAmbitRule(SimpleRule):
@staticmethod
@transaction.atomic
def create(
package: Package,
package: "Package",
name: str = None,
description: str = None,
smirks: str = None,
@ -1197,29 +1195,21 @@ class SimpleAmbitRule(SimpleRule):
r = SimpleAmbitRule()
r.package = package
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
if name is None or name.strip() == "":
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
r.name = name
if description is not None and description.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r.description = description
r.smirks = smirks
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
if not FormatConverter.is_valid_smarts(reactant_filter_smarts.strip()):
raise ValueError(f'Reactant Filter SMARTS "{reactant_filter_smarts}" is invalid!')
else:
r.reactant_filter_smarts = reactant_filter_smarts.strip()
r.reactant_filter_smarts = reactant_filter_smarts
if product_filter_smarts is not None and product_filter_smarts.strip() != "":
if not FormatConverter.is_valid_smarts(product_filter_smarts.strip()):
raise ValueError(f'Product Filter SMARTS "{product_filter_smarts}" is invalid!')
else:
r.product_filter_smarts = product_filter_smarts.strip()
r.product_filter_smarts = product_filter_smarts
r.save()
return r
@ -1240,6 +1230,7 @@ class SimpleAmbitRule(SimpleRule):
@property
def related_reactions(self):
Package = s.GET_PACKAGE_MODEL()
qs = Package.objects.filter(reviewed=True)
return self.reaction_rule.filter(package__in=qs).order_by("name")
@ -1332,7 +1323,7 @@ class SequentialRuleOrdering(models.Model):
class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin):
package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
s.PACKAGE_IMPLEMENTATION, verbose_name="Package", on_delete=models.CASCADE, db_index=True
)
educts = models.ManyToManyField(
"epdb.CompoundStructure", verbose_name="Educts", related_name="reaction_educts"
@ -1354,7 +1345,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
@staticmethod
@transaction.atomic
def create(
package: Package,
package: "Package",
name: str = None,
description: str = None,
educts: Union[List[str], List[CompoundStructure]] = None,
@ -1420,11 +1411,12 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
r = Reaction()
r.package = package
# Clean for potential XSS
if name is not None and name.strip() != "":
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
r.name = name
if description is not None and name.strip() != "":
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r.description = description
r.multi_step = multi_step
@ -1513,7 +1505,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
s.PACKAGE_IMPLEMENTATION, verbose_name="Package", on_delete=models.CASCADE, db_index=True
)
setting = models.ForeignKey(
"epdb.Setting", verbose_name="Setting", on_delete=models.CASCADE, null=True, blank=True
@ -1732,15 +1724,14 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
):
pw = Pathway()
pw.package = package
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 == "":
if name is None or name.strip() == "":
name = f"Pathway {Pathway.objects.filter(package=package).count() + 1}"
pw.name = name
if description is not None and description.strip() != "":
pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
pw.description = description
pw.save()
try:
@ -2035,16 +2026,11 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
for node in end_nodes:
e.end_nodes.add(node)
# 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 == "":
if name is None:
name = f"Reaction {pathway.package.reactions.count() + 1}"
if description is None:
description = s.DEFAULT_VALUES["description"]
description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r = Reaction.create(
pathway.package,
@ -2075,7 +2061,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
class EPModel(PolymorphicModel, EnviPathModel):
package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
s.PACKAGE_IMPLEMENTATION, verbose_name="Package", on_delete=models.CASCADE, db_index=True
)
def _url(self):
@ -2084,17 +2070,17 @@ class EPModel(PolymorphicModel, EnviPathModel):
class PackageBasedModel(EPModel):
rule_packages = models.ManyToManyField(
"Package",
s.PACKAGE_IMPLEMENTATION,
verbose_name="Rule Packages",
related_name="%(app_label)s_%(class)s_rule_packages",
)
data_packages = models.ManyToManyField(
"Package",
s.PACKAGE_IMPLEMENTATION,
verbose_name="Data Packages",
related_name="%(app_label)s_%(class)s_data_packages",
)
eval_packages = models.ManyToManyField(
"Package",
s.PACKAGE_IMPLEMENTATION,
verbose_name="Evaluation Packages",
related_name="%(app_label)s_%(class)s_eval_packages",
)
@ -2198,7 +2184,7 @@ class PackageBasedModel(EPModel):
applicable_rules = self.applicable_rules
reactions = list(self._get_reactions())
ds = RuleBasedDataset.generate_dataset(reactions, applicable_rules, educts_only=True)
ds = Dataset.generate_dataset(reactions, applicable_rules, educts_only=True)
end = datetime.now()
logger.debug(f"build_dataset took {(end - start).total_seconds()} seconds")
@ -2207,7 +2193,7 @@ class PackageBasedModel(EPModel):
ds.save(f)
return ds
def load_dataset(self) -> "Dataset | RuleBasedDataset | EnviFormerDataset":
def load_dataset(self) -> "Dataset":
ds_path = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.pkl")
return Dataset.load(ds_path)
@ -2248,7 +2234,7 @@ class PackageBasedModel(EPModel):
self.model_status = self.BUILT_NOT_EVALUATED
self.save()
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None):
if self.model_status != self.BUILT_NOT_EVALUATED:
raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
@ -2366,39 +2352,37 @@ class PackageBasedModel(EPModel):
eval_reactions = list(
Reaction.objects.filter(package__in=self.eval_packages.all()).distinct()
)
ds = RuleBasedDataset.generate_dataset(
eval_reactions, self.applicable_rules, educts_only=True
)
ds = Dataset.generate_dataset(eval_reactions, self.applicable_rules, educts_only=True)
if isinstance(self, RuleBasedRelativeReasoning):
X = ds.X(exclude_id_col=False, na_replacement=None).to_numpy()
y = ds.y(na_replacement=np.nan).to_numpy()
X = np.array(ds.X(exclude_id_col=False, na_replacement=None))
y = np.array(ds.y(na_replacement=np.nan))
else:
X = ds.X(na_replacement=np.nan).to_numpy()
y = ds.y(na_replacement=np.nan).to_numpy()
X = np.array(ds.X(na_replacement=np.nan))
y = np.array(ds.y(na_replacement=np.nan))
single_gen_result = evaluate_sg(self.model, X, y, np.arange(len(X)), self.threshold)
self.eval_results = self.compute_averages([single_gen_result])
else:
ds = self.load_dataset()
if isinstance(self, RuleBasedRelativeReasoning):
X = ds.X(exclude_id_col=False, na_replacement=None).to_numpy()
y = ds.y(na_replacement=np.nan).to_numpy()
X = np.array(ds.X(exclude_id_col=False, na_replacement=None))
y = np.array(ds.y(na_replacement=np.nan))
else:
X = ds.X(na_replacement=np.nan).to_numpy()
y = ds.y(na_replacement=np.nan).to_numpy()
X = np.array(ds.X(na_replacement=np.nan))
y = np.array(ds.y(na_replacement=np.nan))
n_splits = kwargs.get("n_splits", 20)
n_splits = 20
shuff = ShuffleSplit(n_splits=n_splits, test_size=0.25, random_state=42)
splits = list(shuff.split(X))
from joblib import Parallel, delayed
models = Parallel(n_jobs=min(10, len(splits)))(
models = Parallel(n_jobs=10)(
delayed(train_func)(X, y, train_index, self._model_args())
for train_index, _ in splits
)
evaluations = Parallel(n_jobs=min(10, len(splits)))(
evaluations = Parallel(n_jobs=10)(
delayed(evaluate_sg)(model, X, y, test_index, self.threshold)
for model, (_, test_index) in zip(models, splits)
)
@ -2566,15 +2550,14 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
):
rbrr = RuleBasedRelativeReasoning()
rbrr.package = package
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 == "":
if name is None or name.strip() == "":
name = f"RuleBasedRelativeReasoning {RuleBasedRelativeReasoning.objects.filter(package=package).count() + 1}"
rbrr.name = name
if description is not None and description.strip() != "":
rbrr.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
rbrr.description = description
if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.")
@ -2611,11 +2594,11 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
return rbrr
def _fit_model(self, ds: RuleBasedDataset):
def _fit_model(self, ds: Dataset):
X, y = ds.X(exclude_id_col=False, na_replacement=None), ds.y(na_replacement=None)
model = RelativeReasoning(
start_index=ds.triggered()[0],
end_index=ds.triggered()[-1],
end_index=ds.triggered()[1],
)
model.fit(X, y)
return model
@ -2625,7 +2608,7 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
return {
"clz": "RuleBaseRelativeReasoning",
"start_index": ds.triggered()[0],
"end_index": ds.triggered()[-1],
"end_index": ds.triggered()[1],
}
def _save_model(self, model):
@ -2671,15 +2654,14 @@ class MLRelativeReasoning(PackageBasedModel):
):
mlrr = MLRelativeReasoning()
mlrr.package = package
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 == "":
if name is None or name.strip() == "":
name = f"MLRelativeReasoning {MLRelativeReasoning.objects.filter(package=package).count() + 1}"
mlrr.name = name
if description is not None and description.strip() != "":
mlrr.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
mlrr.description = description
if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.")
@ -2714,11 +2696,11 @@ class MLRelativeReasoning(PackageBasedModel):
return mlrr
def _fit_model(self, ds: RuleBasedDataset):
def _fit_model(self, ds: Dataset):
X, y = ds.X(na_replacement=np.nan), ds.y(na_replacement=np.nan)
model = EnsembleClassifierChain(**s.DEFAULT_MODEL_PARAMS)
model.fit(X.to_numpy(), y.to_numpy())
model.fit(X, y)
return model
def _model_args(self):
@ -2741,7 +2723,7 @@ class MLRelativeReasoning(PackageBasedModel):
start = datetime.now()
ds = self.load_dataset()
classify_ds, classify_prods = ds.classification_dataset([smiles], self.applicable_rules)
pred = self.model.predict_proba(classify_ds.X().to_numpy())
pred = self.model.predict_proba(classify_ds.X())
res = MLRelativeReasoning.combine_products_and_probs(
self.applicable_rules, pred[0], classify_prods[0]
@ -2786,9 +2768,7 @@ class ApplicabilityDomain(EnviPathModel):
@cached_property
def training_set_probs(self):
ds = self.model.load_dataset()
col_ids = ds.block_indices("prob")
return ds[:, col_ids]
return joblib.load(os.path.join(s.MODEL_DIR, f"{self.model.uuid}_train_probs.pkl"))
def build(self):
ds = self.model.load_dataset()
@ -2796,9 +2776,9 @@ class ApplicabilityDomain(EnviPathModel):
start = datetime.now()
# Get Trainingset probs and dump them as they're required when using the app domain
probs = self.model.model.predict_proba(ds.X().to_numpy())
ds.add_probs(probs)
ds.save(os.path.join(s.MODEL_DIR, f"{self.model.uuid}_ds.pkl"))
probs = self.model.model.predict_proba(ds.X())
f = os.path.join(s.MODEL_DIR, f"{self.model.uuid}_train_probs.pkl")
joblib.dump(probs, f)
ad = ApplicabilityDomainPCA(num_neighbours=self.num_neighbours)
ad.build(ds)
@ -2821,20 +2801,15 @@ class ApplicabilityDomain(EnviPathModel):
joblib.dump(ad, f)
def assess(self, structure: Union[str, "CompoundStructure"]):
return self.assess_batch([structure])[0]
def assess_batch(self, structures: List["CompoundStructure | str"]):
ds = self.model.load_dataset()
smiles = []
for struct in structures:
if isinstance(struct, CompoundStructure):
smiles.append(structures.smiles)
if isinstance(structure, CompoundStructure):
smiles = structure.smiles
else:
smiles.append(structures)
smiles = structure
assessment_ds, assessment_prods = ds.classification_dataset(
structures, self.model.applicable_rules
[structure], self.model.applicable_rules
)
# qualified_neighbours_per_rule is a nested dictionary structured as:
@ -2848,61 +2823,82 @@ class ApplicabilityDomain(EnviPathModel):
# it identifies all training structures that have the same trigger reaction activated (i.e., value 1).
# This is used to find "qualified neighbours" — training examples that share the same triggered feature
# with a given assessment structure under a particular rule.
qualified_neighbours_per_rule: Dict = {}
qualified_neighbours_per_rule: Dict[int, Dict[int, List[int]]] = defaultdict(
lambda: defaultdict(list)
)
import polars as pl
for rule_idx, feature_index in enumerate(range(*assessment_ds.triggered())):
feature = ds.columns[feature_index]
if feature.startswith("trig_"):
# TODO unroll loop
for i, cx in enumerate(assessment_ds.X(exclude_id_col=False)):
if int(cx[feature_index]) == 1:
for j, tx in enumerate(ds.X(exclude_id_col=False)):
if int(tx[feature_index]) == 1:
qualified_neighbours_per_rule[i][rule_idx].append(j)
# Select only the triggered columns
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
# trigger that rule.
train_trig = {
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
rule_to_i = {str(r.uuid): i for i, r in enumerate(self.model.applicable_rules)}
probs = self.training_set_probs
# preds = self.model.model.predict_proba(assessment_ds.X())
preds = self.model.combine_products_and_probs(
self.model.applicable_rules,
self.model.model.predict_proba(assessment_ds.X().to_numpy())[0],
self.model.model.predict_proba(assessment_ds.X())[0],
assessment_prods[0],
)
assessments = list()
# loop through our assessment dataset
for i, instance in enumerate(assessment_ds[:, assessment_ds.struct_features()]):
for i, instance in enumerate(assessment_ds):
rule_reliabilities = dict()
local_compatibilities = dict()
neighbours_per_rule = dict()
neighbor_probs_per_rule = dict()
# 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():
# compute tanimoto distance for all neighbours and add to dataset
for rule_idx, vals in qualified_neighbours_per_rule[i].items():
# collect the train dataset instances and store it along with the index (a.k.a. row number) of the
# train dataset
train_instances = []
for v in vals:
train_instances.append((v, ds.at(v)))
# sf is a tuple with start/end index of the features
sf = ds.struct_features()
# compute tanimoto distance for all neighbours
# result ist a list of tuples with train index and computed distance
dists = self._compute_distances(
assessment_ds[i, assessment_ds.struct_features()].to_numpy()[0],
train_instances[:, train_instances.struct_features()].to_numpy(),
instance.X()[0][sf[0] : sf[1]],
[ti[1].X()[0][sf[0] : sf[1]] for ti in train_instances],
)
train_instances = train_instances.with_columns(dist=pl.Series(dists))
dists_with_index = list()
for ti, dist in zip(train_instances, dists):
dists_with_index.append((ti[0], dist[1]))
# 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)
train_instances = train_instances.sort("dist", descending=True)[
: self.num_neighbours
]
dists_with_index = sorted(dists_with_index, key=lambda x: x[1], reverse=True)
dists_with_index = dists_with_index[: self.num_neighbours]
# compute average distance
rule_reliabilities[rule_uuid] = (
train_instances.select(pl.mean("dist")).fill_nan(0.0).item()
rule_reliabilities[rule_idx] = (
sum([d[1] for d in dists_with_index]) / len(dists_with_index)
if len(dists_with_index) > 0
else 0.0
)
# 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
neighbour_datasets = [(d[0], ds.at(d[0])) for d in dists_with_index]
local_compatibilities[rule_idx] = self._compute_compatibility(
rule_idx, probs, neighbour_datasets
)
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()
neighbours_per_rule[rule_idx] = [
CompoundStructure.objects.get(uuid=ds[1].structure_id())
for ds in neighbour_datasets
]
neighbor_probs_per_rule[rule_idx] = [
probs[d[0]][rule_idx] for d in dists_with_index
]
ad_res = {
"ad_params": {
@ -2913,21 +2909,23 @@ class ApplicabilityDomain(EnviPathModel):
"local_compatibility_threshold": self.local_compatibilty_threshold,
},
"assessment": {
"smiles": smiles[i],
"inside_app_domain": self.pca.is_applicable(assessment_ds[i])[0],
"smiles": smiles,
"inside_app_domain": self.pca.is_applicable(instance)[0],
},
}
transformations = list()
for rule_uuid in rule_reliabilities.keys():
rule = Rule.objects.get(uuid=rule_uuid)
for rule_idx in rule_reliabilities.keys():
rule = Rule.objects.get(
uuid=instance.columns[instance.observed()[0] + rule_idx].replace("obs_", "")
)
rule_data = rule.simple_json()
rule_data["image"] = f"{rule.url}?image=svg"
neighbors = []
for n, n_prob in zip(
neighbours_per_rule[rule_uuid], neighbor_probs_per_rule[rule_uuid]
neighbours_per_rule[rule_idx], neighbor_probs_per_rule[rule_idx]
):
neighbor = n.simple_json()
neighbor["image"] = f"{n.url}?image=svg"
@ -2944,14 +2942,14 @@ class ApplicabilityDomain(EnviPathModel):
transformation = {
"rule": rule_data,
"reliability": rule_reliabilities[rule_uuid],
"reliability": rule_reliabilities[rule_idx],
# We're setting it here to False, as we don't know whether "assess" is called during pathway
# prediction or from Model Page. For persisted Nodes this field will be overwritten at runtime
"is_predicted": False,
"local_compatibility": local_compatibilities[rule_uuid],
"probability": preds[rule_to_i[rule_uuid]].probability,
"local_compatibility": local_compatibilities[rule_idx],
"probability": preds[rule_idx].probability,
"transformation_products": [
x.product_set for x in preds[rule_to_i[rule_uuid]].product_sets
x.product_set for x in preds[rule_idx].product_sets
],
"times_triggered": ds.times_triggered(str(rule.uuid)),
"neighbors": neighbors,
@ -2969,24 +2967,32 @@ class ApplicabilityDomain(EnviPathModel):
def _compute_distances(classify_instance: List[int], train_instances: List[List[int]]):
from utilities.ml import tanimoto_distance
distances = [tanimoto_distance(classify_instance, train) for train in train_instances]
distances = [
(i, tanimoto_distance(classify_instance, train))
for i, train in enumerate(train_instances)
]
return distances
def _compute_compatibility(self, rule_idx: int, neighbours: "RuleBasedDataset"):
@staticmethod
def _compute_compatibility(rule_idx: int, preds, neighbours: List[Tuple[int, "Dataset"]]):
tp, tn, fp, fn = 0.0, 0.0, 0.0, 0.0
accuracy = 0.0
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,
)
# Compute tp, tn, fp, fn using polars expressions
tp = obs_pred.filter((pl.col("obs")) & (pl.col("pred"))).height
tn = obs_pred.filter((~pl.col("obs")) & (~pl.col("pred"))).height
fp = obs_pred.filter((~pl.col("obs")) & (pl.col("pred"))).height
fn = obs_pred.filter((pl.col("obs")) & (~pl.col("pred"))).height
for n in neighbours:
obs = n[1].y()[0][rule_idx]
pred = preds[n[0]][rule_idx]
if obs and pred:
tp += 1
elif not obs and pred:
fp += 1
elif obs and not pred:
fn += 1
else:
tn += 1
# Jaccard Index
if tp + tn > 0.0:
accuracy = (tp + tn) / (tp + tn + fp + fn)
return accuracy
@ -3006,15 +3012,14 @@ class EnviFormer(PackageBasedModel):
):
mod = EnviFormer()
mod.package = package
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 == "":
if name is None or name.strip() == "":
name = f"EnviFormer {EnviFormer.objects.filter(package=package).count() + 1}"
mod.name = name
if description is not None and description.strip() != "":
mod.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
mod.description = description
if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.")
@ -3088,24 +3093,44 @@ class EnviFormer(PackageBasedModel):
self.save()
start = datetime.now()
ds = EnviFormerDataset.generate_dataset(self._get_reactions())
# Standardise reactions for the training data, EnviFormer ignores stereochemistry currently
co2 = {"C(=O)=O", "O=C=O"}
ds = []
for reaction in self._get_reactions():
educts = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.educts.all()
]
)
products = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.products.all()
]
)
if products not in co2:
ds.append(f"{educts}>>{products}")
end = datetime.now()
logger.debug(f"build_dataset took {(end - start).total_seconds()} seconds")
f = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.json")
ds.save(f)
with open(f, "w") as d_file:
json.dump(ds, d_file)
return ds
def load_dataset(self):
def load_dataset(self) -> "Dataset":
ds_path = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.json")
return EnviFormerDataset.load(ds_path)
with open(ds_path) as d_file:
ds = json.load(d_file)
return ds
def _fit_model(self, ds):
# Call to enviFormer's fine_tune function and return the model
from enviformer.finetune import fine_tune
start = datetime.now()
model = fine_tune(ds.X(), ds.y(), s.MODEL_DIR, str(self.uuid), device=s.ENVIFORMER_DEVICE)
model = fine_tune(ds, s.MODEL_DIR, str(self.uuid), device=s.ENVIFORMER_DEVICE)
end = datetime.now()
logger.debug(f"EnviFormer finetuning took {(end - start).total_seconds():.2f} seconds")
return model
@ -3121,7 +3146,7 @@ class EnviFormer(PackageBasedModel):
args = {"clz": "EnviFormer"}
return args
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None):
if self.model_status != self.BUILT_NOT_EVALUATED:
raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
@ -3136,20 +3161,21 @@ class EnviFormer(PackageBasedModel):
self.model_status = self.EVALUATING
self.save()
def evaluate_sg(test_ds, predictions, model_thresh):
def evaluate_sg(test_reactions, predictions, model_thresh):
# Group the true products of reactions with the same reactant together
assert len(test_ds) == len(predictions)
true_dict = {}
for r in test_ds:
reactant, true_product_set = r
for r in test_reactions:
reactant, true_product_set = r.split(">>")
true_product_set = {p for p in true_product_set.split(".")}
true_dict[reactant] = true_dict.setdefault(reactant, []) + [true_product_set]
assert len(test_reactions) == len(predictions)
assert sum(len(v) for v in true_dict.values()) == len(test_reactions)
# Group the predicted products of reactions with the same reactant together
pred_dict = {}
for k, pred in enumerate(predictions):
pred_smiles, pred_proba = zip(*pred.items())
reactant, _ = test_ds[k, "educts"], test_ds[k, "products"]
reactant, true_product = test_reactions[k].split(">>")
pred_dict.setdefault(reactant, {"predict": [], "scores": []})
for smiles, proba in zip(pred_smiles, pred_proba):
smiles = set(smiles.split("."))
@ -3184,7 +3210,7 @@ class EnviFormer(PackageBasedModel):
break
# Recall is TP (correct) / TP + FN (len(test_reactions))
rec = {f"{k:.2f}": v / len(test_ds) for k, v in correct.items()}
rec = {f"{k:.2f}": v / len(test_reactions) for k, v in correct.items()}
# Precision is TP (correct) / TP + FP (predicted)
prec = {
f"{k:.2f}": v / predicted[k] if predicted[k] > 0 else 0 for k, v in correct.items()
@ -3263,35 +3289,47 @@ class EnviFormer(PackageBasedModel):
# If there are eval packages perform single generation evaluation on them instead of random splits
if self.eval_packages.count() > 0:
ds = EnviFormerDataset.generate_dataset(
Reaction.objects.filter(package__in=self.eval_packages.all()).distinct()
ds = []
for reaction in Reaction.objects.filter(
package__in=self.eval_packages.all()
).distinct():
educts = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.educts.all()
]
)
test_result = self.model.predict_batch(ds.X())
products = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.products.all()
]
)
ds.append(f"{educts}>>{products}")
test_result = self.model.predict_batch([smirk.split(">>")[0] for smirk in ds])
single_gen_result = evaluate_sg(ds, test_result, self.threshold)
self.eval_results = self.compute_averages([single_gen_result])
else:
from enviformer.finetune import fine_tune
ds = self.load_dataset()
n_splits = kwargs.get("n_splits", 20)
n_splits = 20
shuff = ShuffleSplit(n_splits=n_splits, test_size=0.1, random_state=42)
# Single gen eval is done in one loop of train then evaluate rather than storing all n_splits trained models
# this helps reduce the memory footprint.
single_gen_results = []
for split_id, (train_index, test_index) in enumerate(shuff.split(ds)):
train = ds[train_index]
test = ds[test_index]
train = [ds[i] for i in train_index]
test = [ds[i] for i in test_index]
start = datetime.now()
model = fine_tune(
train.X(), train.y(), s.MODEL_DIR, str(split_id), device=s.ENVIFORMER_DEVICE
)
model = fine_tune(train, s.MODEL_DIR, str(split_id), device=s.ENVIFORMER_DEVICE)
end = datetime.now()
logger.debug(
f"EnviFormer finetuning took {(end - start).total_seconds():.2f} seconds"
)
model.to(s.ENVIFORMER_DEVICE)
test_result = model.predict_batch(test.X())
test_result = model.predict_batch([smirk.split(">>")[0] for smirk in test])
single_gen_results.append(evaluate_sg(test, test_result, self.threshold))
self.eval_results = self.compute_averages(single_gen_results)
@ -3370,12 +3408,23 @@ class EnviFormer(PackageBasedModel):
):
overlap += 1
continue
train_reactions.append(reaction)
train_ds = EnviFormerDataset.generate_dataset(train_reactions)
educts = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.educts.all()
]
)
products = ".".join(
[
FormatConverter.standardize(smile.smiles, remove_stereo=True)
for smile in reaction.products.all()
]
)
train_reactions.append(f"{educts}>>{products}")
logging.debug(
f"{overlap} compounds had to be removed from multigen split due to overlap within pathways"
)
model = fine_tune(train_ds.X(), train_ds.y(), s.MODEL_DIR, f"mg_{split_id}")
model = fine_tune(train_reactions, s.MODEL_DIR, f"mg_{split_id}")
multi_gen_results.append(evaluate_mg(model, test_pathways, self.threshold))
self.eval_results.update(
@ -3399,7 +3448,7 @@ class PluginModel(EPModel):
class Scenario(EnviPathModel):
package = models.ForeignKey(
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
s.PACKAGE_IMPLEMENTATION, verbose_name="Package", on_delete=models.CASCADE, db_index=True
)
scenario_date = models.CharField(max_length=256, null=False, blank=False, default="No date")
scenario_type = models.CharField(
@ -3424,44 +3473,41 @@ class Scenario(EnviPathModel):
scenario_type: str,
additional_information: List["EnviPyModel"],
):
new_s = Scenario()
new_s.package = package
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 == "":
s = Scenario()
s.package = package
if name is None or name.strip() == "":
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() != "":
new_s.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
s.description = description
if scenario_date is not None and scenario_date.strip() != "":
new_s.scenario_date = nh3.clean(scenario_date).strip()
s.scenario_date = scenario_date
if scenario_type is not None and scenario_type.strip() != "":
new_s.scenario_type = scenario_type
s.scenario_type = scenario_type
add_inf = defaultdict(list)
for info in additional_information:
cls_name = info.__class__.__name__
# Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(info.model_dump_json()).strip())
ai_data = json.loads(info.model_dump_json())
ai_data["uuid"] = f"{uuid4()}"
add_inf[cls_name].append(ai_data)
new_s.additional_information = add_inf
s.additional_information = add_inf
new_s.save()
s.save()
return new_s
return s
@transaction.atomic
def add_additional_information(self, data: "EnviPyModel"):
cls_name = data.__class__.__name__
# Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
ai_data = json.loads(data.model_dump_json())
ai_data["uuid"] = f"{uuid4()}"
if cls_name not in self.additional_information:
@ -3496,8 +3542,7 @@ class Scenario(EnviPathModel):
new_ais = defaultdict(list)
for k, vals in data.items():
for v in vals:
# Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(v.model_dump_json()).strip())
ai_data = json.loads(v.model_dump_json())
if hasattr(v, "uuid"):
ai_data["uuid"] = str(v.uuid)
else:
@ -3554,7 +3599,7 @@ class Setting(EnviPathModel):
)
rule_packages = models.ManyToManyField(
"Package",
s.PACKAGE_IMPLEMENTATION,
verbose_name="Setting Rule Packages",
related_name="setting_rule_packages",
blank=True,

View File

@ -7,9 +7,12 @@ from uuid import uuid4
from celery import shared_task
from celery.utils.functional import LRUCache
from django.conf import settings as s
from epdb.logic import SPathway
from epdb.models import EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User, Edge
from epdb.models import Edge, EPModel, JobLog, Node, Pathway, Rule, Setting, User
Package = s.GET_PACKAGE_MODEL()
logger = logging.getLogger(__name__)
ML_CACHE = LRUCache(3) # Cache the three most recent ML models to reduce load times.

View File

@ -193,13 +193,4 @@ urlpatterns = [
re_path(r"^jobs", v.jobs, name="jobs"),
# OAuth Stuff
path("o/userinfo/", v.userinfo, name="oauth_userinfo"),
# Static Pages
re_path(r"^terms$", v.static_terms_of_use, name="terms_of_use"),
re_path(r"^privacy$", v.static_privacy_policy, name="privacy_policy"),
re_path(r"^cookie-policy$", v.static_cookie_policy, name="cookie_policy"),
re_path(r"^about$", v.static_about_us, name="about_us"),
re_path(r"^contact$", v.static_contact_support, name="contact_support"),
re_path(r"^careers$", v.static_careers, name="careers"),
re_path(r"^cite$", v.static_cite, name="cite"),
re_path(r"^legal$", v.static_legal, name="legal"),
]

View File

@ -1,56 +1,56 @@
import json
import logging
from typing import List, Dict, Any
from typing import Any, Dict, List
from django.conf import settings as s
from django.contrib.auth import get_user_model
from django.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest
from django.shortcuts import render, redirect
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, JsonResponse
from django.shortcuts import redirect, render
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from envipy_additional_information import NAME_MAPPING
from oauth2_provider.decorators import protected_resource
import nh3
from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required
from utilities.misc import HTMLGenerator
from .logic import (
EPDBURLParser,
GroupManager,
PackageManager,
UserManager,
SettingManager,
SearchManager,
EPDBURLParser,
SettingManager,
UserManager,
)
from .models import (
Package,
GroupPackagePermission,
Group,
CompoundStructure,
APIToken,
Compound,
CompoundStructure,
Edge,
EnviFormer,
EnzymeLink,
EPModel,
ExternalDatabase,
ExternalIdentifier,
Group,
GroupPackagePermission,
JobLog,
License,
MLRelativeReasoning,
Node,
Pathway,
Permission,
Reaction,
Rule,
Pathway,
Node,
EPModel,
EnviFormer,
MLRelativeReasoning,
RuleBasedRelativeReasoning,
Scenario,
SimpleAmbitRule,
APIToken,
UserPackagePermission,
Permission,
License,
User,
Edge,
ExternalDatabase,
ExternalIdentifier,
EnzymeLink,
JobLog,
UserPackagePermission,
)
Package = s.GET_PACKAGE_MODEL()
logger = logging.getLogger(__name__)
@ -83,14 +83,9 @@ def login(request):
return render(request, "static/login.html", context)
elif request.method == "POST":
from django.contrib.auth import authenticate
from django.contrib.auth import login
username = request.POST.get("username").strip()
if username != request.POST.get("username"):
context["message"] = "Login failed!"
return render(request, "static/login.html", context)
from django.contrib.auth import authenticate, login
username = request.POST.get("username")
password = request.POST.get("password")
# Get email for username and check if the account is active
@ -105,7 +100,6 @@ def login(request):
except get_user_model().DoesNotExist:
context["message"] = "Login failed!"
return render(request, "static/login.html", context)
try:
user = authenticate(username=email, password=password)
except Exception:
@ -143,14 +137,9 @@ def register(request):
context = get_base_context(request)
if request.method == "GET":
# Redirect to unified login page with signup tab
next_url = request.GET.get("next", "")
redirect_url = reverse("login") + "#signup"
if next_url:
redirect_url += f"?next={next_url}"
return redirect(redirect_url)
context["title"] = "enviPath"
context["next"] = request.GET.get("next", "")
return render(request, "static/register.html", context)
elif request.method == "POST":
context["title"] = "enviPath"
if next := request.POST.get("next"):
@ -163,18 +152,18 @@ def register(request):
if not (username and email and password):
context["message"] = "Invalid username/email/password"
return render(request, "static/login.html", context)
return render(request, "static/register.html", context)
if password != rpassword or password == "":
context["message"] = "Registration failed, provided passwords differ!"
return render(request, "static/login.html", context)
return render(request, "static/register.html", context)
try:
u = UserManager.create_user(username, email, password)
logger.info(f"Created user {u.username} ({u.pk})")
except Exception:
context["message"] = "Registration failed! Couldn't create User Account."
return render(request, "static/login.html", context)
return render(request, "static/register.html", context)
if s.ADMIN_APPROVAL_REQUIRED:
context["success_message"] = (
@ -248,7 +237,6 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
"enabled_features": s.FLAGS,
"debug": s.DEBUG,
"external_databases": ExternalDatabase.get_databases(),
"site_id": s.MATOMO_SITE_ID,
},
}
@ -681,8 +669,7 @@ def search(request):
if request.method == "GET":
package_urls = request.GET.getlist("packages")
searchterm = request.GET.get("search", "").strip()
searchterm = request.GET.get("search")
mode = request.GET.get("mode")
# add HTTP_ACCEPT check to differentiate between index and ajax call
@ -783,6 +770,7 @@ def package_models(request, package_uuid):
elif request.method == "POST":
log_post_params(request)
name = request.POST.get("model-name")
description = request.POST.get("model-description")
@ -844,7 +832,7 @@ def package_models(request, package_uuid):
request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
)
from .tasks import dispatch, build_model
from .tasks import build_model, dispatch
dispatch(current_user, build_model, mod.pk)
@ -903,7 +891,7 @@ def package_model(request, package_uuid, model_uuid):
return JsonResponse(res, safe=False)
else:
app_domain_assessment = current_model.app_domain.assess(stand_smiles)
app_domain_assessment = current_model.app_domain.assess(stand_smiles)[0]
return JsonResponse(app_domain_assessment, safe=False)
context = get_base_context(request)
@ -947,14 +935,8 @@ def package_model(request, package_uuid, model_uuid):
else:
return HttpResponseBadRequest()
else:
# TODO: Move cleaning to property updater
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()
name = request.POST.get("model-name", "").strip()
description = request.POST.get("model-description", "").strip()
if any([name, description]):
if name:
@ -1056,16 +1038,8 @@ def package(request, package_uuid):
else:
return HttpResponseBadRequest()
# TODO: Move cleaning to property updater
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")
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")
read = request.POST.get("read") == "on"
@ -1174,7 +1148,7 @@ def package_compounds(request, package_uuid):
elif request.method == "POST":
compound_name = request.POST.get("compound-name")
compound_smiles = request.POST.get("compound-smiles").strip()
compound_smiles = request.POST.get("compound-smiles")
compound_description = request.POST.get("compound-description")
c = Compound.create(current_package, compound_smiles, compound_name, compound_description)
@ -1227,16 +1201,8 @@ def package_compound(request, package_uuid, compound_uuid):
return JsonResponse({"success": current_compound.url})
# TODO: Move cleaning to property updater
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()
new_compound_name = request.POST.get("compound-name", "").strip()
new_compound_description = request.POST.get("compound-description", "").strip()
if new_compound_name:
current_compound.name = new_compound_name
@ -1301,7 +1267,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
elif request.method == "POST":
structure_name = request.POST.get("structure-name")
structure_smiles = request.POST.get("structure-smiles").strip()
structure_smiles = request.POST.get("structure-smiles")
structure_description = request.POST.get("structure-description")
try:
@ -1372,16 +1338,8 @@ def package_compound_structure(request, package_uuid, compound_uuid, structure_u
else:
return HttpResponseBadRequest()
# TODO: Move cleaning to property updater
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()
new_structure_name = request.POST.get("compound-structure-name", "").strip()
new_structure_description = request.POST.get("compound-structure-description", "").strip()
if new_structure_name:
current_structure.name = new_structure_name
@ -1483,11 +1441,11 @@ def package_rules(request, package_uuid):
# Obtain parameters as required by rule type
if rule_type == "SimpleAmbitRule":
params["smirks"] = request.POST.get("rule-smirks").strip()
params["smirks"] = request.POST.get("rule-smirks")
params["reactant_filter_smarts"] = request.POST.get("rule-reactant-smarts")
params["product_filter_smarts"] = request.POST.get("rule-product-smarts")
elif rule_type == "SimpleRDKitRule":
params["reaction_smarts"] = request.POST.get("rule-reaction-smarts").strip()
params["reaction_smarts"] = request.POST.get("rule-reaction-smarts")
elif rule_type == "ParallelRule":
pass
elif rule_type == "SequentialRule":
@ -1588,14 +1546,8 @@ def package_rule(request, package_uuid, rule_uuid):
return JsonResponse({"success": current_rule.url})
# TODO: Move cleaning to property updater
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()
rule_name = request.POST.get("rule-name", "").strip()
rule_description = request.POST.get("rule-description", "").strip()
if rule_name:
current_rule.name = rule_name
@ -1684,8 +1636,8 @@ def package_reactions(request, package_uuid):
elif request.method == "POST":
reaction_name = request.POST.get("reaction-name")
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(".")
products = reactions_smirks.split(">>")[1].split(".")
@ -1746,16 +1698,8 @@ def package_reaction(request, package_uuid, reaction_uuid):
return JsonResponse({"success": current_reaction.url})
# TODO: Move cleaning to property updater
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()
new_reaction_name = request.POST.get("reaction-name", "").strip()
new_reaction_description = request.POST.get("reaction-description", "").strip()
if new_reaction_name:
current_reaction.name = new_reaction_name
@ -1832,9 +1776,8 @@ def package_pathways(request, package_uuid):
name = request.POST.get("name")
description = request.POST.get("description")
smiles = request.POST.get("smiles", "").strip()
pw_mode = request.POST.get("predict", "predict").strip()
smiles = request.POST.get("smiles", "").strip()
if "smiles" in request.POST and smiles == "":
return error(
@ -1843,6 +1786,8 @@ def package_pathways(request, package_uuid):
"Pathway prediction failed due to missing or empty SMILES",
)
smiles = smiles.strip()
try:
stand_smiles = FormatConverter.standardize(smiles)
except ValueError:
@ -2001,14 +1946,8 @@ def package_pathway(request, package_uuid, pathway_uuid):
return JsonResponse({"success": current_pathway.url})
# TODO: Move cleaning to property updater
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")
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 pathway_name is not None and pathway_name.strip() != "":
@ -2096,8 +2035,8 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
elif request.method == "POST":
node_name = request.POST.get("node-name")
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)
return redirect(current_pathway.url)
@ -2259,9 +2198,9 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
elif request.method == "POST":
log_post_params(request)
edge_name = request.POST.get("edge-name")
edge_description = request.POST.get("edge-description")
edge_substrates = request.POST.getlist("edge-substrates")
edge_products = request.POST.getlist("edge-products")
@ -2348,7 +2287,7 @@ def package_scenarios(request, package_uuid):
"all", False
):
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)
context = get_base_context(request)
@ -2386,9 +2325,9 @@ def package_scenarios(request, package_uuid):
context["unreviewed_objects"] = unreviewed_scenario_qs
from envipy_additional_information import (
SEDIMENT_ADDITIONAL_INFORMATION,
SLUDGE_ADDITIONAL_INFORMATION,
SOIL_ADDITIONAL_INFORMATION,
SEDIMENT_ADDITIONAL_INFORMATION,
)
context["scenario_types"] = {
@ -2396,21 +2335,21 @@ def package_scenarios(request, package_uuid):
"name": "soil",
"widgets": [
HTMLGenerator.generate_html(ai, prefix=f"soil_{0}")
for ai in [x for sv in SOIL_ADDITIONAL_INFORMATION.values() for x in sv]
for ai in [x for s in SOIL_ADDITIONAL_INFORMATION.values() for x in s]
],
},
"Sludge Data": {
"name": "sludge",
"widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sludge_{0}")
for ai in [x for sv in SLUDGE_ADDITIONAL_INFORMATION.values() for x in sv]
for ai in [x for s in SLUDGE_ADDITIONAL_INFORMATION.values() for x in s]
],
},
"Water-Sediment System Data": {
"name": "sediment",
"widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sediment_{0}")
for ai in [x for sv in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in sv]
for ai in [x for s in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in s]
],
},
}
@ -2425,7 +2364,6 @@ def package_scenarios(request, package_uuid):
scenario_name = request.POST.get("scenario-name")
scenario_description = request.POST.get("scenario-description")
scenario_date_year = request.POST.get("scenario-date-year")
scenario_date_month = request.POST.get("scenario-date-month")
scenario_date_day = request.POST.get("scenario-date-day")
@ -2439,9 +2377,9 @@ def package_scenarios(request, package_uuid):
scenario_type = request.POST.get("scenario-type")
additional_information = HTMLGenerator.build_models(request.POST.dict())
additional_information = [x for sv in additional_information.values() for x in sv]
additional_information = [x for s in additional_information.values() for x in s]
new_scen = Scenario.create(
s = Scenario.create(
current_package,
name=scenario_name,
description=scenario_description,
@ -2450,7 +2388,7 @@ def package_scenarios(request, package_uuid):
additional_information=additional_information,
)
return redirect(new_scen.url)
return redirect(s.url)
else:
return HttpResponseNotAllowed(
[
@ -2750,7 +2688,6 @@ def settings(request):
name = request.POST.get("prediction-setting-name")
description = request.POST.get("prediction-setting-description")
new_default = request.POST.get("prediction-setting-new-default", "off") == "on"
max_nodes = min(
@ -2901,60 +2838,3 @@ def userinfo(request):
"email_verified": user.is_active,
}
return JsonResponse(res)
# Static Pages
def static_terms_of_use(request):
context = get_base_context(request)
context["title"] = "enviPath - Terms of Use"
context["public_mode"] = True
return render(request, "static/terms_of_use.html", context)
def static_privacy_policy(request):
context = get_base_context(request)
context["title"] = "enviPath - Privacy Policy"
context["public_mode"] = True
return render(request, "static/privacy_policy.html", context)
def static_cookie_policy(request):
context = get_base_context(request)
context["title"] = "enviPath - Cookie Policy"
context["public_mode"] = True
return render(request, "static/cookie_policy.html", context)
def static_about_us(request):
context = get_base_context(request)
context["title"] = "enviPath - About Us"
context["public_mode"] = True
return render(request, "static/about_us.html", context)
def static_contact_support(request):
context = get_base_context(request)
context["title"] = "enviPath - Contact & Support"
context["public_mode"] = True
return render(request, "static/contact.html", context)
def static_careers(request):
context = get_base_context(request)
context["title"] = "enviPath - Careers"
context["public_mode"] = True
return render(request, "static/careers.html", context)
def static_cite(request):
context = get_base_context(request)
context["title"] = "enviPath - How to Cite"
context["public_mode"] = True
return render(request, "static/cite.html", context)
def static_legal(request):
context = get_base_context(request)
context["title"] = "enviPath - Legal Information"
context["public_mode"] = True
return render(request, "static/legal.html", context)

Binary file not shown.

View File

@ -1,22 +1,19 @@
import gzip
import json
import logging
import os.path
from datetime import datetime
from django.conf import settings as s
from django.http import HttpResponseNotAllowed
from django.shortcuts import render
from epdb.logic import PackageManager
from epdb.models import Rule, SimpleAmbitRule, Package, CompoundStructure
from epdb.views import get_base_context, _anonymous_or_real
from utilities.chem import FormatConverter
from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize
from epdb.models import CompoundStructure, Rule, SimpleAmbitRule
from epdb.views import get_base_context
from utilities.chem import FormatConverter
Package = s.GET_PACKAGE_MODEL()
logger = logging.getLogger(__name__)
@ -59,9 +56,7 @@ def run_both_engines(SMILES, SMIRKS):
set(
[
normalize_smiles(str(x))
for x in FormatConverter.sanitize_smiles(
[str(s) for s in all_rdkit_prods]
)[0]
for x in FormatConverter.sanitize_smiles([str(s) for s in all_rdkit_prods])[0]
]
)
)
@ -85,8 +80,7 @@ def migration(request):
url="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1"
)
ALL_SMILES = [
cs.smiles
for cs in CompoundStructure.objects.filter(compound__package=BBD)
cs.smiles for cs in CompoundStructure.objects.filter(compound__package=BBD)
]
RULES = SimpleAmbitRule.objects.filter(package=BBD)
@ -142,9 +136,7 @@ def migration(request):
)
for r in migration_status["results"]:
r["detail_url"] = r["detail_url"].replace(
"http://localhost:8000", s.SERVER_URL
)
r["detail_url"] = r["detail_url"].replace("http://localhost:8000", s.SERVER_URL)
context.update(**migration_status)
@ -152,8 +144,6 @@ def migration(request):
def migration_detail(request, package_uuid, rule_uuid):
current_user = _anonymous_or_real(request)
if request.method == "GET":
context = get_base_context(request)
@ -235,9 +225,7 @@ def compare(request):
context["smirks"] = (
"[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([#1,#6:7])[H:3].[#6;A:2]=O"
)
context["smiles"] = (
"C(CC(=O)N[C@@H](CS[Se-])C(=O)NCC(=O)[O-])[C@@H](C(=O)[O-])N"
)
context["smiles"] = "C(CC(=O)N[C@@H](CS[Se-])C(=O)NCC(=O)[O-])[C@@H](C(=O)[O-])N"
return render(request, "compare.html", context)
elif request.method == "POST":

View File

@ -1,25 +0,0 @@
{
"name": "envipy",
"version": "1.0.0",
"private": true,
"description": "enviPath UI - Tailwind CSS + DaisyUI",
"scripts": {
"dev": "tailwindcss -i static/css/input.css -o static/css/output.css --watch=always",
"build": "tailwindcss -i static/css/input.css -o static/css/output.css --minify"
},
"devDependencies": {
"@tailwindcss/cli": "^4.1.16",
"@tailwindcss/postcss": "^4.1.16",
"daisyui": "^5.4.3",
"postcss": "^8.5.6",
"prettier": "^3.6.2",
"prettier-plugin-jinja-template": "^2.1.0",
"prettier-plugin-tailwindcss": "^0.7.1",
"tailwindcss": "^4.1.16"
},
"keywords": [
"django",
"tailwindcss",
"daisyui"
]
}

740
pnpm-lock.yaml generated
View File

@ -1,740 +0,0 @@
lockfileVersion: '9.0'
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
importers:
.:
devDependencies:
'@tailwindcss/cli':
specifier: ^4.1.16
version: 4.1.16
'@tailwindcss/postcss':
specifier: ^4.1.16
version: 4.1.16
daisyui:
specifier: ^5.4.3
version: 5.4.3
postcss:
specifier: ^8.5.6
version: 8.5.6
prettier:
specifier: ^3.6.2
version: 3.6.2
prettier-plugin-jinja-template:
specifier: ^2.1.0
version: 2.1.0(prettier@3.6.2)
prettier-plugin-tailwindcss:
specifier: ^0.7.1
version: 0.7.1(prettier@3.6.2)
tailwindcss:
specifier: ^4.1.16
version: 4.1.16
packages:
'@alloc/quick-lru@5.2.0':
resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==}
engines: {node: '>=10'}
'@jridgewell/gen-mapping@0.3.13':
resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==}
'@jridgewell/remapping@2.3.5':
resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==}
'@jridgewell/resolve-uri@3.1.2':
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
engines: {node: '>=6.0.0'}
'@jridgewell/sourcemap-codec@1.5.5':
resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==}
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@parcel/watcher-android-arm64@2.5.1':
resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [android]
'@parcel/watcher-darwin-arm64@2.5.1':
resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [darwin]
'@parcel/watcher-darwin-x64@2.5.1':
resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [darwin]
'@parcel/watcher-freebsd-x64@2.5.1':
resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [freebsd]
'@parcel/watcher-linux-arm-glibc@2.5.1':
resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
'@parcel/watcher-linux-arm-musl@2.5.1':
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
engines: {node: '>= 10.0.0'}
cpu: [arm]
os: [linux]
'@parcel/watcher-linux-arm64-glibc@2.5.1':
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-arm64-musl@2.5.1':
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [linux]
'@parcel/watcher-linux-x64-glibc@2.5.1':
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
'@parcel/watcher-linux-x64-musl@2.5.1':
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [linux]
'@parcel/watcher-win32-arm64@2.5.1':
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
engines: {node: '>= 10.0.0'}
cpu: [arm64]
os: [win32]
'@parcel/watcher-win32-ia32@2.5.1':
resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==}
engines: {node: '>= 10.0.0'}
cpu: [ia32]
os: [win32]
'@parcel/watcher-win32-x64@2.5.1':
resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==}
engines: {node: '>= 10.0.0'}
cpu: [x64]
os: [win32]
'@parcel/watcher@2.5.1':
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
engines: {node: '>= 10.0.0'}
'@tailwindcss/cli@4.1.16':
resolution: {integrity: sha512-dsnANPrh2ZooHyZ/8uJhc9ecpcYtufToc21NY09NS9vF16rxPCjJ8dP7TUAtPqlUJTHSmRkN2hCdoYQIlgh4fw==}
hasBin: true
'@tailwindcss/node@4.1.16':
resolution: {integrity: sha512-BX5iaSsloNuvKNHRN3k2RcCuTEgASTo77mofW0vmeHkfrDWaoFAFvNHpEgtu0eqyypcyiBkDWzSMxJhp3AUVcw==}
'@tailwindcss/oxide-android-arm64@4.1.16':
resolution: {integrity: sha512-8+ctzkjHgwDJ5caq9IqRSgsP70xhdhJvm+oueS/yhD5ixLhqTw9fSL1OurzMUhBwE5zK26FXLCz2f/RtkISqHA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [android]
'@tailwindcss/oxide-darwin-arm64@4.1.16':
resolution: {integrity: sha512-C3oZy5042v2FOALBZtY0JTDnGNdS6w7DxL/odvSny17ORUnaRKhyTse8xYi3yKGyfnTUOdavRCdmc8QqJYwFKA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@tailwindcss/oxide-darwin-x64@4.1.16':
resolution: {integrity: sha512-vjrl/1Ub9+JwU6BP0emgipGjowzYZMjbWCDqwA2Z4vCa+HBSpP4v6U2ddejcHsolsYxwL5r4bPNoamlV0xDdLg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@tailwindcss/oxide-freebsd-x64@4.1.16':
resolution: {integrity: sha512-TSMpPYpQLm+aR1wW5rKuUuEruc/oOX3C7H0BTnPDn7W/eMw8W+MRMpiypKMkXZfwH8wqPIRKppuZoedTtNj2tg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [freebsd]
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16':
resolution: {integrity: sha512-p0GGfRg/w0sdsFKBjMYvvKIiKy/LNWLWgV/plR4lUgrsxFAoQBFrXkZ4C0w8IOXfslB9vHK/JGASWD2IefIpvw==}
engines: {node: '>= 10'}
cpu: [arm]
os: [linux]
'@tailwindcss/oxide-linux-arm64-gnu@4.1.16':
resolution: {integrity: sha512-DoixyMmTNO19rwRPdqviTrG1rYzpxgyYJl8RgQvdAQUzxC1ToLRqtNJpU/ATURSKgIg6uerPw2feW0aS8SNr/w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-arm64-musl@4.1.16':
resolution: {integrity: sha512-H81UXMa9hJhWhaAUca6bU2wm5RRFpuHImrwXBUvPbYb+3jo32I9VIwpOX6hms0fPmA6f2pGVlybO6qU8pF4fzQ==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@tailwindcss/oxide-linux-x64-gnu@4.1.16':
resolution: {integrity: sha512-ZGHQxDtFC2/ruo7t99Qo2TTIvOERULPl5l0K1g0oK6b5PGqjYMga+FcY1wIUnrUxY56h28FxybtDEla+ICOyew==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-linux-x64-musl@4.1.16':
resolution: {integrity: sha512-Oi1tAaa0rcKf1Og9MzKeINZzMLPbhxvm7rno5/zuP1WYmpiG0bEHq4AcRUiG2165/WUzvxkW4XDYCscZWbTLZw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@tailwindcss/oxide-wasm32-wasi@4.1.16':
resolution: {integrity: sha512-B01u/b8LteGRwucIBmCQ07FVXLzImWESAIMcUU6nvFt/tYsQ6IHz8DmZ5KtvmwxD+iTYBtM1xwoGXswnlu9v0Q==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
bundledDependencies:
- '@napi-rs/wasm-runtime'
- '@emnapi/core'
- '@emnapi/runtime'
- '@tybys/wasm-util'
- '@emnapi/wasi-threads'
- tslib
'@tailwindcss/oxide-win32-arm64-msvc@4.1.16':
resolution: {integrity: sha512-zX+Q8sSkGj6HKRTMJXuPvOcP8XfYON24zJBRPlszcH1Np7xuHXhWn8qfFjIujVzvH3BHU+16jBXwgpl20i+v9A==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@tailwindcss/oxide-win32-x64-msvc@4.1.16':
resolution: {integrity: sha512-m5dDFJUEejbFqP+UXVstd4W/wnxA4F61q8SoL+mqTypId2T2ZpuxosNSgowiCnLp2+Z+rivdU0AqpfgiD7yCBg==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
'@tailwindcss/oxide@4.1.16':
resolution: {integrity: sha512-2OSv52FRuhdlgyOQqgtQHuCgXnS8nFSYRp2tJ+4WZXKgTxqPy7SMSls8c3mPT5pkZ17SBToGM5LHEJBO7miEdg==}
engines: {node: '>= 10'}
'@tailwindcss/postcss@4.1.16':
resolution: {integrity: sha512-Qn3SFGPXYQMKR/UtqS+dqvPrzEeBZHrFA92maT4zijCVggdsXnDBMsPFJo1eArX3J+O+Gi+8pV4PkqjLCNBk3A==}
braces@3.0.3:
resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==}
engines: {node: '>=8'}
daisyui@5.4.3:
resolution: {integrity: sha512-dfDCJnN4utErGoWfElgdEE252FtfHV9Mxj5Dq1+JzUq3nVkluWdF3JYykP0Xy/y/yArnPXYztO1tLNCow3kjmg==}
detect-libc@1.0.3:
resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==}
engines: {node: '>=0.10'}
hasBin: true
detect-libc@2.1.2:
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
engines: {node: '>=8'}
enhanced-resolve@5.18.3:
resolution: {integrity: sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==}
engines: {node: '>=10.13.0'}
fill-range@7.1.1:
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
engines: {node: '>=8'}
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
is-extglob@2.1.1:
resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
engines: {node: '>=0.10.0'}
is-glob@4.0.3:
resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==}
engines: {node: '>=0.10.0'}
is-number@7.0.0:
resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==}
engines: {node: '>=0.12.0'}
jiti@2.6.1:
resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==}
hasBin: true
lightningcss-android-arm64@1.30.2:
resolution: {integrity: sha512-BH9sEdOCahSgmkVhBLeU7Hc9DWeZ1Eb6wNS6Da8igvUwAe0sqROHddIlvU06q3WyXVEOYDZ6ykBZQnjTbmo4+A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [android]
lightningcss-darwin-arm64@1.30.2:
resolution: {integrity: sha512-ylTcDJBN3Hp21TdhRT5zBOIi73P6/W0qwvlFEk22fkdXchtNTOU4Qc37SkzV+EKYxLouZ6M4LG9NfZ1qkhhBWA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [darwin]
lightningcss-darwin-x64@1.30.2:
resolution: {integrity: sha512-oBZgKchomuDYxr7ilwLcyms6BCyLn0z8J0+ZZmfpjwg9fRVZIR5/GMXd7r9RH94iDhld3UmSjBM6nXWM2TfZTQ==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [darwin]
lightningcss-freebsd-x64@1.30.2:
resolution: {integrity: sha512-c2bH6xTrf4BDpK8MoGG4Bd6zAMZDAXS569UxCAGcA7IKbHNMlhGQ89eRmvpIUGfKWNVdbhSbkQaWhEoMGmGslA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [freebsd]
lightningcss-linux-arm-gnueabihf@1.30.2:
resolution: {integrity: sha512-eVdpxh4wYcm0PofJIZVuYuLiqBIakQ9uFZmipf6LF/HRj5Bgm0eb3qL/mr1smyXIS1twwOxNWndd8z0E374hiA==}
engines: {node: '>= 12.0.0'}
cpu: [arm]
os: [linux]
lightningcss-linux-arm64-gnu@1.30.2:
resolution: {integrity: sha512-UK65WJAbwIJbiBFXpxrbTNArtfuznvxAJw4Q2ZGlU8kPeDIWEX1dg3rn2veBVUylA2Ezg89ktszWbaQnxD/e3A==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-arm64-musl@1.30.2:
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [linux]
lightningcss-linux-x64-gnu@1.30.2:
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-linux-x64-musl@1.30.2:
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [linux]
lightningcss-win32-arm64-msvc@1.30.2:
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
engines: {node: '>= 12.0.0'}
cpu: [arm64]
os: [win32]
lightningcss-win32-x64-msvc@1.30.2:
resolution: {integrity: sha512-5g1yc73p+iAkid5phb4oVFMB45417DkRevRbt/El/gKXJk4jid+vPFF/AXbxn05Aky8PapwzZrdJShv5C0avjw==}
engines: {node: '>= 12.0.0'}
cpu: [x64]
os: [win32]
lightningcss@1.30.2:
resolution: {integrity: sha512-utfs7Pr5uJyyvDETitgsaqSyjCb2qNRAtuqUeWIAKztsOYdcACf2KtARYXg2pSvhkt+9NfoaNY7fxjl6nuMjIQ==}
engines: {node: '>= 12.0.0'}
magic-string@0.30.21:
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
micromatch@4.0.8:
resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==}
engines: {node: '>=8.6'}
mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'}
nanoid@3.3.11:
resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==}
engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1}
hasBin: true
node-addon-api@7.1.1:
resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==}
picocolors@1.1.1:
resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==}
picomatch@2.3.1:
resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==}
engines: {node: '>=8.6'}
postcss@8.5.6:
resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==}
engines: {node: ^10 || ^12 || >=14}
prettier-plugin-jinja-template@2.1.0:
resolution: {integrity: sha512-mzoCp2Oy9BDSug80fw3B3J4n4KQj1hRvoQOL1akqcDKBb5nvYxrik9zUEDs4AEJ6nK7QDTGoH0y9rx7AlnQ78Q==}
peerDependencies:
prettier: ^3.0.0
prettier-plugin-tailwindcss@0.7.1:
resolution: {integrity: sha512-Bzv1LZcuiR1Sk02iJTS1QzlFNp/o5l2p3xkopwOrbPmtMeh3fK9rVW5M3neBQzHq+kGKj/4LGQMTNcTH4NGPtQ==}
engines: {node: '>=20.19'}
peerDependencies:
'@ianvs/prettier-plugin-sort-imports': '*'
'@prettier/plugin-hermes': '*'
'@prettier/plugin-oxc': '*'
'@prettier/plugin-pug': '*'
'@shopify/prettier-plugin-liquid': '*'
'@trivago/prettier-plugin-sort-imports': '*'
'@zackad/prettier-plugin-twig': '*'
prettier: ^3.0
prettier-plugin-astro: '*'
prettier-plugin-css-order: '*'
prettier-plugin-jsdoc: '*'
prettier-plugin-marko: '*'
prettier-plugin-multiline-arrays: '*'
prettier-plugin-organize-attributes: '*'
prettier-plugin-organize-imports: '*'
prettier-plugin-sort-imports: '*'
prettier-plugin-svelte: '*'
peerDependenciesMeta:
'@ianvs/prettier-plugin-sort-imports':
optional: true
'@prettier/plugin-hermes':
optional: true
'@prettier/plugin-oxc':
optional: true
'@prettier/plugin-pug':
optional: true
'@shopify/prettier-plugin-liquid':
optional: true
'@trivago/prettier-plugin-sort-imports':
optional: true
'@zackad/prettier-plugin-twig':
optional: true
prettier-plugin-astro:
optional: true
prettier-plugin-css-order:
optional: true
prettier-plugin-jsdoc:
optional: true
prettier-plugin-marko:
optional: true
prettier-plugin-multiline-arrays:
optional: true
prettier-plugin-organize-attributes:
optional: true
prettier-plugin-organize-imports:
optional: true
prettier-plugin-sort-imports:
optional: true
prettier-plugin-svelte:
optional: true
prettier@3.6.2:
resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==}
engines: {node: '>=14'}
hasBin: true
source-map-js@1.2.1:
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
engines: {node: '>=0.10.0'}
tailwindcss@4.1.16:
resolution: {integrity: sha512-pONL5awpaQX4LN5eiv7moSiSPd/DLDzKVRJz8Q9PgzmAdd1R4307GQS2ZpfiN7ZmekdQrfhZZiSE5jkLR4WNaA==}
tapable@2.3.0:
resolution: {integrity: sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==}
engines: {node: '>=6'}
to-regex-range@5.0.1:
resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==}
engines: {node: '>=8.0'}
snapshots:
'@alloc/quick-lru@5.2.0': {}
'@jridgewell/gen-mapping@0.3.13':
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/remapping@2.3.5':
dependencies:
'@jridgewell/gen-mapping': 0.3.13
'@jridgewell/trace-mapping': 0.3.31
'@jridgewell/resolve-uri@3.1.2': {}
'@jridgewell/sourcemap-codec@1.5.5': {}
'@jridgewell/trace-mapping@0.3.31':
dependencies:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@parcel/watcher-android-arm64@2.5.1':
optional: true
'@parcel/watcher-darwin-arm64@2.5.1':
optional: true
'@parcel/watcher-darwin-x64@2.5.1':
optional: true
'@parcel/watcher-freebsd-x64@2.5.1':
optional: true
'@parcel/watcher-linux-arm-glibc@2.5.1':
optional: true
'@parcel/watcher-linux-arm-musl@2.5.1':
optional: true
'@parcel/watcher-linux-arm64-glibc@2.5.1':
optional: true
'@parcel/watcher-linux-arm64-musl@2.5.1':
optional: true
'@parcel/watcher-linux-x64-glibc@2.5.1':
optional: true
'@parcel/watcher-linux-x64-musl@2.5.1':
optional: true
'@parcel/watcher-win32-arm64@2.5.1':
optional: true
'@parcel/watcher-win32-ia32@2.5.1':
optional: true
'@parcel/watcher-win32-x64@2.5.1':
optional: true
'@parcel/watcher@2.5.1':
dependencies:
detect-libc: 1.0.3
is-glob: 4.0.3
micromatch: 4.0.8
node-addon-api: 7.1.1
optionalDependencies:
'@parcel/watcher-android-arm64': 2.5.1
'@parcel/watcher-darwin-arm64': 2.5.1
'@parcel/watcher-darwin-x64': 2.5.1
'@parcel/watcher-freebsd-x64': 2.5.1
'@parcel/watcher-linux-arm-glibc': 2.5.1
'@parcel/watcher-linux-arm-musl': 2.5.1
'@parcel/watcher-linux-arm64-glibc': 2.5.1
'@parcel/watcher-linux-arm64-musl': 2.5.1
'@parcel/watcher-linux-x64-glibc': 2.5.1
'@parcel/watcher-linux-x64-musl': 2.5.1
'@parcel/watcher-win32-arm64': 2.5.1
'@parcel/watcher-win32-ia32': 2.5.1
'@parcel/watcher-win32-x64': 2.5.1
'@tailwindcss/cli@4.1.16':
dependencies:
'@parcel/watcher': 2.5.1
'@tailwindcss/node': 4.1.16
'@tailwindcss/oxide': 4.1.16
enhanced-resolve: 5.18.3
mri: 1.2.0
picocolors: 1.1.1
tailwindcss: 4.1.16
'@tailwindcss/node@4.1.16':
dependencies:
'@jridgewell/remapping': 2.3.5
enhanced-resolve: 5.18.3
jiti: 2.6.1
lightningcss: 1.30.2
magic-string: 0.30.21
source-map-js: 1.2.1
tailwindcss: 4.1.16
'@tailwindcss/oxide-android-arm64@4.1.16':
optional: true
'@tailwindcss/oxide-darwin-arm64@4.1.16':
optional: true
'@tailwindcss/oxide-darwin-x64@4.1.16':
optional: true
'@tailwindcss/oxide-freebsd-x64@4.1.16':
optional: true
'@tailwindcss/oxide-linux-arm-gnueabihf@4.1.16':
optional: true
'@tailwindcss/oxide-linux-arm64-gnu@4.1.16':
optional: true
'@tailwindcss/oxide-linux-arm64-musl@4.1.16':
optional: true
'@tailwindcss/oxide-linux-x64-gnu@4.1.16':
optional: true
'@tailwindcss/oxide-linux-x64-musl@4.1.16':
optional: true
'@tailwindcss/oxide-wasm32-wasi@4.1.16':
optional: true
'@tailwindcss/oxide-win32-arm64-msvc@4.1.16':
optional: true
'@tailwindcss/oxide-win32-x64-msvc@4.1.16':
optional: true
'@tailwindcss/oxide@4.1.16':
optionalDependencies:
'@tailwindcss/oxide-android-arm64': 4.1.16
'@tailwindcss/oxide-darwin-arm64': 4.1.16
'@tailwindcss/oxide-darwin-x64': 4.1.16
'@tailwindcss/oxide-freebsd-x64': 4.1.16
'@tailwindcss/oxide-linux-arm-gnueabihf': 4.1.16
'@tailwindcss/oxide-linux-arm64-gnu': 4.1.16
'@tailwindcss/oxide-linux-arm64-musl': 4.1.16
'@tailwindcss/oxide-linux-x64-gnu': 4.1.16
'@tailwindcss/oxide-linux-x64-musl': 4.1.16
'@tailwindcss/oxide-wasm32-wasi': 4.1.16
'@tailwindcss/oxide-win32-arm64-msvc': 4.1.16
'@tailwindcss/oxide-win32-x64-msvc': 4.1.16
'@tailwindcss/postcss@4.1.16':
dependencies:
'@alloc/quick-lru': 5.2.0
'@tailwindcss/node': 4.1.16
'@tailwindcss/oxide': 4.1.16
postcss: 8.5.6
tailwindcss: 4.1.16
braces@3.0.3:
dependencies:
fill-range: 7.1.1
daisyui@5.4.3: {}
detect-libc@1.0.3: {}
detect-libc@2.1.2: {}
enhanced-resolve@5.18.3:
dependencies:
graceful-fs: 4.2.11
tapable: 2.3.0
fill-range@7.1.1:
dependencies:
to-regex-range: 5.0.1
graceful-fs@4.2.11: {}
is-extglob@2.1.1: {}
is-glob@4.0.3:
dependencies:
is-extglob: 2.1.1
is-number@7.0.0: {}
jiti@2.6.1: {}
lightningcss-android-arm64@1.30.2:
optional: true
lightningcss-darwin-arm64@1.30.2:
optional: true
lightningcss-darwin-x64@1.30.2:
optional: true
lightningcss-freebsd-x64@1.30.2:
optional: true
lightningcss-linux-arm-gnueabihf@1.30.2:
optional: true
lightningcss-linux-arm64-gnu@1.30.2:
optional: true
lightningcss-linux-arm64-musl@1.30.2:
optional: true
lightningcss-linux-x64-gnu@1.30.2:
optional: true
lightningcss-linux-x64-musl@1.30.2:
optional: true
lightningcss-win32-arm64-msvc@1.30.2:
optional: true
lightningcss-win32-x64-msvc@1.30.2:
optional: true
lightningcss@1.30.2:
dependencies:
detect-libc: 2.1.2
optionalDependencies:
lightningcss-android-arm64: 1.30.2
lightningcss-darwin-arm64: 1.30.2
lightningcss-darwin-x64: 1.30.2
lightningcss-freebsd-x64: 1.30.2
lightningcss-linux-arm-gnueabihf: 1.30.2
lightningcss-linux-arm64-gnu: 1.30.2
lightningcss-linux-arm64-musl: 1.30.2
lightningcss-linux-x64-gnu: 1.30.2
lightningcss-linux-x64-musl: 1.30.2
lightningcss-win32-arm64-msvc: 1.30.2
lightningcss-win32-x64-msvc: 1.30.2
magic-string@0.30.21:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
micromatch@4.0.8:
dependencies:
braces: 3.0.3
picomatch: 2.3.1
mri@1.2.0: {}
nanoid@3.3.11: {}
node-addon-api@7.1.1: {}
picocolors@1.1.1: {}
picomatch@2.3.1: {}
postcss@8.5.6:
dependencies:
nanoid: 3.3.11
picocolors: 1.1.1
source-map-js: 1.2.1
prettier-plugin-jinja-template@2.1.0(prettier@3.6.2):
dependencies:
prettier: 3.6.2
prettier-plugin-tailwindcss@0.7.1(prettier@3.6.2):
dependencies:
prettier: 3.6.2
prettier@3.6.2: {}
source-map-js@1.2.1: {}
tailwindcss@4.1.16: {}
tapable@2.3.0: {}
to-regex-range@5.0.1:
dependencies:
is-number: 7.0.0

0
public/__init__.py Normal file
View File

1
public/admin.py Normal file
View File

@ -0,0 +1 @@
# Register your models here.

6
public/apps.py Normal file
View File

@ -0,0 +1,6 @@
from django.apps import AppConfig
class PublicConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "public"

View File

@ -0,0 +1,56 @@
# Generated by Django 5.2.7 on 2025-10-29 13:32
import django.utils.timezone
import model_utils.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = []
operations = [
migrations.CreateModel(
name="Package",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
(
"uuid",
models.UUIDField(
default=uuid.uuid4, unique=True, verbose_name="UUID of this object"
),
),
("name", models.TextField(default="no name", verbose_name="Name")),
(
"description",
models.TextField(default="no description", verbose_name="Descriptions"),
),
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
("kv", models.JSONField(blank=True, default=dict, null=True)),
("reviewed", models.BooleanField(default=False, verbose_name="Reviewstatus")),
],
options={
"db_table": "epdb_package",
"managed": False,
},
),
]

View File

@ -0,0 +1,16 @@
# Generated by Django 5.2.7 on 2025-10-29 18:39
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
("public", "0001_initial"),
]
operations = [
migrations.AlterModelOptions(
name="package",
options={},
),
]

View File

@ -0,0 +1,25 @@
# Generated by Django 5.2.7 on 2025-10-29 18:40
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0010_alter_userpackagepermission_package_and_more"),
("public", "0002_alter_package_options"),
]
operations = [
migrations.AddField(
model_name="package",
name="license",
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
to="epdb.license",
verbose_name="License",
),
),
]

View File

6
public/models.py Normal file
View File

@ -0,0 +1,6 @@
from epdb.models import AbstractPackage
class Package(AbstractPackage):
class Meta:
db_table = "epdb_package"

1
public/tests.py Normal file
View File

@ -0,0 +1 @@
# Create your tests here.

1
public/views.py Normal file
View File

@ -0,0 +1 @@
# Create your views here.

View File

@ -27,12 +27,10 @@ dependencies = [
"scikit-learn>=1.6.1",
"sentry-sdk[django]>=2.32.0",
"setuptools>=80.8.0",
"nh3==0.3.2",
"polars==1.35.1",
]
[tool.uv.sources]
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" }
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.2" }
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7"}
envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }
@ -67,54 +65,22 @@ docstring-code-format = true
[tool.poe.tasks]
# Main tasks
setup = { sequence = ["db-up", "migrate", "bootstrap"], help = "Complete setup: start database, run migrations, and bootstrap data" }
dev = { shell = """
# Start pnpm CSS watcher in background
pnpm run dev &
PNPM_PID=$!
echo "Started CSS watcher (PID: $PNPM_PID)"
# Cleanup function
cleanup() {
echo "\nShutting down..."
if kill -0 $PNPM_PID 2>/dev/null; then
kill $PNPM_PID
echo " CSS watcher stopped"
fi
if [ ! -z "${DJ_PID:-}" ] && kill -0 $DJ_PID 2>/dev/null; then
kill $DJ_PID
echo " Django server stopped"
fi
}
# Set trap for cleanup
trap cleanup EXIT INT TERM
# Start Django dev server in background
uv run python manage.py runserver &
DJ_PID=$!
# Wait for Django to finish
wait $DJ_PID
""", help = "Start the development server with CSS watcher", deps = ["db-up", "js-deps"] }
build = { sequence = ["build-frontend", "collectstatic"], help = "Build frontend assets and collect static files" }
dev = { cmd = "python manage.py runserver", help = "Start the development server", deps = ["db-up"] }
# Database tasks
db-up = { cmd = "docker compose -f docker-compose.dev.yml up -d", help = "Start PostgreSQL database using Docker Compose" }
db-down = { cmd = "docker compose -f docker-compose.dev.yml down", help = "Stop PostgreSQL database" }
# Frontend tasks
js-deps = { cmd = "pnpm install", help = "Install frontend dependencies" }
# Full cleanup tasks
clean = { sequence = ["clean-db"], help = "Remove model files and database volumes (WARNING: destroys all data!)" }
clean-db = { cmd = "docker compose -f docker-compose.dev.yml down -v", help = "Removes the database container and volume." }
# Django tasks
migrate = { cmd = "uv run python manage.py migrate", help = "Run database migrations" }
migrate = { cmd = "python manage.py migrate", help = "Run database migrations" }
bootstrap = { shell = """
echo "Bootstrapping initial data..."
echo "This will take a bit . Get yourself some coffee..."
uv run python manage.py bootstrap
python manage.py bootstrap
echo " Bootstrap complete"
echo ""
echo "Default admin credentials:"
@ -122,8 +88,4 @@ echo " Username: admin"
echo " Email: admin@envipath.com"
echo " Password: SuperSafe"
""", help = "Bootstrap initial data (anonymous user, packages, models)" }
shell = { cmd = "uv run python manage.py shell", help = "Open Django shell" }
# Build tasks
build-frontend = { cmd = "pnpm run build", help = "Build frontend assets using pnpm", deps = ["js-deps"] }
collectstatic = { cmd = "uv run python manage.py collectstatic --noinput", help = "Collect static files for production", deps = ["build-frontend"] }
shell = { cmd = "python manage.py shell", help = "Open Django shell" }

View File

@ -1,84 +0,0 @@
/**
* DaisyUI Themes - Generated by Style Dictionary
* Theme mappings defined in tokens/daisyui-themes.json
*/
/* Light theme (default) */
@plugin "daisyui/theme" {
name: "envipath";
default: true;
color-scheme: light;
--color-base-100: var(--color-neutral-50);
--color-base-200: var(--color-neutral-100);
--color-base-300: var(--color-neutral-200);
--color-base-content: var(--color-neutral-900);
--color-primary: var(--color-primary-500);
--color-primary-content: var(--color-primary-50);
--color-secondary: var(--color-secondary-500);
--color-secondary-content: var(--color-secondary-50);
--color-accent: var(--color-accent-500);
--color-accent-content: var(--color-accent-50);
--color-neutral: var(--color-neutral-950);
--color-neutral-content: var(--color-neutral-100);
--color-info: var(--color-info-500);
--color-info-content: var(--color-info-950);
--color-success: var(--color-success-500);
--color-success-content: var(--color-success-950);
--color-warning: var(--color-warning-500);
--color-warning-content: var(--color-warning-950);
--color-error: var(--color-error-500);
--color-error-content: var(--color-error-950);
/* border radius */
--radius-selector: 1rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
/* base sizes */
--size-selector: 0.25rem;
--size-field: 0.25rem;
/* border size */
--border: 1px;
/* effects */
--depth: 1;
--noise: 0;
}
/* Dark theme (prefers-color-scheme: dark) */
@plugin "daisyui/theme" {
name: "envipath-dark";
prefersdark: true;
color-scheme: dark;
--color-primary: var(--color-primary-400);
--color-primary-content: var(--color-neutral-950);
--color-secondary: var(--color-secondary-400);
--color-secondary-content: var(--color-neutral-950);
--color-accent: var(--color-primary-500);
--color-accent-content: var(--color-neutral-950);
--color-neutral: var(--color-neutral-300);
--color-neutral-content: var(--color-neutral-900);
--color-base-100: var(--color-neutral-900);
--color-base-200: var(--color-neutral-800);
--color-base-300: var(--color-neutral-700);
--color-base-content: var(--color-neutral-50);
--color-info: var(--color-primary-400);
--color-info-content: var(--color-neutral-950);
--color-success: var(--color-success-400);
--color-success-content: var(--color-neutral-950);
--color-warning: var(--color-warning-400);
--color-warning-content: var(--color-neutral-950);
--color-error: var(--color-error-400);
--color-error-content: var(--color-neutral-950);
--radius-selector: 1rem;
--radius-field: 0.25rem;
--radius-box: 0.5rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 0;
}

View File

@ -1,35 +0,0 @@
@import "tailwindcss";
/* fira-code-latin-wght-normal */
@font-face {
font-family: 'Fira Code Variable';
font-style: normal;
font-display: swap;
font-weight: 300 700;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/fira-code:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
/* inter-latin-wght-normal */
@font-face {
font-family: 'Inter Variable';
font-style: normal;
font-display: swap;
font-weight: 100 900;
src: url(https://cdn.jsdelivr.net/fontsource/fonts/inter:vf@latest/latin-wght-normal.woff2) format('woff2-variations');
unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD;
}
/* Tell Tailwind where to find Django templates and Python files */
@source "../../templates";
/* Custom theme configuration - must come before plugins */
@import "./theme.css";
/* Import DaisyUI plugin */
@plugin "daisyui" {
logs: true;
}
@import "./daisyui-theme.css";

View File

@ -1,111 +0,0 @@
/**
* Tailwind v4 Theme - Generated by Style Dictionary
* This creates Tailwind utility classes from design tokens
*/
@theme {
/* Colors */
--color-primary-50: oklch(0.98 0.02 201);
--color-primary-100: oklch(0.96 0.04 203);
--color-primary-200: oklch(0.92 0.08 205);
--color-primary-300: oklch(0.87 0.12 207);
--color-primary-400: oklch(0.80 0.13 212);
--color-primary-500: oklch(0.71 0.13 215);
--color-primary-600: oklch(0.61 0.11 222);
--color-primary-700: oklch(0.52 0.09 223);
--color-primary-800: oklch(0.45 0.08 224);
--color-primary-900: oklch(0.40 0.07 227);
--color-primary-950: oklch(0.30 0.05 230);
--color-secondary-50: oklch(0.98 0.02 166);
--color-secondary-100: oklch(0.95 0.05 163);
--color-secondary-200: oklch(0.90 0.09 164);
--color-secondary-300: oklch(0.85 0.13 165);
--color-secondary-400: oklch(0.77 0.15 163);
--color-secondary-500: oklch(0.70 0.15 162);
--color-secondary-600: oklch(0.60 0.13 163);
--color-secondary-700: oklch(0.51 0.10 166);
--color-secondary-800: oklch(0.43 0.09 167);
--color-secondary-900: oklch(0.38 0.07 169);
--color-secondary-950: oklch(0.26 0.05 173);
--color-success-50: oklch(0.98 0.02 156);
--color-success-100: oklch(0.96 0.04 157);
--color-success-200: oklch(0.93 0.08 156);
--color-success-300: oklch(0.87 0.14 154);
--color-success-400: oklch(0.80 0.18 152);
--color-success-500: oklch(0.72 0.19 150);
--color-success-600: oklch(0.63 0.17 149);
--color-success-700: oklch(0.53 0.14 150);
--color-success-800: oklch(0.45 0.11 151);
--color-success-900: oklch(0.39 0.09 153);
--color-success-950: oklch(0.27 0.06 153);
--color-warning-50: oklch(0.99 0.03 102);
--color-warning-100: oklch(0.97 0.07 103);
--color-warning-200: oklch(0.95 0.12 102);
--color-warning-300: oklch(0.91 0.17 98);
--color-warning-400: oklch(0.86 0.17 92);
--color-warning-500: oklch(0.80 0.16 86);
--color-warning-600: oklch(0.68 0.14 76);
--color-warning-700: oklch(0.55 0.12 66);
--color-warning-800: oklch(0.48 0.10 62);
--color-warning-900: oklch(0.42 0.09 58);
--color-warning-950: oklch(0.29 0.06 54);
--color-error-50: oklch(0.97 0.01 17);
--color-error-100: oklch(0.94 0.03 18);
--color-error-200: oklch(0.88 0.06 18);
--color-error-300: oklch(0.81 0.10 20);
--color-error-400: oklch(0.71 0.17 22);
--color-error-500: oklch(0.64 0.21 25);
--color-error-600: oklch(0.58 0.22 27);
--color-error-700: oklch(0.51 0.19 28);
--color-error-800: oklch(0.44 0.16 27);
--color-error-900: oklch(0.40 0.13 26);
--color-error-950: oklch(0.26 0.09 26);
--color-neutral-50: oklch(0.98 0.00 248);
--color-neutral-100: oklch(0.97 0.01 248);
--color-neutral-200: oklch(0.93 0.01 256);
--color-neutral-300: oklch(0.87 0.02 253);
--color-neutral-400: oklch(0.71 0.04 257);
--color-neutral-500: oklch(0.55 0.04 257);
--color-neutral-600: oklch(0.45 0.04 257);
--color-neutral-700: oklch(0.37 0.04 257);
--color-neutral-800: oklch(0.28 0.04 260);
--color-neutral-900: oklch(0.28 0.04 260);
--color-neutral-950: oklch(0.28 0.04 260);
/* Spacing */
--spacing-0: 0;
--spacing-1: 0.25rem;
--spacing-2: 0.5rem;
--spacing-3: 0.75rem;
--spacing-4: 1rem;
--spacing-5: 1.25rem;
--spacing-6: 1.5rem;
--spacing-7: 1.75rem;
--spacing-8: 2rem;
--spacing-10: 2.5rem;
--spacing-12: 3rem;
--spacing-16: 4rem;
--spacing-20: 5rem;
--spacing-24: 6rem;
--spacing-32: 8rem;
--spacing-40: 10rem;
--spacing-48: 12rem;
--spacing-56: 14rem;
--spacing-64: 16rem;
/* Typography */
--font-family-sans: 'Inter Variable', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-family-mono: 'Fira Code Variable', 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, monospace;
--font-family-base: 'Inter Variable', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
--font-size-xs: 0.75rem;
--font-size-sm: 0.875rem;
--font-size-base: 1rem;
--font-size-lg: 1.125rem;
--font-size-xl: 1.25rem;
--font-size-2xl: 1.5rem;
--font-size-3xl: 1.875rem;
--font-size-4xl: 2.25rem;
--font-size-5xl: 3rem;
--font-size-6xl: 3.75rem;
--font-size-7xl: 4.5rem;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 21 KiB

225
static/images/logo-long.svg Normal file
View File

@ -0,0 +1,225 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
version="1.1"
width="314.98749"
height="28.8125"
id="svg3004"
xml:space="preserve"><metadata
id="metadata3010"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3008" /><g
transform="matrix(1.25,0,0,-1.25,0,28.8125)"
id="g3012"><g
transform="scale(0.1,0.1)"
id="g3014"><path
d="m 957.473,175.816 0,-4.296 -18.453,0 0,-48.614 -5.04,0 0,48.614 -18.378,0 0,4.296 41.871,0"
id="path3016"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 969.695,175.816 0,-22.968 31.425,0 0,22.968 5.04,0 0,-52.91 -5.04,0 0,25.637 -31.425,0 0,-25.637 -5.039,0 0,52.91 5.039,0"
id="path3018"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1055.58,175.816 0,-4.296 -31.49,0 0,-19.122 29.49,0 0,-4.293 -29.49,0 0,-20.898 31.87,0 0,-4.301 -36.91,0 0,52.91 36.53,0"
id="path3020"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1124.58,175.816 0,-4.296 -31.5,0 0,-19.122 29.49,0 0,-4.293 -29.49,0 0,-20.898 31.87,0 0,-4.301 -36.91,0 0,52.91 36.54,0"
id="path3022"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1139.76,175.816 30.83,-44.757 0.15,0 0,44.757 5.04,0 0,-52.91 -5.64,0 -30.82,44.754 -0.15,0 0,-44.754 -5.04,0 0,52.91 5.63,0"
id="path3024"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1188.15,175.816 17.19,-47.355 0.15,0 17.06,47.355 5.34,0 -19.65,-52.91 -5.86,0 -19.56,52.91 5.33,0"
id="path3026"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1235.15,122.906 5.043,0 0,52.9102 -5.043,0 0,-52.9102 z"
id="path3028"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1277.3,150.691 c 1.54,0 2.99,0.243 4.38,0.711 1.39,0.469 2.59,1.145 3.63,2.036 1.04,0.886 1.86,1.968 2.48,3.222 0.63,1.258 0.92,2.703 0.92,4.336 0,3.262 -0.94,5.824 -2.81,7.703 -1.88,1.875 -4.74,2.821 -8.6,2.821 l -18.82,0 0,-20.829 18.82,0 z m 0.38,25.125 c 2.16,0 4.23,-0.269 6.19,-0.816 1.95,-0.543 3.65,-1.367 5.1,-2.48 1.46,-1.118 2.62,-2.54 3.49,-4.297 0.86,-1.758 1.29,-3.821 1.29,-6.192 0,-3.359 -0.86,-6.273 -2.6,-8.742 -1.72,-2.473 -4.29,-4.055 -7.69,-4.746 l 0,-0.145 c 1.73,-0.25 3.16,-0.703 4.29,-1.367 1.14,-0.668 2.06,-1.527 2.78,-2.558 0.72,-1.035 1.23,-2.239 1.56,-3.594 0.31,-1.363 0.53,-2.832 0.62,-4.41 0.05,-0.887 0.1,-1.977 0.16,-3.262 0.05,-1.285 0.15,-2.582 0.29,-3.891 0.15,-1.312 0.38,-2.543 0.71,-3.711 0.32,-1.156 0.74,-2.054 1.3,-2.699 l -5.56,0 c -0.29,0.496 -0.54,1.098 -0.7,1.809 -0.18,0.723 -0.31,1.461 -0.37,2.234 -0.08,0.762 -0.14,1.512 -0.2,2.254 -0.05,0.742 -0.1,1.387 -0.14,1.926 -0.09,1.875 -0.26,3.742 -0.49,5.598 -0.22,1.851 -0.69,3.503 -1.4,4.961 -0.72,1.46 -1.76,2.632 -3.11,3.523 -1.36,0.887 -3.23,1.281 -5.6,1.191 l -19.12,0 0,-23.496 -5.03,0 0,52.91 24.23,0"
id="path3030"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1308.46,140.879 c 0.77,-2.793 1.95,-5.289 3.56,-7.484 1.61,-2.2 3.66,-3.965 6.19,-5.301 2.52,-1.336 5.54,-2.004 9.04,-2.004 3.51,0 6.51,0.668 9,2.004 2.5,1.336 4.55,3.101 6.15,5.301 1.6,2.195 2.8,4.691 3.56,7.484 0.77,2.789 1.15,5.613 1.15,8.48 0,2.914 -0.38,5.758 -1.15,8.52 -0.76,2.769 -1.96,5.25 -3.56,7.449 -1.6,2.199 -3.65,3.969 -6.15,5.297 -2.49,1.336 -5.49,2.008 -9,2.008 -3.5,0 -6.52,-0.672 -9.04,-2.008 -2.53,-1.328 -4.58,-3.098 -6.19,-5.297 -1.61,-2.199 -2.79,-4.68 -3.56,-7.449 -0.75,-2.762 -1.15,-5.606 -1.15,-8.52 0,-2.867 0.4,-5.691 1.15,-8.48 z m -4.63,18.934 c 1.04,3.308 2.6,6.23 4.67,8.777 2.08,2.547 4.68,4.57 7.82,6.078 3.14,1.504 6.79,2.254 10.93,2.254 4.15,0 7.78,-0.75 10.89,-2.254 3.11,-1.508 5.71,-3.531 7.78,-6.078 2.08,-2.547 3.64,-5.469 4.67,-8.777 1.05,-3.313 1.56,-6.797 1.56,-10.454 0,-3.656 -0.51,-7.136 -1.56,-10.449 -1.03,-3.308 -2.59,-6.226 -4.67,-8.738 -2.07,-2.52 -4.67,-4.531 -7.78,-6.043 -3.11,-1.5 -6.74,-2.266 -10.89,-2.266 -4.14,0 -7.79,0.766 -10.93,2.266 -3.14,1.512 -5.74,3.523 -7.82,6.043 -2.07,2.512 -3.63,5.43 -4.67,8.738 -1.04,3.313 -1.54,6.793 -1.54,10.449 0,3.657 0.5,7.141 1.54,10.454"
id="path3032"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1367.77,175.816 30.84,-44.757 0.15,0 0,44.757 5.05,0 0,-52.91 -5.64,0 -30.83,44.754 -0.15,0 0,-44.754 -5.04,0 0,52.91 5.62,0"
id="path3034"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1423.89,175.816 18.3,-46.386 18.22,46.386 7.41,0 0,-52.91 -5.03,0 0,45.723 -0.15,0 -18.09,-45.723 -4.74,0 -18.15,45.723 -0.15,0 0,-45.723 -5.04,0 0,52.91 7.42,0"
id="path3036"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1517.1,175.816 0,-4.296 -31.48,0 0,-19.122 29.48,0 0,-4.293 -29.48,0 0,-20.898 31.86,0 0,-4.301 -36.9,0 0,52.91 36.52,0"
id="path3038"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1532.29,175.816 30.82,-44.757 0.14,0 0,44.757 5.03,0 0,-52.91 -5.62,0 -30.81,44.754 -0.15,0 0,-44.754 -5.03,0 0,52.91 5.62,0"
id="path3040"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1617.34,175.816 0,-4.296 -18.44,0 0,-48.614 -5.04,0 0,48.614 -18.38,0 0,4.296 41.86,0"
id="path3042"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1647.8,143.656 -10.22,27.121 -10.6,-27.121 20.82,0 z m -7.18,32.16 20.74,-52.91 -5.4,0 -6.46,16.449 -24.07,0 -6.38,-16.449 -5.33,0 21.26,52.91 5.64,0"
id="path3044"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1673.44,175.816 0,-48.609 29.65,0 0,-4.301 -34.68,0 0,52.91 5.03,0"
id="path3046"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1769.74,165.254 c -1.02,1.605 -2.25,2.953 -3.71,4.043 -1.46,1.082 -3.06,1.914 -4.81,2.48 -1.76,0.567 -3.6,0.856 -5.52,0.856 -3.51,0 -6.53,-0.672 -9.05,-2.008 -2.52,-1.328 -4.58,-3.098 -6.18,-5.297 -1.61,-2.199 -2.8,-4.68 -3.56,-7.449 -0.77,-2.762 -1.15,-5.606 -1.15,-8.52 0,-2.867 0.38,-5.691 1.15,-8.48 0.76,-2.793 1.95,-5.289 3.56,-7.484 1.6,-2.2 3.66,-3.965 6.18,-5.301 2.52,-1.336 5.54,-2.004 9.05,-2.004 2.46,0 4.69,0.449 6.66,1.336 1.98,0.89 3.68,2.101 5.12,3.633 1.43,1.523 2.59,3.324 3.48,5.371 0.89,2.05 1.45,4.261 1.71,6.633 l 5.03,0 c -0.35,-3.258 -1.11,-6.204 -2.3,-8.821 -1.18,-2.617 -2.7,-4.84 -4.59,-6.664 -1.88,-1.824 -4.08,-3.238 -6.63,-4.223 -2.54,-0.992 -5.37,-1.492 -8.48,-1.492 -4.17,0 -7.81,0.766 -10.93,2.266 -3.15,1.512 -5.76,3.523 -7.83,6.043 -2.07,2.512 -3.62,5.43 -4.65,8.738 -1.04,3.313 -1.57,6.793 -1.57,10.449 0,3.657 0.53,7.141 1.57,10.454 1.03,3.308 2.58,6.23 4.65,8.777 2.07,2.547 4.68,4.57 7.83,6.078 3.12,1.504 6.76,2.254 10.93,2.254 2.52,0 4.97,-0.371 7.38,-1.106 2.39,-0.738 4.56,-1.843 6.51,-3.296 1.95,-1.461 3.58,-3.254 4.89,-5.372 1.3,-2.125 2.13,-4.574 2.47,-7.335 l -5.04,0 c -0.43,2.019 -1.16,3.839 -2.17,5.441"
id="path3048"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1791.01,140.879 c 0.76,-2.793 1.95,-5.289 3.56,-7.484 1.6,-2.2 3.66,-3.965 6.18,-5.301 2.52,-1.336 5.53,-2.004 9.04,-2.004 3.51,0 6.5,0.668 9.01,2.004 2.49,1.336 4.54,3.101 6.14,5.301 1.61,2.195 2.79,4.691 3.57,7.484 0.75,2.789 1.14,5.613 1.14,8.48 0,2.914 -0.39,5.758 -1.14,8.52 -0.78,2.769 -1.96,5.25 -3.57,7.449 -1.6,2.199 -3.65,3.969 -6.14,5.297 -2.51,1.336 -5.5,2.008 -9.01,2.008 -3.51,0 -6.52,-0.672 -9.04,-2.008 -2.52,-1.328 -4.58,-3.098 -6.18,-5.297 -1.61,-2.199 -2.8,-4.68 -3.56,-7.449 -0.78,-2.762 -1.16,-5.606 -1.16,-8.52 0,-2.867 0.38,-5.691 1.16,-8.48 z m -4.64,18.934 c 1.04,3.308 2.6,6.23 4.67,8.777 2.08,2.547 4.68,4.57 7.82,6.078 3.13,1.504 6.77,2.254 10.93,2.254 4.15,0 7.78,-0.75 10.89,-2.254 3.11,-1.508 5.72,-3.531 7.79,-6.078 2.07,-2.547 3.62,-5.469 4.66,-8.777 1.04,-3.313 1.56,-6.797 1.56,-10.454 0,-3.656 -0.52,-7.136 -1.56,-10.449 -1.04,-3.308 -2.59,-6.226 -4.66,-8.738 -2.07,-2.52 -4.68,-4.531 -7.79,-6.043 -3.11,-1.5 -6.74,-2.266 -10.89,-2.266 -4.16,0 -7.8,0.766 -10.93,2.266 -3.14,1.512 -5.74,3.523 -7.82,6.043 -2.07,2.512 -3.63,5.43 -4.67,8.738 -1.04,3.313 -1.56,6.793 -1.56,10.449 0,3.657 0.52,7.141 1.56,10.454"
id="path3050"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1850.33,175.816 30.82,-44.757 0.15,0 0,44.757 5.04,0 0,-52.91 -5.64,0 -30.82,44.754 -0.15,0 0,-44.754 -5.04,0 0,52.91 5.64,0"
id="path3052"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1935.39,175.816 0,-4.296 -18.45,0 0,-48.614 -5.04,0 0,48.614 -18.37,0 0,4.296 41.86,0"
id="path3054"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1965.86,143.656 -10.23,27.121 -10.6,-27.121 20.83,0 z m -7.21,32.16 20.76,-52.91 -5.41,0 -6.44,16.449 -24.08,0 -6.38,-16.449 -5.33,0 21.26,52.91 5.62,0"
id="path3056"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1993.71,175.816 18.3,-46.386 18.24,46.386 7.41,0 0,-52.91 -5.04,0 0,45.723 -0.15,0 -18.09,-45.723 -4.73,0 -18.16,45.723 -0.14,0 0,-45.723 -5.05,0 0,52.91 7.41,0"
id="path3058"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2050.78,122.906 5.0273,0 0,52.9102 -5.0273,0 0,-52.9102 z"
id="path3060"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2074.63,175.816 30.83,-44.757 0.15,0 0,44.757 5.04,0 0,-52.91 -5.63,0 -30.83,44.754 -0.15,0 0,-44.754 -5.04,0 0,52.91 5.63,0"
id="path3062"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2151.78,143.656 -10.24,27.121 -10.58,-27.121 20.82,0 z m -7.2,32.16 20.76,-52.91 -5.41,0 -6.45,16.449 -24.09,0 -6.36,-16.449 -5.34,0 21.27,52.91 5.62,0"
id="path3064"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2177.93,175.816 30.82,-44.757 0.16,0 0,44.757 5.05,0 0,-52.91 -5.64,0 -30.84,44.754 -0.14,0 0,-44.754 -5.04,0 0,52.91 5.63,0"
id="path3066"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2263.01,175.816 0,-4.296 -18.46,0 0,-48.614 -5.04,0 0,48.614 -18.38,0 0,4.296 41.88,0"
id="path3068"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 945.785,32.6602 c 1.875,0 3.656,0.1562 5.336,0.4804 1.676,0.3164 3.16,0.8985 4.445,1.7383 1.286,0.8477 2.301,1.9649 3.043,3.375 0.739,1.4102 1.11,3.1719 1.11,5.2969 0,3.4101 -1.203,5.9648 -3.602,7.668 -2.387,1.7031 -5.836,2.5585 -10.332,2.5585 l -17.344,0 0,-21.1171 17.344,0 z m 0,25.414 c 2.028,0 3.778,0.2344 5.262,0.711 1.48,0.4609 2.719,1.1015 3.711,1.9218 0.98,0.8125 1.726,1.7696 2.215,2.8555 0.5,1.082 0.742,2.2422 0.742,3.4766 0,6.6211 -3.977,9.9375 -11.93,9.9375 l -17.344,0 0,-18.9024 17.344,0 z m 0,23.1953 c 2.223,0 4.36,-0.2109 6.41,-0.6289 2.051,-0.4218 3.852,-1.1328 5.41,-2.1484 1.559,-1.0156 2.805,-2.3438 3.743,-4 0.937,-1.6563 1.406,-3.7227 1.406,-6.1914 0,-1.3867 -0.219,-2.7305 -0.668,-4.0391 -0.445,-1.3086 -1.074,-2.4961 -1.887,-3.5547 -0.808,-1.0625 -1.781,-1.9687 -2.89,-2.7031 -1.114,-0.7422 -2.36,-1.2617 -3.739,-1.5586 l 0,-0.1523 c 3.403,-0.4453 6.121,-1.8321 8.145,-4.1797 2.031,-2.3477 3.043,-5.25 3.043,-8.711 0,-0.8398 -0.074,-1.7851 -0.227,-2.8515 -0.144,-1.0625 -0.437,-2.1524 -0.886,-3.2617 -0.446,-1.1133 -1.086,-2.2149 -1.93,-3.2969 -0.836,-1.0859 -1.957,-2.0391 -3.363,-2.8516 -1.414,-0.8203 -3.141,-1.4883 -5.192,-2.0039 -2.051,-0.5195 -4.508,-0.7812 -7.375,-0.7812 l -22.379,0 0,52.914 22.379,0"
id="path3070"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 975.426,28.3555 5.04297,0 0,52.9141 -5.04297,0 0,-52.9141 z"
id="path3072"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 997.105,46.3281 c 0.762,-2.789 1.95,-5.2812 3.555,-7.4804 1.6,-2.1993 3.67,-3.9688 6.18,-5.2969 2.53,-1.3399 5.54,-2.0039 9.04,-2.0039 3.51,0 6.52,0.664 9.01,2.0039 2.5,1.3281 4.54,3.0976 6.15,5.2969 1.61,2.1992 2.79,4.6914 3.55,7.4804 0.77,2.793 1.16,5.6211 1.16,8.4844 0,2.918 -0.39,5.7578 -1.16,8.5273 -0.76,2.7618 -1.94,5.2461 -3.55,7.4454 -1.61,2.2031 -3.65,3.9648 -6.15,5.3007 -2.49,1.3321 -5.5,2 -9.01,2 -3.5,0 -6.51,-0.6679 -9.04,-2 -2.51,-1.3359 -4.58,-3.0976 -6.18,-5.3007 -1.605,-2.1993 -2.793,-4.6836 -3.555,-7.4454 -0.773,-2.7695 -1.152,-5.6093 -1.152,-8.5273 0,-2.8633 0.379,-5.6914 1.152,-8.4844 z m -4.632,18.9336 c 1.035,3.3125 2.59,6.2383 4.668,8.7813 2.074,2.5429 4.679,4.5742 7.819,6.0781 3.13,1.5078 6.77,2.2578 10.92,2.2578 4.15,0 7.79,-0.75 10.9,-2.2578 3.11,-1.5039 5.71,-3.5352 7.78,-6.0781 2.08,-2.543 3.63,-5.4688 4.67,-8.7813 1.04,-3.3047 1.56,-6.789 1.56,-10.4492 0,-3.6602 -0.52,-7.1406 -1.56,-10.4414 -1.04,-3.3164 -2.59,-6.2305 -4.67,-8.7461 -2.07,-2.5195 -4.67,-4.5312 -7.78,-6.0391 -3.11,-1.5039 -6.75,-2.2656 -10.9,-2.2656 -4.15,0 -7.79,0.7617 -10.92,2.2656 -3.14,1.5079 -5.745,3.5196 -7.819,6.0391 -2.078,2.5156 -3.633,5.4297 -4.668,8.7461 -1.035,3.3008 -1.559,6.7812 -1.559,10.4414 0,3.6602 0.524,7.1445 1.559,10.4492"
id="path3074"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1087.1,81.2695 0,-4.2929 -18.45,0 0,-48.6211 -5.04,0 0,48.6211 -18.38,0 0,4.2929 41.87,0"
id="path3076"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1118.15,56.1484 c 1.53,0 2.99,0.2383 4.37,0.7071 1.39,0.4687 2.6,1.1484 3.63,2.0351 1.04,0.8907 1.86,1.9688 2.49,3.2227 0.61,1.2617 0.93,2.7109 0.93,4.332 0,3.2656 -0.95,5.836 -2.82,7.7031 -1.88,1.8829 -4.75,2.8282 -8.6,2.8282 l -18.82,0 0,-20.8282 18.82,0 z m 0.37,25.1211 c 2.17,0 4.23,-0.2695 6.19,-0.8203 1.95,-0.539 3.65,-1.3633 5.11,-2.4765 1.46,-1.1133 2.62,-2.543 3.49,-4.3008 0.86,-1.7578 1.29,-3.8125 1.29,-6.1875 0,-3.3594 -0.86,-6.2696 -2.59,-8.7461 -1.73,-2.4688 -4.3,-4.0469 -7.71,-4.7383 l 0,-0.1484 c 1.73,-0.25 3.16,-0.7032 4.3,-1.3672 1.14,-0.668 2.06,-1.5235 2.78,-2.5586 0.71,-1.0391 1.23,-2.2344 1.55,-3.5977 0.33,-1.3593 0.54,-2.8281 0.63,-4.4062 0.05,-0.8867 0.1,-1.9766 0.15,-3.2656 0.05,-1.2852 0.15,-2.5782 0.3,-3.8868 0.15,-1.3125 0.38,-2.5468 0.71,-3.707 0.31,-1.1562 0.74,-2.0586 1.29,-2.707 l -5.56,0 c -0.29,0.5 -0.53,1.1015 -0.7,1.8203 -0.17,0.7187 -0.3,1.4531 -0.37,2.2265 -0.07,0.7618 -0.14,1.5118 -0.19,2.25 -0.04,0.7461 -0.1,1.3946 -0.15,1.9297 -0.1,1.875 -0.26,3.7461 -0.48,5.6016 -0.22,1.8555 -0.69,3.5 -1.41,4.9609 -0.71,1.4532 -1.75,2.6289 -3.11,3.5235 -1.36,0.8906 -3.22,1.2812 -5.59,1.1875 l -19.12,0 0,-23.5 -5.04,0 0,52.914 24.23,0"
id="path3078"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1174.25,49.1055 -10.23,27.125 -10.6,-27.125 20.83,0 z m -7.19,32.164 20.75,-52.914 -5.41,0 -6.45,16.457 -24.08,0 -6.38,-16.457 -5.33,0 21.27,52.914 5.63,0"
id="path3080"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1200.41,81.2695 30.83,-44.7617 0.15,0 0,44.7617 5.04,0 0,-52.914 -5.63,0 -30.84,44.7578 -0.15,0 0,-44.7578 -5.04,0 0,52.914 5.64,0"
id="path3082"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1252.87,38.9609 c 0.9,-1.8281 2.12,-3.289 3.67,-4.375 1.57,-1.0898 3.4,-1.8671 5.52,-2.3359 2.12,-0.4727 4.4,-0.7031 6.83,-0.7031 1.37,0 2.89,0.1914 4.52,0.5976 1.62,0.3907 3.14,1.0196 4.55,1.8828 1.42,0.8711 2.58,1.9727 3.52,3.336 0.94,1.3515 1.41,3.0039 1.41,4.9258 0,1.4843 -0.33,2.7695 -1.01,3.8593 -0.66,1.086 -1.53,1.9961 -2.58,2.7383 -1.07,0.7422 -2.24,1.3438 -3.53,1.8164 -1.28,0.4688 -2.55,0.8555 -3.78,1.1524 l -11.78,2.8789 c -1.54,0.4062 -3.02,0.8906 -4.49,1.4922 -1.44,0.5898 -2.72,1.375 -3.81,2.3672 -1.09,0.9843 -1.95,2.2031 -2.63,3.6289 -0.67,1.4336 -1,3.1875 -1,5.2617 0,1.289 0.25,2.7929 0.74,4.5234 0.49,1.7266 1.42,3.3594 2.79,4.8906 1.35,1.5274 3.21,2.8243 5.59,3.8907 2.37,1.0625 5.4,1.5898 9.1,1.5898 2.62,0 5.13,-0.3398 7.49,-1.0351 2.38,-0.6915 4.47,-1.7305 6.22,-3.1133 1.8,-1.3789 3.21,-3.0977 4.26,-5.1446 1.08,-2.0546 1.6,-4.4453 1.6,-7.1601 l -5.03,0 c -0.1,2.0351 -0.56,3.7969 -1.37,5.3047 -0.82,1.5039 -1.88,2.7617 -3.19,3.7773 -1.3,1.0117 -2.81,1.7852 -4.53,2.3008 -1.7,0.5195 -3.49,0.7773 -5.37,0.7773 -1.72,0 -3.39,-0.1914 -5,-0.5586 -1.61,-0.371 -3.01,-0.9609 -4.22,-1.7812 -1.21,-0.8164 -2.18,-1.8906 -2.93,-3.2227 -0.74,-1.332 -1.11,-2.9882 -1.11,-4.9648 0,-1.2305 0.22,-2.3086 0.64,-3.2227 0.42,-0.914 0.98,-1.6953 1.72,-2.332 0.75,-0.6484 1.61,-1.1601 2.56,-1.5547 0.98,-0.4023 1.99,-0.7226 3.08,-0.9687 l 12.9,-3.1875 c 1.88,-0.4883 3.64,-1.0938 5.3,-1.8164 1.65,-0.711 3.11,-1.6055 4.37,-2.6602 1.27,-1.0625 2.24,-2.3633 2.97,-3.8945 0.71,-1.5313 1.07,-3.3867 1.07,-5.5586 0,-0.5899 -0.07,-1.3828 -0.2,-2.3672 -0.11,-0.9922 -0.41,-2.0313 -0.87,-3.1523 -0.47,-1.1133 -1.14,-2.2344 -2,-3.3711 -0.87,-1.1368 -2.06,-2.1602 -3.56,-3.0782 -1.51,-0.9062 -3.37,-1.6562 -5.6,-2.2226 -2.22,-0.5586 -4.88,-0.8516 -8,-0.8516 -3.11,0 -6,0.3594 -8.68,1.0781 -2.65,0.7188 -4.94,1.8125 -6.81,3.2969 -1.88,1.4844 -3.32,3.3789 -4.34,5.707 -1,2.3204 -1.43,5.1055 -1.3,8.375 l 5.05,0 c -0.06,-2.7148 0.37,-4.9921 1.25,-6.8164"
id="path3084"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1331.72,81.2695 0,-4.2929 -28.53,0 0,-19.1211 25.35,0 0,-4.3008 -25.35,0 0,-25.1992 -5.04,0 0,52.914 33.57,0"
id="path3086"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1343.54,46.3281 c 0.78,-2.789 1.95,-5.2812 3.55,-7.4804 1.6,-2.1993 3.68,-3.9688 6.2,-5.2969 2.51,-1.3399 5.53,-2.0039 9.03,-2.0039 3.51,0 6.51,0.664 9.01,2.0039 2.5,1.3281 4.55,3.0976 6.15,5.2969 1.6,2.1992 2.78,4.6914 3.56,7.4804 0.76,2.793 1.15,5.6211 1.15,8.4844 0,2.918 -0.39,5.7578 -1.15,8.5273 -0.78,2.7618 -1.96,5.2461 -3.56,7.4454 -1.6,2.2031 -3.65,3.9648 -6.15,5.3007 -2.5,1.3321 -5.5,2 -9.01,2 -3.5,0 -6.52,-0.6679 -9.03,-2 -2.52,-1.3359 -4.6,-3.0976 -6.2,-5.3007 -1.6,-2.1993 -2.77,-4.6836 -3.55,-7.4454 -0.77,-2.7695 -1.14,-5.6093 -1.14,-8.5273 0,-2.8633 0.37,-5.6914 1.14,-8.4844 z m -4.63,18.9336 c 1.03,3.3125 2.59,6.2383 4.66,8.7813 2.09,2.5429 4.69,4.5742 7.82,6.0781 3.14,1.5078 6.79,2.2578 10.93,2.2578 4.15,0 7.8,-0.75 10.9,-2.2578 3.11,-1.5039 5.7,-3.5352 7.78,-6.0781 2.09,-2.543 3.63,-5.4688 4.66,-8.7813 1.04,-3.3047 1.57,-6.789 1.57,-10.4492 0,-3.6602 -0.53,-7.1406 -1.57,-10.4414 -1.03,-3.3164 -2.57,-6.2305 -4.66,-8.7461 -2.08,-2.5195 -4.67,-4.5312 -7.78,-6.0391 -3.1,-1.5039 -6.75,-2.2656 -10.9,-2.2656 -4.14,0 -7.79,0.7617 -10.93,2.2656 -3.13,1.5079 -5.73,3.5196 -7.82,6.0391 -2.07,2.5156 -3.63,5.4297 -4.66,8.7461 -1.04,3.3008 -1.56,6.7812 -1.56,10.4414 0,3.6602 0.52,7.1445 1.56,10.4492"
id="path3088"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1421.16,56.1484 c 1.54,0 3,0.2383 4.39,0.7071 1.37,0.4687 2.58,1.1484 3.62,2.0351 1.04,0.8907 1.87,1.9688 2.49,3.2227 0.61,1.2617 0.91,2.7109 0.91,4.332 0,3.2656 -0.93,5.836 -2.81,7.7031 -1.88,1.8829 -4.73,2.8282 -8.6,2.8282 l -18.83,0 0,-20.8282 18.83,0 z m 0.37,25.1211 c 2.18,0 4.24,-0.2695 6.18,-0.8203 1.96,-0.539 3.67,-1.3633 5.12,-2.4765 1.47,-1.1133 2.63,-2.543 3.49,-4.3008 0.86,-1.7578 1.3,-3.8125 1.3,-6.1875 0,-3.3594 -0.87,-6.2696 -2.6,-8.7461 -1.72,-2.4688 -4.3,-4.0469 -7.71,-4.7383 l 0,-0.1484 c 1.74,-0.25 3.17,-0.7032 4.3,-1.3672 1.13,-0.668 2.06,-1.5235 2.78,-2.5586 0.72,-1.0391 1.24,-2.2344 1.56,-3.5977 0.32,-1.3593 0.52,-2.8281 0.63,-4.4062 0.05,-0.8867 0.1,-1.9766 0.15,-3.2656 0.05,-1.2852 0.15,-2.5782 0.29,-3.8868 0.16,-1.3125 0.38,-2.5468 0.7,-3.707 0.33,-1.1562 0.76,-2.0586 1.3,-2.707 l -5.55,0 c -0.31,0.5 -0.54,1.1015 -0.71,1.8203 -0.17,0.7187 -0.29,1.4531 -0.37,2.2265 -0.08,0.7618 -0.13,1.5118 -0.18,2.25 -0.05,0.7461 -0.1,1.3946 -0.15,1.9297 -0.1,1.875 -0.25,3.7461 -0.48,5.6016 -0.22,1.8555 -0.7,3.5 -1.41,4.9609 -0.73,1.4532 -1.76,2.6289 -3.12,3.5235 -1.36,0.8906 -3.21,1.2812 -5.59,1.1875 l -19.13,0 0,-23.5 -5.03,0 0,52.914 24.23,0"
id="path3090"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1456.21,81.2695 18.3,-46.3906 18.24,46.3906 7.41,0 0,-52.914 -5.04,0 0,45.7265 -0.15,0 -18.08,-45.7265 -4.74,0 -18.17,45.7265 -0.13,0 0,-45.7265 -5.05,0 0,52.914 7.41,0"
id="path3092"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1541.21,49.1055 -10.22,27.125 -10.6,-27.125 20.82,0 z m -7.19,32.164 20.74,-52.914 -5.42,0 -6.42,16.457 -24.08,0 -6.38,-16.457 -5.33,0 21.27,52.914 5.62,0"
id="path3094"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1593,81.2695 0,-4.2929 -18.47,0 0,-48.6211 -5.04,0 0,48.6211 -18.37,0 0,4.2929 41.88,0"
id="path3096"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1600.55,28.3555 5.0391,0 0,52.9141 -5.0391,0 0,-52.9141 z"
id="path3098"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1622.23,46.3281 c 0.76,-2.789 1.94,-5.2812 3.55,-7.4804 1.6,-2.1993 3.67,-3.9688 6.19,-5.2969 2.52,-1.3399 5.53,-2.0039 9.04,-2.0039 3.5,0 6.5,0.664 9.01,2.0039 2.48,1.3281 4.54,3.0976 6.14,5.2969 1.61,2.1992 2.8,4.6914 3.56,7.4804 0.76,2.793 1.15,5.6211 1.15,8.4844 0,2.918 -0.39,5.7578 -1.15,8.5273 -0.76,2.7618 -1.95,5.2461 -3.56,7.4454 -1.6,2.2031 -3.66,3.9648 -6.14,5.3007 -2.51,1.3321 -5.51,2 -9.01,2 -3.51,0 -6.52,-0.6679 -9.04,-2 -2.52,-1.3359 -4.59,-3.0976 -6.19,-5.3007 -1.61,-2.1993 -2.79,-4.6836 -3.55,-7.4454 -0.77,-2.7695 -1.16,-5.6093 -1.16,-8.5273 0,-2.8633 0.39,-5.6914 1.16,-8.4844 z m -4.64,18.9336 c 1.03,3.3125 2.6,6.2383 4.68,8.7813 2.07,2.5429 4.67,4.5742 7.8,6.0781 3.14,1.5078 6.79,2.2578 10.94,2.2578 4.16,0 7.78,-0.75 10.88,-2.2578 3.13,-1.5039 5.72,-3.5352 7.8,-6.0781 2.07,-2.543 3.62,-5.4688 4.66,-8.7813 1.03,-3.3047 1.55,-6.789 1.55,-10.4492 0,-3.6602 -0.52,-7.1406 -1.55,-10.4414 -1.04,-3.3164 -2.59,-6.2305 -4.66,-8.7461 -2.08,-2.5195 -4.67,-4.5312 -7.8,-6.0391 -3.1,-1.5039 -6.72,-2.2656 -10.88,-2.2656 -4.15,0 -7.8,0.7617 -10.94,2.2656 -3.13,1.5079 -5.73,3.5196 -7.8,6.0391 -2.08,2.5156 -3.65,5.4297 -4.68,8.7461 -1.03,3.3008 -1.55,6.7812 -1.55,10.4414 0,3.6602 0.52,7.1445 1.55,10.4492"
id="path3100"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1681.55,81.2695 30.82,-44.7617 0.15,0 0,44.7617 5.04,0 0,-52.914 -5.64,0 -30.82,44.7578 -0.15,0 0,-44.7578 -5.03,0 0,52.914 5.63,0"
id="path3102"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1775.58,55.332 c 3.51,0 6.35,0.8946 8.52,2.6719 2.17,1.7774 3.26,4.4922 3.26,8.1484 0,3.6602 -1.09,6.3711 -3.26,8.1485 -2.17,1.7851 -5.01,2.6758 -8.52,2.6758 l -17.35,0 0,-21.6446 17.35,0 z m 1.12,25.9375 c 2.36,0 4.51,-0.3359 6.43,-0.9961 1.94,-0.6679 3.58,-1.6562 4.99,-2.9648 1.37,-1.3125 2.43,-2.9063 3.17,-4.7852 0.74,-1.875 1.11,-4.0039 1.11,-6.3711 0,-2.3671 -0.37,-4.4921 -1.11,-6.371 -0.74,-1.8829 -1.8,-3.4766 -3.17,-4.7852 -1.41,-1.3047 -3.05,-2.293 -4.99,-2.9609 -1.92,-0.6641 -4.07,-0.9961 -6.43,-0.9961 l -18.47,0 0,-22.6836 -5.04,0 0,52.914 23.51,0"
id="path3104"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1824.93,49.1055 -10.22,27.125 -10.6,-27.125 20.82,0 z m -7.19,32.164 20.76,-52.914 -5.41,0 -6.45,16.457 -24.09,0 -6.38,-16.457 -5.33,0 21.28,52.914 5.62,0"
id="path3106"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1876.73,81.2695 0,-4.2929 -18.45,0 0,-48.6211 -5.04,0 0,48.6211 -18.38,0 0,4.2929 41.87,0"
id="path3108"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1888.96,81.2695 0,-22.9687 31.41,0 0,22.9687 5.04,0 0,-52.914 -5.04,0 0,25.6445 -31.41,0 0,-25.6445 -5.04,0 0,52.914 5.04,0"
id="path3110"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 1938.38,81.2695 12.01,-46.3125 0.15,0 12.9,46.3125 6.3,0 12.96,-46.3125 0.15,0 12.07,46.3125 5.04,0 -14.59,-52.914 -5.34,0 -13.42,47.3515 -0.14,0 -13.34,-47.3515 -5.48,0 -14.67,52.914 5.4,0"
id="path3112"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2034.73,49.1055 -10.24,27.125 -10.59,-27.125 20.83,0 z m -7.2,32.164 20.75,-52.914 -5.41,0 -6.44,16.457 -24.09,0 -6.37,-16.457 -5.34,0 21.27,52.914 5.63,0"
id="path3114"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2043.84,81.2695 5.93,0 17.41,-26.8281 17.33,26.8281 6.01,0 -20.9,-31.125 0,-21.789 -5.04,0 0,21.789 -20.74,31.125"
id="path3116"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2144.01,56.1484 c 1.55,0 3,0.2383 4.38,0.7071 1.39,0.4687 2.6,1.1484 3.63,2.0351 1.05,0.8907 1.88,1.9688 2.48,3.2227 0.62,1.2617 0.93,2.7109 0.93,4.332 0,3.2656 -0.94,5.836 -2.81,7.7031 -1.89,1.8829 -4.75,2.8282 -8.61,2.8282 l -18.81,0 0,-20.8282 18.81,0 z m 0.38,25.1211 c 2.18,0 4.23,-0.2695 6.19,-0.8203 1.95,-0.539 3.66,-1.3633 5.1,-2.4765 1.47,-1.1133 2.63,-2.543 3.5,-4.3008 0.86,-1.7578 1.29,-3.8125 1.29,-6.1875 0,-3.3594 -0.86,-6.2696 -2.59,-8.7461 -1.73,-2.4688 -4.3,-4.0469 -7.7,-4.7383 l 0,-0.1484 c 1.72,-0.25 3.15,-0.7032 4.28,-1.3672 1.15,-0.668 2.07,-1.5235 2.79,-2.5586 0.72,-1.0391 1.24,-2.2344 1.55,-3.5977 0.32,-1.3593 0.54,-2.8281 0.63,-4.4062 0.05,-0.8867 0.1,-1.9766 0.15,-3.2656 0.05,-1.2852 0.15,-2.5782 0.29,-3.8868 0.15,-1.3125 0.39,-2.5468 0.71,-3.707 0.33,-1.1562 0.76,-2.0586 1.3,-2.707 l -5.55,0 c -0.3,0.5 -0.54,1.1015 -0.71,1.8203 -0.17,0.7187 -0.3,1.4531 -0.38,2.2265 -0.07,0.7618 -0.13,1.5118 -0.18,2.25 -0.04,0.7461 -0.1,1.3946 -0.15,1.9297 -0.1,1.875 -0.26,3.7461 -0.49,5.6016 -0.22,1.8555 -0.68,3.5 -1.39,4.9609 -0.73,1.4532 -1.76,2.6289 -3.13,3.5235 -1.35,0.8906 -3.21,1.2812 -5.59,1.1875 l -19.11,0 0,-23.5 -5.04,0 0,52.914 24.23,0"
id="path3118"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2208.34,81.2695 0,-4.2929 -31.49,0 0,-19.1211 29.49,0 0,-4.3008 -29.49,0 0,-20.8945 31.87,0 0,-4.3047 -36.91,0 0,52.914 36.53,0"
id="path3120"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2221.6,38.9609 c 0.9,-1.8281 2.13,-3.289 3.66,-4.375 1.58,-1.0898 3.4,-1.8671 5.53,-2.3359 2.12,-0.4727 4.41,-0.7031 6.82,-0.7031 1.38,0 2.9,0.1914 4.53,0.5976 1.62,0.3907 3.13,1.0196 4.55,1.8828 1.41,0.8711 2.58,1.9727 3.52,3.336 0.94,1.3515 1.4,3.0039 1.4,4.9258 0,1.4843 -0.32,2.7695 -0.99,3.8593 -0.66,1.086 -1.55,1.9961 -2.59,2.7383 -1.06,0.7422 -2.24,1.3438 -3.53,1.8164 -1.28,0.4688 -2.54,0.8555 -3.78,1.1524 l -11.77,2.8789 c -1.55,0.4062 -3.03,0.8906 -4.5,1.4922 -1.45,0.5898 -2.73,1.375 -3.82,2.3672 -1.08,0.9843 -1.95,2.2031 -2.62,3.6289 -0.66,1.4336 -1,3.1875 -1,5.2617 0,1.289 0.24,2.7929 0.74,4.5234 0.5,1.7266 1.42,3.3594 2.78,4.8906 1.36,1.5274 3.23,2.8243 5.59,3.8907 2.38,1.0625 5.41,1.5898 9.12,1.5898 2.61,0 5.11,-0.3398 7.48,-1.0351 2.37,-0.6915 4.46,-1.7305 6.24,-3.1133 1.77,-1.3789 3.19,-3.0977 4.24,-5.1446 1.08,-2.0546 1.6,-4.4453 1.6,-7.1601 l -5.03,0 c -0.1,2.0351 -0.55,3.7969 -1.38,5.3047 -0.81,1.5039 -1.87,2.7617 -3.18,3.7773 -1.31,1.0117 -2.82,1.7852 -4.53,2.3008 -1.71,0.5195 -3.48,0.7773 -5.37,0.7773 -1.73,0 -3.4,-0.1914 -5.01,-0.5586 -1.6,-0.371 -3,-0.9609 -4.21,-1.7812 -1.21,-0.8164 -2.19,-1.8906 -2.94,-3.2227 -0.74,-1.332 -1.1,-2.9882 -1.1,-4.9648 0,-1.2305 0.21,-2.3086 0.63,-3.2227 0.43,-0.914 0.99,-1.6953 1.73,-2.332 0.75,-0.6484 1.62,-1.1601 2.56,-1.5547 0.97,-0.4023 1.99,-0.7226 3.08,-0.9687 l 12.9,-3.1875 c 1.87,-0.4883 3.64,-1.0938 5.3,-1.8164 1.65,-0.711 3.11,-1.6055 4.37,-2.6602 1.26,-1.0625 2.24,-2.3633 2.97,-3.8945 0.71,-1.5313 1.07,-3.3867 1.07,-5.5586 0,-0.5899 -0.07,-1.3828 -0.2,-2.3672 -0.11,-0.9922 -0.41,-2.0313 -0.87,-3.1523 -0.48,-1.1133 -1.15,-2.2344 -2.01,-3.3711 -0.86,-1.1368 -2.05,-2.1602 -3.55,-3.0782 -1.5,-0.9062 -3.37,-1.6562 -5.6,-2.2226 -2.22,-0.5586 -4.89,-0.8516 -8.01,-0.8516 -3.11,0 -5.99,0.3594 -8.67,1.0781 -2.66,0.7188 -4.94,1.8125 -6.8,3.2969 -1.89,1.4844 -3.33,3.3789 -4.36,5.707 -0.99,2.3204 -1.43,5.1055 -1.29,8.375 l 5.04,0 c -0.05,-2.7148 0.38,-4.9921 1.26,-6.8164"
id="path3122"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2270.24,46.3281 c 0.79,-2.789 1.96,-5.2812 3.57,-7.4804 1.6,-2.1993 3.67,-3.9688 6.18,-5.2969 2.52,-1.3399 5.54,-2.0039 9.04,-2.0039 3.51,0 6.52,0.664 9.02,2.0039 2.49,1.3281 4.54,3.0976 6.15,5.2969 1.6,2.1992 2.77,4.6914 3.55,7.4804 0.77,2.793 1.15,5.6211 1.15,8.4844 0,2.918 -0.38,5.7578 -1.15,8.5273 -0.78,2.7618 -1.95,5.2461 -3.55,7.4454 -1.61,2.2031 -3.66,3.9648 -6.15,5.3007 -2.5,1.3321 -5.51,2 -9.02,2 -3.5,0 -6.52,-0.6679 -9.04,-2 -2.51,-1.3359 -4.58,-3.0976 -6.18,-5.3007 -1.61,-2.1993 -2.78,-4.6836 -3.57,-7.4454 -0.75,-2.7695 -1.13,-5.6093 -1.13,-8.5273 0,-2.8633 0.38,-5.6914 1.13,-8.4844 z m -4.62,18.9336 c 1.04,3.3125 2.59,6.2383 4.66,8.7813 2.09,2.5429 4.68,4.5742 7.83,6.0781 3.14,1.5078 6.78,2.2578 10.92,2.2578 4.15,0 7.8,-0.75 10.9,-2.2578 3.11,-1.5039 5.7,-3.5352 7.79,-6.0781 2.07,-2.543 3.63,-5.4688 4.66,-8.7813 1.04,-3.3047 1.56,-6.789 1.56,-10.4492 0,-3.6602 -0.52,-7.1406 -1.56,-10.4414 -1.03,-3.3164 -2.59,-6.2305 -4.66,-8.7461 -2.09,-2.5195 -4.68,-4.5312 -7.79,-6.0391 -3.1,-1.5039 -6.75,-2.2656 -10.9,-2.2656 -4.14,0 -7.78,0.7617 -10.92,2.2656 -3.15,1.5079 -5.74,3.5196 -7.83,6.0391 -2.07,2.5156 -3.62,5.4297 -4.66,8.7461 -1.04,3.3008 -1.56,6.7812 -1.56,10.4414 0,3.6602 0.52,7.1445 1.56,10.4492"
id="path3124"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2328.53,81.2695 0,-32.7539 c 0,-3.0625 0.35,-5.664 1.03,-7.8242 0.69,-2.1406 1.71,-3.8984 3.05,-5.2578 1.33,-1.3555 2.97,-2.3477 4.88,-2.9648 1.93,-0.6172 4.11,-0.9219 6.52,-0.9219 2.47,0 4.67,0.3047 6.61,0.9219 1.93,0.6171 3.55,1.6093 4.88,2.9648 1.34,1.3594 2.35,3.1172 3.04,5.2578 0.69,2.1602 1.04,4.7617 1.04,7.8242 l 0,32.7539 5.04,0 0,-33.8672 c 0,-2.7148 -0.38,-5.2968 -1.14,-7.7382 -0.77,-2.4493 -1.99,-4.5899 -3.65,-6.418 -1.66,-1.8242 -3.76,-3.2695 -6.36,-4.332 -2.6,-1.0586 -5.74,-1.5938 -9.46,-1.5938 -3.65,0 -6.77,0.5352 -9.37,1.5938 -2.59,1.0625 -4.71,2.5078 -6.37,4.332 -1.66,1.8281 -2.87,3.9687 -3.63,6.418 -0.77,2.4414 -1.13,5.0234 -1.13,7.7382 l 0,33.8672 5.02,0"
id="path3126"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2400.87,56.1484 c 1.51,0 2.99,0.2383 4.36,0.7071 1.39,0.4687 2.59,1.1484 3.63,2.0351 1.03,0.8907 1.86,1.9688 2.49,3.2227 0.62,1.2617 0.92,2.7109 0.92,4.332 0,3.2656 -0.94,5.836 -2.82,7.7031 -1.86,1.8829 -4.73,2.8282 -8.58,2.8282 l -18.83,0 0,-20.8282 18.83,0 z m 0.36,25.1211 c 2.18,0 4.23,-0.2695 6.2,-0.8203 1.94,-0.539 3.64,-1.3633 5.11,-2.4765 1.45,-1.1133 2.61,-2.543 3.47,-4.3008 0.88,-1.7578 1.29,-3.8125 1.29,-6.1875 0,-3.3594 -0.85,-6.2696 -2.58,-8.7461 -1.73,-2.4688 -4.29,-4.0469 -7.71,-4.7383 l 0,-0.1484 c 1.73,-0.25 3.17,-0.7032 4.31,-1.3672 1.11,-0.668 2.05,-1.5235 2.77,-2.5586 0.71,-1.0391 1.23,-2.2344 1.55,-3.5977 0.32,-1.3593 0.52,-2.8281 0.63,-4.4062 0.05,-0.8867 0.11,-1.9766 0.16,-3.2656 0.04,-1.2852 0.15,-2.5782 0.29,-3.8868 0.14,-1.3125 0.38,-2.5468 0.7,-3.707 0.31,-1.1562 0.75,-2.0586 1.3,-2.707 l -5.56,0 c -0.29,0.5 -0.53,1.1015 -0.71,1.8203 -0.16,0.7187 -0.29,1.4531 -0.36,2.2265 -0.09,0.7618 -0.14,1.5118 -0.19,2.25 -0.05,0.7461 -0.1,1.3946 -0.15,1.9297 -0.09,1.875 -0.27,3.7461 -0.47,5.6016 -0.23,1.8555 -0.69,3.5 -1.42,4.9609 -0.71,1.4532 -1.75,2.6289 -3.1,3.5235 -1.37,0.8906 -3.23,1.2812 -5.6,1.1875 l -19.12,0 0,-23.5 -5.04,0 0,52.914 24.23,0"
id="path3128"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2465.16,70.7109 c -1.02,1.6094 -2.27,2.9493 -3.71,4.0391 -1.47,1.0859 -3.08,1.9141 -4.82,2.4844 -1.75,0.5625 -3.59,0.8515 -5.53,0.8515 -3.5,0 -6.51,-0.6679 -9.03,-2 -2.52,-1.3359 -4.6,-3.0976 -6.18,-5.3007 -1.62,-2.1993 -2.8,-4.6836 -3.58,-7.4454 -0.76,-2.7695 -1.14,-5.6093 -1.14,-8.5273 0,-2.8633 0.38,-5.6914 1.14,-8.4844 0.78,-2.789 1.96,-5.2812 3.58,-7.4804 1.58,-2.1993 3.66,-3.9688 6.18,-5.2969 2.52,-1.3399 5.53,-2.0039 9.03,-2.0039 2.47,0 4.7,0.4453 6.66,1.332 2,0.8906 3.7,2.1016 5.13,3.6289 1.44,1.5274 2.6,3.3281 3.48,5.3711 0.9,2.0508 1.46,4.2695 1.71,6.6367 l 5.04,0 c -0.35,-3.2578 -1.13,-6.1953 -2.3,-8.8203 -1.19,-2.6172 -2.72,-4.8359 -4.59,-6.668 -1.88,-1.8281 -4.09,-3.2304 -6.63,-4.2226 -2.56,-0.9844 -5.37,-1.4844 -8.5,-1.4844 -4.15,0 -7.79,0.7617 -10.93,2.2656 -3.13,1.5079 -5.74,3.5196 -7.83,6.0391 -2.05,2.5156 -3.62,5.4297 -4.65,8.7461 -1.04,3.3008 -1.56,6.7812 -1.56,10.4414 0,3.6602 0.52,7.1445 1.56,10.4492 1.03,3.3125 2.6,6.2383 4.65,8.7813 2.09,2.5429 4.7,4.5742 7.83,6.0781 3.14,1.5078 6.78,2.2578 10.93,2.2578 2.52,0 4.97,-0.3711 7.38,-1.1094 2.39,-0.7382 4.57,-1.8398 6.52,-3.2968 1.95,-1.461 3.57,-3.25 4.89,-5.3711 1.31,-2.1289 2.14,-4.5703 2.48,-7.3399 l -5.04,0 c -0.45,2.0274 -1.17,3.8399 -2.17,5.4492"
id="path3130"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 2519.59,81.2695 0,-4.2929 -31.5,0 0,-19.1211 29.5,0 0,-4.3008 -29.5,0 0,-20.8945 31.86,0 0,-4.3047 -36.9,0 0,52.914 36.54,0"
id="path3132"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 109.109,130.879 c -2.171,6.246 -5.242,11.781 -9.2145,16.601 -3.9765,4.825 -8.8007,8.7 -14.4765,11.637 -5.6758,2.938 -12.1133,4.395 -19.2969,4.395 -7.375,0 -13.9023,-1.457 -19.5781,-4.395 -5.6797,-2.937 -10.5039,-6.812 -14.4766,-11.637 -3.9726,-4.82 -7.1406,-10.41 -9.5078,-16.75 -2.3633,-6.332 -3.9258,-12.816 -4.6758,-19.433 l 94.7732,0 c -0.179,6.812 -1.371,13.348 -3.547,19.582 z M 20.5703,76.2539 c 1.8008,-6.9141 4.6875,-13.1055 8.6563,-18.5937 3.9765,-5.4883 8.9922,-10.0235 15.0429,-13.6133 6.0547,-3.6016 13.336,-5.3985 21.8516,-5.3985 13.0586,0 23.2734,3.4102 30.6484,10.2188 7.3755,6.8008 12.4885,15.8867 15.3205,27.2344 l 17.883,0 C 126.184,59.457 119.234,46.5898 109.109,37.5195 98.9922,28.4258 84.6602,23.8906 66.1211,23.8906 c -11.5352,0 -21.5234,2.043 -29.9336,6.1133 C 27.7578,34.0664 20.9023,39.6445 15.6094,46.7422 10.3125,53.8281 6.39063,62.0586 3.83203,71.4336 1.28125,80.7891 0,90.6719 0,101.086 c 0,9.641 1.28125,19.098 3.83203,28.371 2.5586,9.27 6.48047,17.551 11.77737,24.828 5.2929,7.285 12.1484,13.153 20.5781,17.606 8.4102,4.437 18.3984,6.664 29.9336,6.664 11.7266,0 21.7539,-2.375 30.0859,-7.102 8.32,-4.719 15.078,-10.922 20.285,-18.578 5.196,-7.66 8.938,-16.461 11.203,-26.395 2.278,-9.937 3.219,-20 2.84,-30.2222 l -112.6522,0 c 0,-6.4336 0.8867,-13.1055 2.6875,-20.0039"
id="path3134"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 157.859,174.297 0,-25.258 0.571,0 c 3.41,8.895 9.457,16.039 18.164,21.426 8.695,5.398 18.254,8.09 28.66,8.09 10.219,0 18.777,-1.325 25.68,-3.977 6.91,-2.648 12.441,-6.379 16.601,-11.203 4.16,-4.824 7.098,-10.738 8.797,-17.734 1.699,-7.008 2.555,-14.864 2.555,-23.555 l 0,-94.2188 -17.875,0 0,91.3708 c 0,6.246 -0.567,12.059 -1.703,17.461 -1.141,5.387 -3.121,10.067 -5.957,14.047 -2.844,3.969 -6.676,7.09 -11.5,9.359 -4.821,2.274 -10.829,3.407 -18.02,3.407 -7.191,0 -13.578,-1.278 -19.152,-3.828 -5.578,-2.555 -10.309,-6.055 -14.192,-10.5 -3.879,-4.442 -6.91,-9.743 -9.082,-15.895 -2.172,-6.156 -3.355,-12.82 -3.547,-20.004 l 0,-85.4178 -17.871,0 0,146.4298 17.871,0"
id="path3136"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 274.938,174.297 45.976,-128.5509 0.566,0 45.407,128.5509 18.449,0 -54.77,-146.4298 -19.019,0 -56.465,146.4298 19.856,0"
id="path3138"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 409.707,174.297 0,-146.4298 -17.875,0 0,146.4298 17.875,0 z m 0,56.187 0,-28.664 -17.875,0 0,28.664 17.875,0"
id="path3140"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 487.02,104.863 c 3.683,0 7.222,0.27 10.632,0.809 3.407,0.547 6.403,1.601 8.993,3.172 2.589,1.566 4.668,3.785 6.238,6.648 1.558,2.852 2.347,6.613 2.347,11.235 0,4.636 -0.789,8.39 -2.347,11.25 -1.57,2.859 -3.649,5.074 -6.238,6.64 -2.59,1.571 -5.586,2.629 -8.993,3.172 -3.41,0.547 -6.949,0.813 -10.632,0.813 l -24.934,0 0,-43.739 24.934,0 z m 8.793,68.68 c 9.128,0 16.898,-1.32 23.304,-3.988 6.406,-2.66 11.613,-6.16 15.633,-10.524 4.023,-4.359 6.961,-9.34 8.797,-14.922 1.84,-5.589 2.758,-11.379 2.758,-17.382 0,-5.852 -0.918,-11.614 -2.758,-17.27 -1.836,-5.652 -4.774,-10.664 -8.797,-15.0273 -4.02,-4.3633 -9.227,-7.8672 -15.633,-10.5234 -6.406,-2.6602 -14.176,-3.9844 -23.304,-3.9844 l -33.727,0 0,-52.336 -32.102,0 0,145.9571 65.829,0"
id="path3142"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 614.273,76.7539 c -1.835,-0.6211 -3.812,-1.1289 -5.929,-1.5351 -2.114,-0.4102 -4.324,-0.75 -6.641,-1.0196 -2.316,-0.2773 -4.637,-0.6211 -6.953,-1.0273 -2.176,-0.4102 -4.328,-0.9571 -6.437,-1.6328 -2.114,-0.6836 -3.958,-1.6016 -5.52,-2.7618 -1.566,-1.1562 -2.832,-2.625 -3.777,-4.3945 -0.957,-1.7773 -1.438,-4.0234 -1.438,-6.7461 0,-2.5937 0.481,-4.7695 1.438,-6.5429 0.945,-1.7696 2.246,-3.168 3.879,-4.1915 1.636,-1.0234 3.543,-1.7382 5.726,-2.1484 2.176,-0.4101 4.426,-0.6094 6.746,-0.6094 5.723,0 10.145,0.9532 13.285,2.8672 3.133,1.9024 5.45,4.1875 6.953,6.8438 1.497,2.6562 2.418,5.3476 2.758,8.0742 0.336,2.7226 0.516,4.9101 0.516,6.543 l 0,10.8398 c -1.234,-1.1055 -2.766,-1.9492 -4.606,-2.5586 z m -57.339,40.9841 c 3,4.496 6.812,8.106 11.449,10.832 4.637,2.723 9.844,4.672 15.637,5.828 5.789,1.157 11.617,1.735 17.48,1.735 5.316,0 10.691,-0.375 16.145,-1.125 5.457,-0.75 10.425,-2.211 14.921,-4.395 4.504,-2.175 8.18,-5.207 11.043,-9.093 2.86,-3.883 4.297,-9.032 4.297,-15.434 l 0,-54.9922 c 0,-4.7774 0.27,-9.336 0.817,-13.6954 0.539,-4.3632 1.496,-7.6328 2.859,-9.8125 l -29.437,0 c -0.543,1.6329 -0.989,3.3008 -1.329,5.0118 -0.343,1.6992 -0.582,3.4414 -0.718,5.2109 -4.633,-4.7734 -10.082,-8.1133 -16.348,-10.0156 -6.273,-1.9063 -12.676,-2.8672 -19.219,-2.8672 -5.043,0 -9.746,0.6172 -14.105,1.8398 -4.367,1.2266 -8.18,3.1367 -11.449,5.7305 -3.27,2.5859 -5.825,5.8594 -7.668,9.8125 -1.84,3.9492 -2.758,8.6523 -2.758,14.1016 0,5.9961 1.058,10.9375 3.168,14.8242 2.109,3.8789 4.836,6.9726 8.179,9.2969 3.34,2.3164 7.157,4.0585 11.446,5.2148 4.297,1.1562 8.617,2.0742 12.98,2.7539 4.364,0.6836 8.656,1.2266 12.879,1.6367 4.227,0.4102 7.973,1.0235 11.25,1.8438 3.266,0.8125 5.856,2.0117 7.762,3.582 1.91,1.5664 2.793,3.8477 2.664,6.8435 0,3.137 -0.516,5.617 -1.535,7.461 -1.028,1.844 -2.395,3.266 -4.09,4.293 -1.707,1.02 -3.68,1.699 -5.93,2.039 -2.25,0.344 -4.672,0.516 -7.258,0.516 -5.718,0 -10.222,-1.223 -13.488,-3.68 -3.277,-2.453 -5.183,-6.543 -5.726,-12.265 l -29.032,0 c 0.41,6.816 2.118,12.46 5.114,16.968"
id="path3144"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 720.172,133.277 0,-19.422 -21.254,0 0,-52.3355 c 0,-4.9023 0.812,-8.1718 2.449,-9.8047 1.637,-1.6406 4.903,-2.4609 9.817,-2.4609 1.632,0 3.195,0.0703 4.699,0.2031 1.496,0.1328 2.926,0.3438 4.289,0.6172 l 0,-22.4883 c -2.453,-0.414 -5.176,-0.6836 -8.176,-0.8203 -2.996,-0.1289 -5.926,-0.1992 -8.789,-0.1992 -4.5,0 -8.754,0.3047 -12.773,0.918 -4.024,0.6094 -7.563,1.8086 -10.629,3.5781 -3.071,1.7695 -5.489,4.293 -7.254,7.5586 -1.781,3.2773 -2.664,7.5703 -2.664,12.8828 l 0,62.3511 -17.578,0 0,19.422 17.578,0 0,31.684 29.031,0 0,-31.684 21.254,0"
id="path3146"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 755.918,173.543 0,-54.984 0.613,0 c 3.684,6.125 8.387,10.586 14.11,13.379 5.722,2.796 11.312,4.195 16.761,4.195 7.77,0 14.137,-1.059 19.114,-3.164 4.972,-2.114 8.894,-5.043 11.754,-8.793 2.863,-3.75 4.871,-8.317 6.031,-13.699 1.152,-5.383 1.734,-11.3481 1.734,-17.8832 l 0,-65.0079 -29.027,0 0,59.6954 c 0,8.7148 -1.363,15.2227 -4.086,19.5157 -2.731,4.293 -7.567,6.433 -14.516,6.433 -7.902,0 -13.633,-2.343 -17.172,-7.039 -3.543,-4.703 -5.316,-12.441 -5.316,-23.2144 l 0,-55.3907 -29.023,0 0,145.9571 29.023,0"
id="path3148"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /><path
d="m 863.082,0 21.875,0 0,190.547 -21.875,0 0,-190.547 z"
id="path3150"
style="fill:#090c0d;fill-opacity:1;fill-rule:nonzero;stroke:none" /></g></g></svg>

After

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

View File

@ -1,3 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 104 26" role="img" fill="currentColor">
<path id="ep-logo-name" d="M13.638 12.451a6.613 6.613 0 0 0-1.152-2.075 5.687 5.687 0 0 0-1.81-1.455c-.708-.369-1.513-.55-2.411-.55-.922 0-1.739.181-2.447.55a5.702 5.702 0 0 0-1.81 1.455 7.21 7.21 0 0 0-1.188 2.092 10.28 10.28 0 0 0-.586 2.43h11.848a8.013 8.013 0 0 0-.444-2.447zM2.571 19.277a6.885 6.885 0 0 0 1.082 2.324 6.187 6.187 0 0 0 1.88 1.702c.757.452 1.667.676 2.732.676 1.633 0 2.91-.427 3.83-1.277.923-.852 1.563-1.987 1.917-3.405h2.234c-.474 2.08-1.342 3.689-2.608 4.824-1.264 1.135-3.056 1.702-5.373 1.702-1.442 0-2.69-.254-3.742-.765-1.053-.507-1.91-1.203-2.572-2.092C1.29 22.082.8 21.052.48 19.88A13.991 13.991 0 0 1 0 16.174c0-1.206.16-2.388.479-3.547a9.56 9.56 0 0 1 1.472-3.103c.662-.91 1.519-1.643 2.572-2.2 1.051-.554 2.3-.833 3.742-.833 1.466 0 2.72.296 3.76.887A7.487 7.487 0 0 1 14.562 9.7c.65.959 1.118 2.058 1.401 3.3.284 1.243.402 2.5.354 3.777H2.234c0 .806.113 1.638.337 2.5M19.732 7.024v3.156h.07c.428-1.113 1.184-2.004 2.271-2.678a6.654 6.654 0 0 1 3.584-1.01c1.277 0 2.346.163 3.21.495.862.332 1.555.799 2.075 1.402.52.603.887 1.342 1.1 2.216.212.877.32 1.858.32 2.945v11.777h-2.235v-11.42c0-.782-.071-1.51-.214-2.184-.142-.673-.39-1.26-.745-1.757a3.61 3.61 0 0 0-1.436-1.17c-.603-.283-1.354-.425-2.253-.425-.898 0-1.697.16-2.395.479a5.221 5.221 0 0 0-1.772 1.31 6.034 6.034 0 0 0-1.136 1.988 8.128 8.128 0 0 0-.444 2.5v10.679h-2.234V7.024h2.234M34.367 7.024l5.747 16.067h.071L45.86 7.024h2.307l-6.846 18.303h-2.378L31.885 7.024h2.482M51.214 7.024v18.303h-2.235V7.024h2.235zm0-7.024V3.58h-2.235V0h2.235M61.037 15.703c.46 0 .902-.034 1.33-.103.424-.068.799-.2 1.122-.395.325-.195.584-.474.78-.833.196-.356.294-.825.294-1.404 0-.578-.098-1.047-.294-1.406a2.162 2.162 0 0 0-.78-.83 3.104 3.104 0 0 0-1.123-.395 8.39 8.39 0 0 0-1.329-.103H57.92v5.469h3.117zm1.099-8.587c1.14 0 2.112.166 2.913.498.801.335 1.452.772 1.953 1.316.503.545.871 1.167 1.1 1.866a6.89 6.89 0 0 1 .346 2.172c0 .733-.115 1.453-.346 2.159a5.043 5.043 0 0 1-1.1 1.88c-.501.544-1.152.984-1.953 1.316-.8.332-1.772.498-2.913.498H57.92v6.54h-4.013V7.116h8.229M76.944 19.216a5.26 5.26 0 0 1-.742.19c-.264.052-.54.096-.83.13-.29.034-.579.076-.87.127-.27.051-.54.12-.804.205-.263.085-.494.2-.69.344-.195.144-.354.33-.472.55-.12.222-.18.502-.18.844 0 .323.06.596.18.818.118.22.28.396.485.523.205.129.443.217.716.268.272.051.553.076.844.076.715 0 1.267-.117 1.66-.357.392-.239.68-.525.869-.857.187-.332.303-.668.344-1.008a6.93 6.93 0 0 0 .065-.818v-1.355a1.615 1.615 0 0 1-.575.32zm-7.168-5.124a4.345 4.345 0 0 1 1.43-1.353 6.257 6.257 0 0 1 1.956-.73 11.148 11.148 0 0 1 2.185-.215c.664 0 1.336.047 2.018.14a6.193 6.193 0 0 1 1.866.549 3.69 3.69 0 0 1 1.38 1.137c.356.486.537 1.128.537 1.93v6.874c0 .596.033 1.167.102 1.712.066.544.186.954.357 1.225h-3.68a5.23 5.23 0 0 1-.256-1.277 4.735 4.735 0 0 1-2.043 1.253 8.261 8.261 0 0 1-2.402.356c-.63 0-1.219-.076-1.763-.23a4.016 4.016 0 0 1-1.432-.715 3.34 3.34 0 0 1-.958-1.228c-.23-.493-.344-1.081-.344-1.762 0-.75.131-1.368.395-1.853a3.324 3.324 0 0 1 1.023-1.163 4.648 4.648 0 0 1 1.43-.651 15.515 15.515 0 0 1 1.623-.345 27.623 27.623 0 0 1 1.61-.202c.527-.052.996-.13 1.406-.232.408-.1.732-.252.97-.447.239-.195.349-.48.333-.857 0-.39-.065-.7-.192-.933a1.406 1.406 0 0 0-.511-.534 2.014 2.014 0 0 0-.741-.257 6.208 6.208 0 0 0-.907-.063c-.715 0-1.278.151-1.686.459-.41.308-.648.818-.716 1.533h-3.63c.052-.852.265-1.557.64-2.121M90.181 12.15v2.427h-2.657v6.543c0 .613.101 1.02.306 1.226.205.205.613.308 1.227.308.204 0 .399-.01.587-.027.188-.015.366-.042.537-.076v2.81a8.619 8.619 0 0 1-1.023.103c-.373.017-.74.024-1.098.024a10.41 10.41 0 0 1-1.597-.115 3.728 3.728 0 0 1-1.328-.446 2.356 2.356 0 0 1-.907-.945c-.222-.41-.333-.945-.333-1.61v-7.795h-2.198v-2.426h2.198V8.19h3.629v3.96h2.657M94.703 7.116v6.873h.075c.462-.764 1.05-1.323 1.764-1.672.716-.35 1.415-.523 2.096-.523.97 0 1.767.132 2.388.396.622.263 1.113.63 1.47 1.098.358.47.609 1.04.754 1.712.144.674.217 1.418.217 2.236v8.125h-3.629v-7.46c0-1.09-.17-1.905-.511-2.44-.34-.537-.945-.805-1.814-.805-.988 0-1.704.293-2.146.88-.443.587-.664 1.556-.664 2.901v6.924h-3.628V7.116h3.628" class="logo-main"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

View File

@ -1,3 +0,0 @@
<svg fill="currentColor" id="ep-logo-square" role="img" viewBox="0 0 65 65" xmlns="http://www.w3.org/2000/svg">
<path d="M26.538 25.283c-.532-1.519-1.274-2.861-2.24-4.037a11.163 11.163 0 0 0-3.522-2.828c-1.381-.712-2.948-1.07-4.697-1.07-1.791 0-3.379.358-4.76 1.07a11.11 11.11 0 0 0-3.522 2.828c-.966 1.176-1.737 2.533-2.308 4.07a19.839 19.839 0 0 0-1.139 4.728h23.047a15.559 15.559 0 0 0-.86-4.76zM5 38.57c.44 1.685 1.143 3.189 2.11 4.527.966 1.333 2.187 2.436 3.656 3.31 1.475.875 3.243 1.308 5.313 1.308 3.178 0 5.665-.83 7.456-2.485 1.793-1.65 3.037-3.867 3.726-6.626h4.35c-.922 4.054-2.613 7.179-5.073 9.39-2.46 2.208-5.947 3.315-10.46 3.315-2.802 0-5.233-.502-7.274-1.489-2.056-.986-3.721-2.348-5.01-4.072-1.284-1.724-2.241-3.725-2.861-6.005C.313 37.465 0 35.058 0 32.529c0-2.343.313-4.649.933-6.9.62-2.254 1.577-4.267 2.861-6.04 1.289-1.772 2.954-3.197 5.01-4.281 2.041-1.08 4.472-1.616 7.275-1.616 2.856 0 5.293.571 7.32 1.724 2.026 1.147 3.666 2.66 4.931 4.521 1.265 1.86 2.177 3.999 2.725 6.416a27.9 27.9 0 0 1 .692 7.348H4.35c0 1.567.215 3.188.65 4.868M49.301 31.456c.914 0 1.793-.07 2.638-.202.845-.136 1.586-.4 2.23-.78.641-.391 1.159-.942 1.548-1.656.387-.702.582-1.64.582-2.782 0-1.148-.195-2.08-.582-2.79-.39-.707-.907-1.26-1.547-1.65-.645-.386-1.386-.65-2.231-.78a16.554 16.554 0 0 0-2.638-.206h-6.176v10.846h6.176zm2.184-17.032c2.26 0 4.189.331 5.776.995 1.592.66 2.88 1.524 3.877 2.608a10.007 10.007 0 0 1 2.177 3.696c.46 1.393.684 2.828.684 4.313 0 1.455-.224 2.88-.684 4.28a9.955 9.955 0 0 1-2.177 3.727c-.997 1.079-2.285 1.953-3.877 2.607-1.587.664-3.516.992-5.776.992h-8.36v12.974h-7.964V14.424h16.324" class="logo-main"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

BIN
static/images/uoa.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

View File

@ -1,170 +0,0 @@
/**
* Discourse API Integration for enviPath Community
* Handles fetching topics from the Discourse forum API
*/
class DiscourseAPI {
constructor() {
this.baseUrl = 'https://community.envipath.org';
this.categoryId = 10; // Announcements category
this.limit = 3; // Number of topics to fetch
}
/**
* Fetch topics from Discourse API
* @param {number} limit - Number of topics to fetch
* @returns {Promise<Array>} Array of topic objects
*/
async fetchTopics(limit = this.limit) {
try {
const url = `${this.baseUrl}/c/announcements/${this.categoryId}.json`;
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
return this.processTopics(data.topic_list.topics, limit);
} catch (error) {
console.error('Error fetching Discourse topics:', error);
return this.getFallbackTopics();
}
}
/**
* Process raw Discourse topics into standardized format
* @param {Array} topics - Raw topics from Discourse API
* @param {number} limit - Number of topics to return
* @returns {Array} Processed topics
*/
processTopics(topics, limit) {
return topics
.slice(0, limit)
.map(topic => ({
id: topic.id,
title: topic.title,
excerpt: this.extractExcerpt(topic.excerpt),
url: `${this.baseUrl}/t/${topic.slug}/${topic.id}`,
replies: topic.reply_count,
views: topic.views,
created_at: topic.created_at,
category: 'Announcements',
category_id: this.categoryId,
author: topic.last_poster_username,
author_avatar: this.getAvatarUrl(topic.last_poster_username)
}))
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); // Latest first
}
/**
* Extract excerpt from topic content
* @param {string} excerpt - Raw excerpt from Discourse
* @returns {string} Cleaned excerpt
*/
extractExcerpt(excerpt) {
if (!excerpt) return 'Click to read more';
// Remove HTML tags and clean up; collapse whitespace; do not add manual ellipsis
return excerpt
.replace(/<[^>]*>/g, '') // Remove HTML tags
.replace(/&nbsp;/g, ' ') // Replace &nbsp; with spaces
.replace(/&amp;/g, '&') // Replace &amp; with &
.replace(/&lt;/g, '<') // Replace &lt; with <
.replace(/&gt;/g, '>') // Replace &gt; with >
.replace(/\s+/g, ' ') // Collapse all whitespace/newlines
.trim()
}
/**
* Get avatar URL for user
* @param {string} username - Username
* @returns {string} Avatar URL
*/
getAvatarUrl(username) {
if (!username) return `${this.baseUrl}/letter_avatar_proxy/v4/letter/u/1.png`;
return `${this.baseUrl}/user_avatar/${this.baseUrl.replace('https://', '')}/${username}/40/1_1.png`;
}
/**
* Get fallback topics when API fails
* @returns {Array} Fallback topics
*/
getFallbackTopics() {
return [
{
id: 110,
title: "enviPath Beta Update: Major Improvements to Prediction, Analysis & Collaboration!",
excerpt: "We're excited to announce major updates to the enviPath beta platform! This release includes significant improvements to our prediction algorithms, enhanced analysis tools, and new collaboration features that will make environmental biotransformation research more accessible and efficient.",
url: "https://community.envipath.org/t/envipath-beta-update-major-improvements-to-prediction-analysis-collaboration/110",
replies: 0,
views: 16,
created_at: "2025-09-23T00:00:00Z",
category: "Announcements",
category_id: 10,
author: "wicker",
author_avatar: "https://community.envipath.org/user_avatar/community.envipath.org/wicker/40/1_1.png"
}
];
}
/**
* Format date for display
* @param {string} dateString - ISO date string
* @returns {string} Formatted date
*/
formatDate(dateString) {
const date = new Date(dateString);
return date.toLocaleDateString();
}
/**
* Load topics and call render function
* @param {string} containerId - ID of container element
* @param {Function} renderCallback - Function to render topics
*/
async loadTopics(containerId = 'community-news-container', renderCallback = null) {
const container = document.getElementById(containerId);
if (!container) {
console.error(`Container with ID '${containerId}' not found`);
return;
}
// Hide loading spinner
const loading = document.getElementById('loading');
if (loading) {
loading.style.display = 'none';
}
try {
const topics = await this.fetchTopics();
if (renderCallback && typeof renderCallback === 'function') {
renderCallback(topics);
} else {
// Default rendering - just log topics
console.log('Topics loaded:', topics);
}
} catch (error) {
console.error('Error loading topics:', error);
container.innerHTML = '<p class="text-neutral">No updates found. Head over to the <a href="https://community.envipath.org" target="_blank" class="link link-primary">community</a> to see the latest discussions.</p>';
}
}
}
// Export for use in other scripts
window.DiscourseAPI = DiscourseAPI;
// Auto-initialize if container exists
document.addEventListener('DOMContentLoaded', function() {
if (document.getElementById('community-news-container')) {
const discourseAPI = new DiscourseAPI();
discourseAPI.loadTopics('community-news-container', function(topics) {
// This will be handled by the template's render function
if (window.renderDiscourseTopics) {
window.renderDiscourseTopics(topics);
}
});
}
});

View File

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

View File

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

View File

@ -1,17 +1,15 @@
<!DOCTYPE html>
<html data-theme="envipath">
<html>
{% load static %}
<head>
<title>{{ title }}</title>
<meta name="csrf-token" content="{{ csrf_token }}">
{# Favicon #}
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
{# Tailwind CSS disabled for legacy Bootstrap framework #}
{# Pages using this framework will be migrated to framework_modern.html incrementally #}
{# <link href="{% static 'css/output.css' %}" rel="stylesheet" type="text/css"/> #}
{# Legacy Bootstrap 3.3.7 - scoped to .legacy-bootstrap #}
<style>
html, body {
height: 100%; /* ensure body fills viewport */
overflow-x: hidden; /* prevent horizontal scroll */
}
</style>
{# TODO use bundles from bootstrap 3.3.7 #}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>
@ -22,16 +20,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-select/1.7.3/js/bootstrap-select.min.js"></script>
<script src="https://community.envipath.org/javascripts/embed-topics.js"></script>
<!-- CDN END -->
{# Bootstrap compatibility styles #}
<style>
/* Ensure proper viewport behavior */
html, body {
height: 100%; /* ensure body fills viewport */
overflow-x: hidden; /* prevent horizontal scroll */
}
</style>
<meta name="csrf-token" content="{{ csrf_token }}">
<script>
const csrftoken = document.querySelector('[name=csrf-token]').content;
@ -44,7 +33,8 @@
}
});
</script>
{# Favicon #}
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
<!-- {# C3 CSS #}-->
<!-- <link id="css-c3" href="{% static 'css/c3.css' %}" rel="stylesheet" type="text/css"/>-->
<!-- {# EP CSS #}-->
@ -66,7 +56,7 @@
(function () {
var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '{{ meta.site_id }}']);
_paq.push(['setSiteId', '10']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
@ -78,8 +68,6 @@
</head>
<body>
<!-- Legacy Bootstrap navbar - isolated from Tailwind -->
<div class="legacy-bootstrap">
<nav class="navbar navbar-default navbar-inverse" style="border-radius:0px;" role="navigation">
<div class="container-fluid">
<!-- Brand and toggle get grouped for better mobile display -->
@ -158,7 +146,8 @@
</li>
{% if meta.user.username == 'anonymous' %}
<li>
<a href="{% url 'login' %}" id="loginButton" style="margin-right:10px">Login</a>
<a href="#signup" id="loginButton" data-toggle="modal" data-target="#signupmodal"
style="margin-right:10px">Login</a>
</li>
{% else %}
<li class="dropdown">
@ -188,9 +177,6 @@
</div>
</div>
</nav>
</div>
<!-- End legacy Bootstrap navbar -->
<div id="docContent" class="content container">
{% if breadcrumbs %}
<div id="bread">
@ -235,8 +221,7 @@
{% endif %}
</div>
<!-- FOOTER - Legacy Bootstrap -->
<div class="legacy-bootstrap">
<!-- FOOTER -->
<div class="container text-center">
<hr/>
<div class="row">
@ -273,9 +258,6 @@
</ul>
</div>
</div>
</div>
<!-- End legacy Bootstrap footer -->
<script>
$(function () {
// Hide actionsbutton if theres no action defined

View File

@ -1,176 +0,0 @@
<!DOCTYPE html>
<html data-theme="envipath" lang="en">
{% load static %}
<head>
<title>{{ title }}</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="csrf-token" content="{{ csrf_token }}">
{# Favicon #}
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
{# Tailwind CSS + DaisyUI Output #}
<link href="{% static 'css/output.css' %}" rel="stylesheet" type="text/css"/>
{# jQuery - Keep for compatibility with existing JS #}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
{# Font Awesome #}
<link href="https://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
{# Discourse embed for community #}
<script src="https://community.envipath.org/javascripts/embed-topics.js"></script>
<script>
const csrftoken = document.querySelector('[name=csrf-token]').content;
// Setup CSRF header for all jQuery AJAX requests
$.ajaxSetup({
beforeSend: function(xhr, settings) {
if (!/^(GET|HEAD|OPTIONS|TRACE)$/i.test(settings.type)) {
xhr.setRequestHeader("X-CSRFToken", csrftoken);
}
}
});
</script>
{# General EP JS #}
<script src="{% static 'js/pps.js' %}"></script>
{# Modal Steps for Stepwise Modal Wizards #}
<script src="{% static 'js/jquery-bootstrap-modal-steps.js' %}"></script>
{% if not debug %}
<!-- Matomo -->
<script>
var _paq = window._paq = window._paq || [];
_paq.push(['trackPageView']);
_paq.push(['enableLinkTracking']);
(function () {
var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '{{ meta.site_id }}']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
s.parentNode.insertBefore(g, s);
})();
</script>
<!-- End Matomo Code -->
{% endif %}
</head>
<body class="min-h-screen bg-base-300">
{% include "includes/navbar.html" %}
{# Main Content Area #}
<main class="w-full">
{% block main_content %}
{# Breadcrumbs - outside main content, optional #}
{% if breadcrumbs %}
<div id="bread" class="max-w-7xl mx-auto px-4 py-4">
<div class="text-sm breadcrumbs">
<ul>
{% for elem in breadcrumbs %}
{% for name, url in elem.items %}
{% if forloop.parentloop.last %}
<li>{{ name }}</li>
{% else %}
<li><a href="{{ url }}">{{ name }}</a></li>
{% endif %}
{% endfor %}
{% endfor %}
</ul>
</div>
</div>
{% endif %}
{# Main content container - paper effect on medium+ screens #}
<div id="docContent" class="w-full xl:w-xl md:mx-auto md:my-8 bg-base-100 md:shadow-2xl md:rounded-lg border-2">
{# Messages - inside paper #}
{% if message %}
<div id="message" class="alert alert-info m-4">
{{ message }}
</div>
{% endif %}
{# Page content - no enforced styles #}
{% block content %}
{% endblock content %}
{# License - inside paper if present #}
{% if meta.url_contains_package and meta.current_package.license %}
<div class="collapse collapse-arrow bg-base-200 m-8">
<input type="checkbox" checked />
<div class="collapse-title text-xl font-medium">
License
</div>
<div class="collapse-content">
<a target="_blank" href="{{ meta.current_package.license.link }}">
<img src="{{ meta.current_package.license.image_link }}" alt="License">
</a>
</div>
</div>
{% endif %}
</div>
{% endblock main_content %}
</main>
{% include "includes/footer.html" %}
{# Floating Help Tab #}
{% if not public_mode %}
<div class="fixed right-0 top-1/2 -translate-y-1/2 z-50">
<a href="https://community.envipath.org/" target="_blank"
class="flex items-center justify-center btn btn-secondary hover:btn-secondary-focus text-secondary-content text-sm shadow-lg transition-all duration-300 hover:scale-105 hover:-translate-x-1"
title="Get Help from the Community">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-question-mark-icon lucide-message-circle-question-mark"><path d="M2.992 16.342a2 2 0 0 1 .094 1.167l-1.065 3.29a1 1 0 0 0 1.236 1.168l3.413-.998a2 2 0 0 1 1.099.092 10 10 0 1 0-4.777-4.719"/><path d="M9.09 9a3 3 0 0 1 5.83 1c0 2-3 3-3 3"/>
<path d="M12 17h.01"/>
</svg>
</a>
</div>
{% endif %}
{# Modals - TODO: Convert these to DaisyUI modals #}
{% block modals %}
{# Note: These modals still use Bootstrap markup and will need conversion #}
{% include "modals/cite_modal.html" %}
{% include "modals/signup_modal.html" %}
{% include "modals/predict_modal.html" %}
{% include "modals/batch_predict_modal.html" %}
{% endblock %}
<script>
$(function () {
// Hide actionsbutton if there's no action defined
if ($('#actionsButton ul').children().length > 0) {
$('#actionsButton').show();
}
});
// Global keyboard shortcut for search (Cmd+K on Mac, Ctrl+K on Windows/Linux)
document.addEventListener('keydown', function(event) {
// Check if user is typing in an input field
const activeElement = document.activeElement;
const isInputField = activeElement && (
activeElement.tagName === 'INPUT' ||
activeElement.tagName === 'TEXTAREA' ||
activeElement.contentEditable === 'true'
);
if (isInputField) {
return; // Don't trigger shortcut when typing in input fields
}
// Check for Cmd+K (Mac) or Ctrl+K (Windows/Linux)
const isMac = /Mac/.test(navigator.platform);
const isCorrectModifier = isMac ? event.metaKey : event.ctrlKey;
if (isCorrectModifier && event.key === 'k') {
event.preventDefault();
window.location.href = '/search';
}
});
</script>
</body>
</html>

View File

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

View File

@ -1,71 +0,0 @@
{% load static %}
{# Modern DaisyUI Navbar #}
<div class="navbar bg-neutral-50 text-neutral-950 shadow-lg x-50">
<div class="navbar-start">
<a href="{{ meta.server_url }}" class="btn btn-ghost normal-case text-xl">
<svg class="h-8 fill-base-content" viewBox="0 0 104 26" role="img">
<use href='{% static "/images/logo-name.svg" %}#ep-logo-name' />
</svg>
</a>
</div>
{% if not public_mode %}
<div class="navbar-center hidden lg:flex">
<a href="#" role="button" class="btn btn-ghost" id="predictLink">Predict</a>
<!-- <li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li> -->
<!--<li><a href="{{ meta.server_url }}/browse" id="browseLink">Browse</a></li>-->
<div class="dropdown dropdown-center">
<div tabindex="0" role="button" class="btn btn-ghost">Browse</div>
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-1 w-52 p-2 shadow-sm">
<li><a href="{{ meta.server_url }}/pathway" id="pathwayLink">Pathway</a></li>
<li><a href="{{ meta.server_url }}/rule" id="ruleLink">Rule</a></li>
<li><a href="{{ meta.server_url }}/compound" id="compoundLink">Compound</a></li>
<li><a href="{{ meta.server_url }}/reaction" id="reactionLink">Reaction</a></li>
<li><a href="{{ meta.server_url }}/model" id="relative-reasoningLink">Model</a></li>
<li><a href="{{ meta.server_url }}/scenario" id="scenarioLink">Scenario</a></li>
</ul>
</div>
</div>
{% endif %}
<div class="navbar-end">
{% if not public_mode %}
<a href="/search" role="button" >
<div class="flex items-center badge badge-dash space-x-1 bg-base-200 text-base-content/50 p-2 m-1">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-search-icon lucide-search"><path d="m21 21-4.34-4.34"/><circle cx="11" cy="11" r="8"/></svg>
<span id="search-shortcut">⌘K</span>
</div>
</a>
{% endif %}
{% if meta.user.username == 'anonymous' or public_mode %}
<a href="{% url 'login' %}" id="loginButton" class="p-2">Login</a>
{% else %}
<div class="dropdown dropdown-end">
<div tabindex="0" role="button" class="btn btn-ghost m-1 btn-circle" id="loggedInButton">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-circle-user-icon lucide-circle-user"><circle cx="12" cy="12" r="10"/><circle cx="12" cy="10" r="3"/><path d="M7 20.662V19a2 2 0 0 1 2-2h6a2 2 0 0 1 2 2v1.662"/></svg>
</div>
<ul tabindex="-1" class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2 shadow-sm">
<li><a href="{{ meta.user.url }}" id="accountbutton">Settings</a></li>
<li>
<form id="logoutForm" action="{% url 'logout' %}" method="post" style="display: none;">
{% csrf_token %}
<input type="hidden" name="logout" value="true">
</form>
<a href="#" id="logoutButton" onclick="event.preventDefault(); document.getElementById('logoutForm').submit();">Logout</a>
</li>
</ul>
</div>
{% endif %}
</div>
</div>
<script>
// OS-aware search shortcut display
(function() {
const isMac = /Mac/.test(navigator.platform);
const shortcutElement = document.getElementById('search-shortcut');
if (shortcutElement) {
shortcutElement.textContent = isMac ? '⌘K' : 'Ctrl+K';
}
})();
</script>

View File

@ -1,360 +1,186 @@
{% extends "framework_modern.html" %}
{% extends "framework.html" %}
{% load static %}
{% block main_content %}
<!-- Hero Section with Logo and Search -->
<section class="hero h-fit max-w-5xl w-full shadow-none mx-auto relative">
<div class="hero min-h-[calc(100vh*0.4)] bg-gradient-to-br from-primary-800 to-primary-600"
style="background-image: url('{% static "/images/hero.png" %}'); background-size: cover; background-position: center;"
>
<div class="hero-overlay"></div>
<!-- Predict Pathway text over the image -->
<div class="absolute bottom-40 left-1/8 -translate-x-8 z-10">
<h2 class="text-3xl text-base-100 text-shadow-lg text-left">Predict Your Pathway</h2>
{% block content %}
<!-- TODO rename ids as well as remove pathways if modal is closed!-->
<div class="modal fade" tabindex="-1" id="foundMatching" role="dialog" aria-labelledby="foundModal"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="modal-title" id="newPackMod">Found Pathway in Database</h4>
</div>
<div class="modal-body">
<p>We found at least one pathway in the database with the given root
compound. Do you want to open any of the existing pathways or
predict a new one? To open an existing pathway, simply click
on the pathway, to predict a new one, click Predict. The predicted
pathway might differ from the ones in the database due to the
settings used in the prediction.</p>
<div id="foundPathways"></div>
</div>
<div class="modal-footer">
<a id="modal-predict" class="btn btn-primary" href="#">Predict</a>
<button type="button" id="cancel-predict" class="btn btn-default" data-dismiss="modal">Cancel
</button>
</div>
</div>
</section>
<div class="shadow-md max-w-5xl mx-auto bg-base-200">
<!-- Predict Pathway Section -->
<div class="flex-col lg:flex-row-reverse w-full mx-auto -mt-32 relative z-20 mb-10 ">
<div class="card bg-base-100 shrink-0 shadow-xl w-3/4 mx-auto transition-all duration-300 ease-in-out">
<div class="card-body">
<!-- Input Mode Toggle - Fixed position outside fieldset -->
<div class="flex flex-row justify-start items-center h-fit ml-8 my-4">
<div class="flex items-center gap-2">
<!-- <span class="text-sm text-neutral-500">Input Mode:</span> -->
<label class="toggle text-base-content toggle-md">
<input type="checkbox" />
<svg aria-label="smiles mode" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" class="size-5">
<g
stroke-linejoin="round"
stroke-linecap="round"
stroke-width="2"
fill="currentColor"
stroke="none"
>
<path fill-rule="evenodd" d="M8 2.75A.75.75 0 0 1 8.75 2h7.5a.75.75 0 0 1 0 1.5h-3.215l-4.483 13h2.698a.75.75 0 0 1 0 1.5h-7.5a.75.75 0 0 1 0-1.5h3.215l4.483-13H8.75A.75.75 0 0 1 8 2.75Z" clip-rule="evenodd" />
</g>
</svg>
<svg
aria-label="draw mode"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
stroke="none"
class="size-5"
>
<path d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" />
</svg>
</label>
</div>
</div>
<div class="row">
<div class="col-xs-8">
<div class="jumbotron">
<h1>
<img id="image-logo-long" class="img-responsive" alt="enviPath" width="1000ex"
src='{% static "/images/logo-long.svg" %}'/>
</h1>
<p>enviPath is a database and prediction system for the microbial
biotransformation of organic environmental contaminants. The
database provides the possibility to store and view experimentally
observed biotransformation pathways. The pathway prediction system
provides different relative reasoning models to predict likely biotransformation
pathways and products. You can try it out below.
</p>
<p>
<a class="btn" style="background-color:#222222;color:#9d9d9d" role="button" target="_blank"
href="https://wiki.envipath.org/index.php/Main_Page">Learn more &gt;&gt;</a>
</p>
</div>
<fieldset class="fieldset transition-all duration-300 ease-in-out overflow-hidden">
</div>
<div id="loading"></div>
<div class="col-xs-4">
<d-topics-list discourse-url="https://community.envipath.org" per-page="10" category="10"
template="complete"></d-topics-list>
</div>
</div>
<div class="row">
<form id="index-form" action="{{ meta.current_package.url }}/pathway" method="POST">
{% csrf_token %}
<div id="text-input-container" class="transition-all duration-300 ease-in-out opacity-100 transform scale-100">
<div class="join w-full mx-auto">
<input type="text" id="index-form-text-input" placeholder="canonical SMILES string" class="input grow input-md join-item" />
<button class="btn btn-neutral join-item">Predict!</button>
<div class="input-group" id="index-form-bar">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-expanded="false">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li>
<iframe id="index-form-ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%"
height="510"></iframe>
</li>
</ul>
</div>
<div class="label relative w-full mt-1" >
<div class="flex gap-2">
<a href="#" class="example-link cursor-pointer hover:text-primary"
data-smiles="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
title="load example">Caffeine</a>
<a href="#" class="example-link cursor-pointer hover:text-primary"
data-smiles="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
title="load example">Ibuprofen</a>
<input type="text" class="form-control" id='index-form-text-input'
placeholder="Enter a SMILES to predict a Pathway or type something to search">
<div class="input-group-btn">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-haspopup="true"
aria-expanded="false" id="action-button">Predict <span class="caret"></span></button>
<ul class="dropdown-menu">
<li><a id="dropdown-predict">Predict</a></li>
<li><a id="dropdown-search">Search</a></li>
</ul>
<button class="btn" style="background-color:#222222;color:#9d9d9d" type="button" id="run-button">Go!
</button>
</div>
<a class="absolute top-0 left-[calc(100%-5.4rem)]" href="#">Advanced</a>
</div>
</div>
<div id="ketcher-container" class="hidden w-full transition-all duration-300 ease-in-out opacity-0 transform scale-95">
<iframe id="index-ketcher" src="{% static '/js/ketcher2/ketcher.html' %}"
width="100%" height="400" class="rounded-lg"></iframe>
<button class="btn btn-lg bg-primary-950 text-primary-50 join-item w-full mt-2">Predict!</button>
<a class="label mx-auto w-full mt-1" href="#">Advanced</a>
</div>
<input type="hidden" id="index-form-smiles" name="smiles" value="smiles">
<input type="hidden" id="index-form-predict" name="predict" value="predict">
<input type="hidden" id="current-action" value="predict">
</form>
</fieldset>
</div>
</div>
</div>
<!-- Community News Section -->
<section class="py-16 bg-base-200 z-10 mx-8">
<div class="max-w-7xl mx-auto px-4">
<h2 class="h2 font-bold text-left mb-8">Community Updates</h2>
<div id="community-news-container" class="flex gap-4 justify-center">
<!-- News cards will be populated here -->
<div id="loading" class="flex justify-center w-full">
<span class="loading loading-spinner loading-lg"></span>
</div>
</div>
<div class="text-right mt-6">
<a href="https://community.envipath.org/c/announcements/10" target="_blank" class="btn btn-ghost btn-sm">
Read More Announcements
</a>
</div>
<!-- Discourse API integration -->
<script src="{% static 'js/discourse-api.js' %}"></script>
</div>
</section>
<!-- Mission Statement Section -->
<section class="py-16 from-base-200 to-base-100 bg-gradient-to-b">
<div class="mx-auto px-8 md:px-12">
<div class="flex flex-row gap-4">
<div class="w-1/3">
<img src="{% static "/images/ep-rule-artwork.png" %}" alt="rule-based iterative tree greneration" class="w-full h-full object-contain" />
</div>
<div class="space-y-4 text-left w-2/3 mr-8">
<h2 class="h2 font-bold mb-8">About enviPath</h2>
<p class="">
enviPath is a database and prediction system for the microbial
biotransformation of organic environmental contaminants. The
database provides the possibility to store and view experimentally
observed biotransformation pathways.
</p>
<p class="">
The pathway prediction system provides different relative reasoning models
to predict likely biotransformation pathways and products. Explore our tools
and contribute to advancing environmental biotransformation research.
</p>
<div class="flex flex-row gap-4 float-right">
<a href="/about" class="btn btn-ghost-neutral">Read More</a>
<a href="/about" class="btn btn-neutral">Publications</a>
</div>
</div>
</div>
</div>
</section>
<!-- Partners Section -->
<section class="py-14 sm:py-12 bg-base-100">
<div class="mx-auto px-6 lg:px-8">
<div class="divider"><h2 class="text-center text-lg/8 font-semibold">Backed by Science</h2></div>
<div class="mx-auto mt-10 grid max-w-lg grid-cols-4 items-center gap-x-8 gap-y-10 sm:max-w-xl sm:grid-cols-6 sm:gap-x-10 lg:mx-0 lg:max-w-none lg:grid-cols-3">
<img src="{% static "/images/uoa-logo-small.png" %}" alt="The University of Auckland" class=" max-h-20 w-full object-contain lg:col-span-1" />
<img src="{% static "/images/logo-eawag.svg" %}" alt="Eawag" class=" max-h-12 w-full object-contain lg:col-span-1" />
<img src="{% static "/images/uzh-logo.svg" %}" alt="University of Zurich" class="2 max-h-16 w-full object-contain lg:col-span-1" />
</div>
</div>
</section>
</div>
<p></p>
<script language="javascript">
var currentPackage = "{{ meta.current_package.url }}";
// Discourse API integration is now handled by discourse-api.js
function goButtonClicked() {
$(this).prop("disabled", true);
// Function to render Discourse topics into cards
function renderDiscourseTopics(topics) {
const container = document.getElementById('community-news-container');
if (!container) return;
var action = $('#action-button').text().trim();
// Clear container
container.innerHTML = '';
var textSmiles = $('#index-form-text-input').val().trim();
// Create cards for each topic
topics.forEach(topic => {
const card = createDiscourseCard(topic);
container.insertAdjacentHTML('beforeend', card);
});
if (textSmiles === '') {
$(this).prop("disabled", false);
return;
}
// Function to create HTML card for a topic
function createDiscourseCard(topic) {
const date = new Date(topic.created_at).toLocaleDateString();
var ketcherSmiles = getKetcher('index-form-ketcher').getSmiles().trim();
return `
<div class="card bg-white shadow-xs hover:shadow-lg transition-shadow duration-300 h-64 w-75 flex-shrink-0">
<div class="card-body flex flex-col h-full">
<h3 class="card-title leading-tight font-normal tracking-tight h-12 mb-2 line-clamp-2 text-ellipsis wrap-break-word overflow-hidden">
<a href="${topic.url}" target="_blank" class="hover:text-primary">
${topic.title}
</a>
</h3>
<div class="text-sm line-clamp-4 break-words" >
${topic.excerpt}
</div>
<div class="flex flex-row items-center justify-between">
<div class="flex items-center gap-2">
<div class="avatar tooltip tooltip-right" data-tip="${topic.author}">
<div class="w-8 rounded-full">
<img src="${topic.author_avatar}" alt="${topic.author}" />
</div>
</div>
<span class="text-xs text-gray-500">${date}</span>
</div>
<a href="${topic.url}" target="_blank" class="btn btn-ghost text-neutral-500 rounded-full p-2">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 7v14"/>
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z"/>
</svg>
</a>
</div>
</div>
</div>
`;
if (action !== 'Search' && ketcherSmiles !== '' && textSmiles !== ketcherSmiles) {
console.log("Ketcher and TextInput differ!");
}
// Make render function globally available
window.renderDiscourseTopics = renderDiscourseTopics;
// Toggle functionality with smooth animations
function toggleInputMode() {
const toggle = $('input[type="checkbox"]');
const textContainer = $('#text-input-container');
const ketcherContainer = $('#ketcher-container');
const formCard = $('.card');
const fieldset = $('.fieldset');
if (toggle.is(':checked')) {
// Draw mode - show Ketcher, hide text input
textContainer.addClass('opacity-0 transform scale-95');
textContainer.removeClass('opacity-100 transform scale-100');
// Adjust fieldset padding for Ketcher mode - reduce padding and make more compact
fieldset.removeClass('p-8');
fieldset.addClass('p-4');
// Wait for fade out to complete, then hide and show new content
setTimeout(() => {
textContainer.addClass('hidden');
ketcherContainer.removeClass('hidden opacity-0 transform scale-95');
ketcherContainer.addClass('opacity-100 transform scale-100');
// Force re-evaluation of iframe size
const iframe = document.getElementById('index-ketcher');
if (iframe) {
iframe.style.height = '400px';
}
}, 300);
if (action === 'Search') {
var par = {};
par['search'] = textSmiles;
par['mode'] = 'text';
var queryString = $.param(par, true);
window.location.href = "/search?" + queryString;
} else {
// SMILES mode - show text input, hide Ketcher
ketcherContainer.addClass('opacity-0 transform scale-95');
ketcherContainer.removeClass('opacity-100 transform scale-100');
// Restore fieldset padding for text input mode
fieldset.removeClass('p-4');
fieldset.addClass('p-8');
// Wait for fade out to complete, then hide and show new content
setTimeout(() => {
ketcherContainer.addClass('hidden');
textContainer.removeClass('hidden opacity-0 transform scale-95');
textContainer.addClass('opacity-100 transform scale-100');
}, 300);
// Transfer SMILES from Ketcher to text input if available
if (window.indexKetcher && window.indexKetcher.getSmiles) {
const smiles = window.indexKetcher.getSmiles();
if (smiles && smiles.trim() !== '') {
$('#index-form-text-input').val(smiles);
}
}
$('#index-form-smiles').val(textSmiles);
$('#index-form').submit();
}
}
// Ketcher integration
function indexKetcherToTextInput() {
$('#index-form-smiles').val(this.ketcher.getSmiles());
function actionDropdownClicked() {
var suffix = ' <span class="caret"></span>';
var dropdownVal = $(this).text();
if (dropdownVal === 'Search') {
$("#index-form").attr("action", '/search');
$("#index-form").attr("method", 'GET');
} else {
$("#index-form").attr("action", currentPackage + "/pathway");
}
$('#action-button').html(dropdownVal + suffix);
}
function ketcherToTextInput() {
$('#index-form-text-input').val(this.ketcher.getSmiles());
}
$(function () {
// Initialize fieldset with proper padding
$('.fieldset').addClass('p-8');
// Toggle event listener
$('input[type="checkbox"]').on('change', toggleInputMode);
$('#index-form').on("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
goButtonClicked();
}
});
// Ketcher iframe load handler
$('#index-ketcher').on('load', function() {
// Code that should be executed once DOM is ready goes here
$('#dropdown-predict').on('click', actionDropdownClicked);
$('#dropdown-search').on('click', actionDropdownClicked);
$('#run-button').on('click', goButtonClicked);
// Update Ketcher Width
var fullWidth = $('#index-form-bar').width();
$('#index-form-ketcher').width(fullWidth);
// add a listener that gets triggered whenever the structure in ketcher has changed
$('#index-form-ketcher').on('load', function () {
const checkKetcherReady = () => {
const win = this.contentWindow;
win = this.contentWindow
if (win.ketcher && 'editor' in win.ketcher) {
window.indexKetcher = win.ketcher;
win.ketcher.editor.event.change.handlers.push({
once: false,
priority: 0,
f: indexKetcherToTextInput,
f: ketcherToTextInput,
ketcher: win.ketcher
});
} else {
setTimeout(checkKetcherReady, 100);
}
};
checkKetcherReady();
});
// Handle example link clicks
$('.example-link').on('click', function(e) {
e.preventDefault();
const smiles = $(this).data('smiles');
const title = $(this).attr('title');
// Check if we're in Ketcher mode or text input mode
if ($('input[type="checkbox"]').is(':checked')) {
// In Ketcher mode - set the SMILES in Ketcher
if (window.indexKetcher && window.indexKetcher.setMolecule) {
window.indexKetcher.setMolecule(smiles);
}
} else {
// In text input mode - set the SMILES in the text input
$('#index-form-text-input').val(smiles);
}
// Show a brief feedback
const originalText = $(this).text();
$(this).text(`loaded!`);
setTimeout(() => {
$(this).text(originalText);
}, 1000);
});
// Handle form submission on Enter
$('#index-form').on("submit", function (e) {
e.preventDefault();
var textSmiles = '';
// Check if we're in Ketcher mode and extract SMILES
if ($('input[type="checkbox"]').is(':checked') && window.indexKetcher) {
textSmiles = window.indexKetcher.getSmiles().trim();
} else {
textSmiles = $('#index-form-text-input').val().trim();
}
if (textSmiles === '') {
return;
}
$('#index-form-smiles').val(textSmiles);
$("#index-form").attr("action", currentPackage + "/pathway");
$("#index-form").attr("method", 'POST');
this.submit();
});
// Discourse topics are now loaded automatically by discourse-api.js
});
</script>
{% endblock main_content %}
{% endblock content %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -13,34 +13,18 @@
<!-- Include required libs -->
<script src="https://d3js.org/d3.v5.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/c3@0.7.20/c3.min.js"></script>
<link
href="https://cdn.jsdelivr.net/npm/c3@0.7.20/c3.min.css"
rel="stylesheet"
/>
<link href="https://cdn.jsdelivr.net/npm/c3@0.7.20/c3.min.css" rel="stylesheet">
<div class="panel-group" id="model-detail">
<div class="panel panel-default">
<div
class="panel-heading"
id="headingPanel"
style="font-size:2rem;height: 46px"
>
{{ model.name|safe }}
<div
id="actionsButton"
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ model.name }}
<div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"
>
<a
href="#"
class="dropdown-toggle"
data-toggle="dropdown"
role="button"
aria-haspopup="true"
aria-expanded="false"
><span class="glyphicon glyphicon-wrench"></span> Actions
<span class="caret"></span><span style="padding-right:1em"></span
></a>
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false"><span
class="glyphicon glyphicon-wrench"></span> Actions <span class="caret"></span><span
style="padding-right:1em"></span></a>
<ul id="actionsList" class="dropdown-menu">
{% block actions %}
{% include "actions/objects/model.html" %}
@ -49,135 +33,81 @@
</div>
</div>
<div class="panel-body">
<p>{{ model.description|safe }}</p>
<p> {{ model.description }} </p>
</div>
{% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning' %}
{% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning'%}
<!-- Rule Packages -->
<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">
<h4 class="panel-title">
<a
id="rule-package-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#rule-package"
>Rule Packages</a
>
<a id="rule-package-link" data-toggle="collapse" data-parent="#model-detail"
href="#rule-package">Rule Packages</a>
</h4>
</div>
<div id="rule-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for p in model.rule_packages.all %}
<a class="list-group-item" href="{{ p.url }}"
>{{ p.name|safe }}</a
>
<a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a>
{% endfor %}
</div>
</div>
<!-- Reaction Packages -->
<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">
<h4 class="panel-title">
<a
id="reaction-package-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#reaction-package"
>Rule Packages</a
>
<a id="reaction-package-link" data-toggle="collapse" data-parent="#model-detail"
href="#reaction-package">Rule Packages</a>
</h4>
</div>
<div id="reaction-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for p in model.data_packages.all %}
<a class="list-group-item" href="{{ p.url }}"
>{{ p.name|safe }}</a
>
<a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a>
{% endfor %}
</div>
</div>
{% if model.eval_packages.all|length > 0 %}
<!-- Eval Packages -->
<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">
<h4 class="panel-title">
<a
id="eval-package-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#eval-package"
>Rule Packages</a
>
<a id="eval-package-link" data-toggle="collapse" data-parent="#model-detail"
href="#eval-package">Rule Packages</a>
</h4>
</div>
<div id="eval-package" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for p in model.eval_packages.all %}
<a class="list-group-item" href="{{ p.url }}"
>{{ p.name|safe }}</a
>
<a class="list-group-item" href="{{ p.url }}">{{ p.name }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Model Status -->
<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">
<h4 class="panel-title">
<a
id="model-status-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#model-status"
>Model Status</a
>
<a id="model-status-link" data-toggle="collapse" data-parent="#model-detail"
href="#model-status">Model Status</a>
</h4>
</div>
<div id="model-status" class="panel-collapse collapse in">
<div class="panel-body list-group-item">{{ model.status }}</div>
<div class="panel-body list-group-item">
{{ model.status }}
</div>
</div>
{% endif %}
{% if model.ready_for_prediction %}
<!-- Predict Panel -->
<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">
<h4 class="panel-title">
<a
id="predict-smiles-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#predict-smiles"
>Predict</a
>
<a id="predict-smiles-link" data-toggle="collapse" data-parent="#model-detail"
href="#predict-smiles">Predict</a>
</h4>
</div>
<div id="predict-smiles" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<div class="input-group">
<input
id="smiles-to-predict"
type="text"
class="form-control"
placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C"
/>
<input id="smiles-to-predict" type="text" class="form-control"
placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C">
<span class="input-group-btn">
<button
class="btn btn-default"
type="submit"
id="predict-button"
>
Predict!
</button>
<button class="btn btn-default" type="submit" id="predict-button">Predict!</button>
</span>
</div>
<div id="predictLoading"></div>
@ -189,37 +119,18 @@
{% if model.ready_for_prediction and model.app_domain %}
<!-- App Domain -->
<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">
<h4 class="panel-title">
<a
id="app-domain-assessment-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#app-domain-assessment"
>Applicability Domain Assessment</a
>
<a id="app-domain-assessment-link" data-toggle="collapse" data-parent="#model-detail"
href="#app-domain-assessment">Applicability Domain Assessment</a>
</h4>
</div>
<div id="app-domain-assessment" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<div class="input-group">
<input
id="smiles-to-assess"
type="text"
class="form-control"
placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C"
/>
<input id="smiles-to-assess" type="text" class="form-control" placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C">
<span class="input-group-btn">
<button
class="btn btn-default"
type="submit"
id="assess-button"
>
Assess!
</button>
<button class="btn btn-default" type="submit" id="assess-button">Assess!</button>
</span>
</div>
<div id="appDomainLoading"></div>
@ -231,34 +142,23 @@
{% if model.model_status == 'FINISHED' %}
<!-- Single Gen Curve Panel -->
<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">
<h4 class="panel-title">
<a
id="sg-curve-link"
data-toggle="collapse"
data-parent="#model-detail"
href="#sg-curve"
>Precision Recall Curve</a
>
<a id="sg-curve-link" data-toggle="collapse" data-parent="#model-detail"
href="#sg-curve">Precision Recall Curve</a>
</h4>
</div>
<div id="sg-curve" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<!-- Center container contents -->
<div
class="container"
style="display: flex;justify-content: center;"
>
<div class="container" style="display: flex;justify-content: center;">
<div id="sg-curve-plotdiv" class="chart">
<div id="sg-chart"></div>
</div>
</div>
</div>
</div>
{# prettier-ignore-start #}
<script>
$(function () {
if (!($('#sg-chart').length > 0)) {
@ -365,61 +265,44 @@
});
});
</script>
{# prettier-ignore-end #}
<!-- End Single Gen Curve Panel -->
{% endif %}
</div>
</div>
<script>
function handlePredictionResponse(data) {
res = "<table class='table table-striped'>";
res += "<thead>";
res += "<th scope='col'>#</th>";
columns = ["products", "image", "probability", "btrule"];
function handlePredictionResponse(data) {
res = "<table class='table table-striped'>"
res += "<thead>"
res += "<th scope='col'>#</th>"
columns = ['products', 'image', 'probability', 'btrule']
for (col in columns) {
res += "<th scope='col'>" + columns[col] + "</th>";
res += "<th scope='col'>" + columns[col] + "</th>"
}
res += "</thead>";
res += "<tbody>";
res += "</thead>"
res += "<tbody>"
var cnt = 1;
for (transformation in data) {
res += "<tr>";
res += "<th scope='row'>" + cnt + "</th>";
res +=
"<th scope='row'>" +
data[transformation]["products"][0].join(", ") +
"</th>";
res +=
"<th scope='row'>" +
"<img width='400' src='{% url 'depict' %}?smiles=" +
encodeURIComponent(data[transformation]["products"][0].join(".")) +
"'></th>";
res +=
"<th scope='row'>" +
data[transformation]["probability"].toFixed(3) +
"</th>";
if (data[transformation]["btrule"] != null) {
res +=
"<th scope='row'>" +
"<a href='" +
data[transformation]["btrule"]["url"] +
"'>" +
data[transformation]["btrule"]["name"] +
"</a>" +
"</th>";
res += "<tr>"
res += "<th scope='row'>" + cnt + "</th>"
res += "<th scope='row'>" + data[transformation]['products'][0].join(', ') + "</th>"
res += "<th scope='row'>" + "<img width='400' src='{% url 'depict' %}?smiles=" + encodeURIComponent(data[transformation]['products'][0].join('.')) + "'></th>"
res += "<th scope='row'>" + data[transformation]['probability'].toFixed(3) + "</th>"
if (data[transformation]['btrule'] != null) {
res += "<th scope='row'>" + "<a href='" + data[transformation]['btrule']['url'] + "'>" + data[transformation]['btrule']['name'] + "</a>" + "</th>"
} else {
res += "<th scope='row'>N/A</th>";
res += "<th scope='row'>N/A</th>"
}
res += "</tr>";
res += "</tr>"
cnt += 1;
}
res += "</tbody>";
res += "</table>";
res += "</tbody>"
res += "</table>"
$("#predictResultTable").append(res);
}
@ -428,40 +311,37 @@
$("#" + divid).empty();
}
if ($("#predict-button").length > 0) {
if ($('#predict-button').length > 0) {
$("#predict-button").on("click", function (e) {
e.preventDefault();
clear("predictResultTable");
data = {
smiles: $("#smiles-to-predict").val(),
classify: "ILikeCats!",
};
"smiles": $("#smiles-to-predict").val(),
"classify": "ILikeCats!"
}
if (data["smiles"].trim() === "") {
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append(
"Please enter a SMILES string to predict!",
);
$("#predictResultTable").append("Please enter a SMILES string to predict!");
return;
}
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: "get",
type: 'get',
data: data,
url: "",
url: '',
success: function (data, textStatus) {
try {
$("#predictLoading").empty();
handlePredictionResponse(data);
} catch (error) {
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append(
"Error while processing response :/",
);
$("#predictResultTable").append("Error while processing response :/");
}
},
error: function (jqXHR, textStatus, errorThrown, x) {
@ -473,30 +353,30 @@
});
}
if ($("#assess-button").length > 0) {
if ($('#assess-button').length > 0) {
$("#assess-button").on("click", function (e) {
e.preventDefault();
clear("appDomainAssessmentResultTable");
data = {
smiles: $("#smiles-to-assess").val(),
"app-domain-assessment": "ILikeCats!",
};
"smiles": $("#smiles-to-assess").val(),
"app-domain-assessment": "ILikeCats!"
}
if (data["smiles"].trim() === "") {
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append(
"Please enter a SMILES string to predict!",
);
$("#appDomainAssessmentResultTable").append("Please enter a SMILES string to predict!");
return;
}
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: "get",
type: 'get',
data: data,
url: "",
url: '',
success: function (data, textStatus) {
try {
$("#appDomainLoading").empty();
@ -504,23 +384,19 @@
console.log(data);
} catch (error) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass(
"alert alert-danger",
);
$("#appDomainAssessmentResultTable").append(
"Error while processing response :/",
);
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Error while processing response :/");
}
},
error: function (jqXHR, textStatus, errorThrown) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append(
jqXHR.responseJSON.error,
);
},
$("#appDomainAssessmentResultTable").append(jqXHR.responseJSON.error);
}
});
});
}
</script>
{% endblock content %}

View File

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

View File

@ -14,7 +14,7 @@
<div class="panel-group" id="package-detail">
<div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ package.name|safe }}
{{ package.name }}
<div id="actionsButton"
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
class="dropdown"><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 panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ pathway.name|safe }}
{{ pathway.name }}
</div>
</div>
<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 class="panel-body list-group-item">
{% for s in pathway.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %}
</div>
</div>
@ -266,7 +266,7 @@
<td colspan="2">
<li class="list-group-item">
<a href="{{ pathway.setting.model.url }}">
{{ pathway.setting.model.name|safe }}
{{ pathway.setting.model.name }}
</a>
</li>
</td>
@ -299,7 +299,7 @@
{% for p in pathway.setting.rule_packages.all %}
<li class="list-group-item">
<a href="{{ p.url }}">
{{ p.name|safe }}
{{ p.name }}
</a>
</li>
{% endfor %}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,132 +0,0 @@
{% extends "framework_modern.html" %}
{% load static %}
{% block main_content %}
<div class="max-w-4xl mx-auto px-4 py-8">
<!-- Breadcrumbs -->
<div class="text-sm breadcrumbs mb-4">
<ul>
<li><a href="/">Home</a></li>
<li>About Us</li>
</ul>
</div>
<!-- Main Content -->
<div class="bg-base-100 shadow-xl rounded-lg p-8">
<h1 class="text-4xl font-bold mb-6">About enviPath</h1>
<div class="prose max-w-none">
<!-- Hero Image/Graphic -->
<div class="mb-8">
<img src="{% static '/images/ep-rule-artwork.png' %}" alt="enviPath System" class="w-full max-w-2xl mx-auto rounded-lg shadow-md" />
</div>
<p class="text-lg mb-6">
enviPath is a comprehensive database and prediction system for the microbial biotransformation of
organic environmental contaminants. Since 2015, we have been at the forefront of computational
environmental chemistry, helping researchers understand and predict biodegradation pathways.
</p>
<h2 class="text-2xl font-semibold mt-8 mb-4">Our Mission</h2>
<p class="mb-4">
Our mission is to advance environmental science through innovative computational tools that predict
and analyze the biotransformation of chemical compounds. We strive to provide researchers, regulators,
and industry professionals with accurate, accessible tools for understanding environmental fate and behavior.
</p>
<h2 class="text-2xl font-semibold mt-8 mb-4">What We Offer</h2>
<div class="grid md:grid-cols-2 gap-6 mb-6">
<div class="card bg-base-200 shadow-md">
<div class="card-body">
<h3 class="card-title text-primary">Pathway Database</h3>
<p>Access experimentally observed biotransformation pathways and reactions from curated scientific literature.</p>
</div>
</div>
<div class="card bg-base-200 shadow-md">
<div class="card-body">
<h3 class="card-title text-primary">Prediction System</h3>
<p>Use our relative reasoning models to predict likely biotransformation pathways and products for new compounds.</p>
</div>
</div>
<div class="card bg-base-200 shadow-md">
<div class="card-body">
<h3 class="card-title text-primary">Machine Learning Models</h3>
<p>Leverage advanced ML algorithms trained on extensive biodegradation data for accurate predictions.</p>
</div>
</div>
<div class="card bg-base-200 shadow-md">
<div class="card-body">
<h3 class="card-title text-primary">Community Platform</h3>
<p>Join our active community of researchers to share knowledge, discuss findings, and collaborate.</p>
</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Our Technology</h2>
<p class="mb-4">
enviPath employs a unique combination of rule-based and machine learning approaches to predict
biotransformation pathways:
</p>
<ul class="list-disc list-inside mb-4 space-y-2">
<li><strong>Relative Reasoning:</strong> Uses structural similarity to known biotransformations</li>
<li><strong>Rule-Based Systems:</strong> Applies expert-curated transformation rules</li>
<li><strong>Machine Learning:</strong> Leverages neural networks for pattern recognition</li>
<li><strong>Hybrid Models:</strong> Combines multiple approaches for optimal accuracy</li>
</ul>
<h2 class="text-2xl font-semibold mt-8 mb-4">Our Partners</h2>
<p class="mb-4">
enviPath is backed by leading research institutions and collaborators:
</p>
<div class="flex flex-wrap justify-center items-center gap-8 my-8">
<img src="{% static '/images/uoa-logo-small.png' %}" alt="The University of Auckland" class="h-20 object-contain" />
<img src="{% static '/images/logo-eawag.svg' %}" alt="Eawag" class="h-16 object-contain" />
<img src="{% static '/images/uzh-logo.svg' %}" alt="University of Zurich" class="h-20 object-contain" />
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Our Team</h2>
<p class="mb-4">
enviPath is developed and maintained by a dedicated team of computational chemists, environmental
scientists, and software engineers. Our interdisciplinary approach ensures that the platform meets
the needs of the scientific community while remaining accessible and user-friendly.
</p>
<h2 class="text-2xl font-semibold mt-8 mb-4">Research Impact</h2>
<p class="mb-4">
Since its inception, enviPath has contributed to numerous scientific publications and environmental
assessments. Our tools are used by:
</p>
<ul class="list-disc list-inside mb-4 space-y-2">
<li>Academic researchers in environmental chemistry and toxicology</li>
<li>Regulatory agencies for chemical risk assessment</li>
<li>Chemical manufacturers for product development and safety evaluation</li>
<li>Environmental consultants for contamination studies</li>
</ul>
<h2 class="text-2xl font-semibold mt-8 mb-4">Open Science Commitment</h2>
<p class="mb-4">
We are committed to open science principles. enviPath provides free access to our database and
prediction tools for academic research. We actively contribute to the scientific community through
publications, open-source software, and collaboration.
</p>
<div class="card bg-primary text-primary-content mt-8">
<div class="card-body">
<h3 class="card-title">Get Involved</h3>
<p>Join our community, contribute data, or collaborate on research projects.</p>
<div class="card-actions justify-end mt-4">
<a href="https://community.envipath.org/" target="_blank" class="btn btn-secondary">Visit Community</a>
<a href="/contact" class="btn btn-ghost">Contact Us</a>
</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Publications</h2>
<p class="mb-4">
To learn more about the science behind enviPath, please visit our
<a href="/cite" class="link link-primary">citations page</a> for key publications and how to cite our work.
</p>
</div>
</div>
</div>
{% endblock main_content %}

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