80 Commits

Author SHA1 Message Date
34d5318534 reintroduces envipytags 2025-11-11 12:47:24 +01:00
34d3b75c6f searchterm default 2025-11-11 12:32:44 +01:00
b9ac713cb2 minor 2025-11-11 11:41:48 +01:00
05bee9b718 mrg 2025-11-11 10:54:36 +01:00
34589efbde [Fix] Mitigate XSS attack vector by cleaning input before it hits our Database (#171)
## Changes

- All text input fields are now cleaned with nh3 to remove html tags. We allow certain html tags under `settings.py/ALLOWED_HTML_TAGS` so we can easily update the tags we allow in the future.
- All names and descriptions now use the template tag `nh_safe` in all html files.
- Usernames and emails are a small exception and are not allowed any html tags

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Co-authored-by: jebus <lorsbach@envipath.com>
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#171
Reviewed-by: jebus <lorsbach@envipath.com>
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-11-11 22:49:55 +13:00
1cccefa991 [Feature] Basic Test Workflow (#186)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#186
Reviewed-by: liambrydon <lbry121@aucklanduni.ac.nz>
Reviewed-by: Tobias O <tobias.olenyi@envipath.com>
2025-11-11 21:07:25 +13:00
0d9947e6ce chore: add tailwindcss autosort 2025-11-10 18:36:00 +13:00
e3b381ab41 chore: ignore code-workspace 2025-11-10 18:34:09 +13:00
97626337aa chore: add prettier formatting to html 2025-11-10 18:30:07 +13:00
2aded2ddd7 Merge remote-tracking branch 'origin/develop' into feature/frontend_update 2025-11-10 17:52:00 +13:00
e26d5a21e3 [Enhancement] Refactor Dataset (#184)
# Summary
I have introduced a new base `class Dataset` in `ml.py` which all datasets should subclass. It stores the dataset as a polars DataFrame with the column names and number of columns determined by the subclass. It implements generic methods such as `add_row`, `at`, `limit` and dataset saving. It also details abstract methods required by the subclasses. These include `X`, `y` and `generate_dataset`.

There are two subclasses that currently exist. `RuleBasedDataset` for the MLRR models and `EnviFormerDataset` for the enviFormer models.

# Old Dataset to New RuleBasedDataset Functionality Translation

- [x] \_\_init\_\_
    - self.columns and self.num_labels moved to base Dataset class
    - self.data moved to base class with name self.df along with initialising from list or from another DataFrame
    - struct_features, triggered and observed remain the same
- [x] \_block\_indices
    - function moved to base Dataset class
- [x] structure_id
    - stays in RuleBasedDataset, now requires an index for the row of interest
- [x] add_row
    - moved to base Dataset class, now calls add_rows so one or more rows can be added at a time
- [x] times_triggered
    - stays in RuleBasedDataset, now does a look up using polars df.filter
- [x] struct_features (see init)
- [x] triggered (see init)
- [x] observed (see init)
- [x] at
    - removed in favour of indexing with getitem
- [x] limit
    - removed in favour of indexing with getitem
- [x] classification_dataset
    - stays in RuleBasedDataset, largely the same just with new dataset construction using add_rows
- [x] generate_dataset
    - stays in RuleBasedDataset, largely the same just with new dataset construction using add_rows
- [x] X
    - moved to base Dataset as @abstract_method, RuleBasedDataset implementation functionally the same but uses polars
- [x] trig
    - stays in RuleBasedDataset, functionally the same but uses polars
- [x] y
    - moved to base Dataset as @abstract_method, RuleBasedDataset implementation functionally the same but uses polars
- [x] \_\_get_item\_\_
    - moved to base dataset, now passes item to the dataframe for polars to handle
- [x] to_arff
    - stays in RuleBasedDataset, functionally the same but uses polars
- [x] \_\_repr\_\_
    - moved to base dataset
- [x] \_\_iter\_\_
    - moved to base Dataset, now uses polars iter_rows

# Base Dataset class Features
The following functions are available in the base Dataset class

- init - Create the dataset from a list of columns and data in format list of list. Or can create a dataset from a polars Dataframe, this is essential for recreating itself during indexing. Can create an empty dataset by just passing column names.
- add_rows - Add rows to the Dataset, we check that the new data length is the same but it is presumed that the column order matches the existing dataframe
- add_row - Add one row, see add_rows
- block_indices - Returns the column indices that start with the given prefix
- columns - Property, returns dataframe.columns
- shape - Property, returns dataframe.shape
- X - Abstract method to be implemented by the subclasses, it should represent the input to a ML model
- y - Abstract method to be implemented by the subclasses, it should represent the target for a ML model
- generate_dataset - Abstract and static method to be implemented by the subclasses, should return an initialised subclass of Dataset
- iter - returns the iterable from dataframe.iter_rows()
- getitem - passes the item argument to the dataframe. If the result of indexing the dataframe is another dataframe, the new dataframe is  packaged into a new Dataset of the same subclass. If the result of indexing is something else (int, float, polar Series) return the result.
- save - Pickle and save the dataframe to the given path
- load - Static method to load the dataset from the given path
- to_numpy - returns the dataframe as a numpy array. Required for compatibility with training of the ECC model
- repr - return a representation of the dataset
- len - return the length of the dataframe
- iter_rows - Return dataframe.iterrows with arguments passed through. Mainly used to get the named iterable which returns rows of the dataframe as dict of column names: column values instead of tuple of column values.
- filter - pass to dataframe.filter and recreates self with the result
- select - pass to dataframe.select and recreates self with the result
- with_columns - pass to dataframe.with_columns and recreates self with the result
- sort - pass to dataframe.sort and recreates self with the result
- item - pass to dataframe.item
- fill_nan - fill the dataframe nan's with value
- height - Property, returns the height (number of rows) of the dataframe

- [x] App domain
- [x] MACCS alternatives

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#184
Reviewed-by: jebus <lorsbach@envipath.com>
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-11-07 08:46:17 +13:00
f5133c1980 fix: remove obsolete page id 2025-11-06 10:36:04 +13:00
7fbc49afd3 chore: update citations 2025-11-05 17:50:56 +13:00
a087a518f6 chore: remove incorrect license header 2025-11-05 17:39:21 +13:00
881e0e6798 chore: fix typo 2025-11-05 17:38:52 +13:00
2eab66e9ee refactor: added meta.site_id for matomo 2025-11-05 17:37:44 +13:00
ab927b11a2 refactor: remove dependency-groups 2025-11-05 17:36:43 +13:00
fde60c3ad3 refactor: remove optional stubs 2025-11-05 17:35:45 +13:00
61a43da822 refactor: set enviformer to main 2025-11-05 17:34:31 +13:00
211ebfd19b refactor: remove enviformer loading in settings 2025-11-05 17:33:41 +13:00
06a6c23d05 fix: add tailwindcss/cli 2025-11-05 17:30:15 +13:00
3536a14e47 Merge remote-tracking branch 'origin/develop' into feature/frontend_update 2025-11-05 17:25:27 +13:00
98d62e1d1f [Feature] Make Matomo Site ID configurable via .env (#183)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#183
2025-11-05 10:19:07 +13:00
7eb4029ac9 refactor: add public_mode for static pages to remove nav elements 2025-11-04 19:34:04 +13:00
7b38fc2e37 fix: remove jobs clash 2025-11-04 19:33:31 +13:00
4834348454 Merge remote-tracking branch 'origin/develop' into feature/frontend_update 2025-10-30 14:02:57 +13:00
13ed86a780 [Feature] Identify Missing Rules (#177)
Fixes #97
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#177
2025-10-30 00:47:45 +13:00
f1b4c5aadb [Feature] Adding list_display to various django admin sites (#180)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#180
2025-10-29 22:26:28 +13:00
0a52b12f02 fix: handle line-clamp issue with news 2025-10-29 19:59:45 +13:00
14571d23a6 docs: add pnpm note 2025-10-29 18:23:28 +13:00
ea8475f0e2 docs: update README regarding dev command 2025-10-29 18:07:56 +13:00
442d139217 chore: remove obsolete doc 2025-10-29 18:06:21 +13:00
1ba511a31d chore: minimize fallback data 2025-10-29 18:02:30 +13:00
5d89341955 chore: delete obsolete runserver command 2025-10-29 18:01:21 +13:00
5f390ac2d2 fix: reenable modal showing 2025-10-29 17:52:10 +13:00
46d21e60d2 chore: add example input to search 2025-10-29 16:36:01 +13:00
13be240226 feat: working search redirect 2025-10-29 16:30:00 +13:00
167a72f5a3 fix: remove obsolete menu list 2025-10-29 16:01:13 +13:00
1736319bd7 style: update navbar and add browse back 2025-10-29 15:58:17 +13:00
e87aae6bf7 style: add legal footers on login 2025-10-29 12:16:42 +13:00
253523c81f feat: add mock legal (impressum page) 2025-10-29 12:16:24 +13:00
15809a4ccf style: update login pages 2025-10-29 12:01:35 +13:00
b7e1dac66a feat: add mockup for static pages 2025-10-29 11:13:31 +13:00
849ebbe7f8 style: update hero 2025-10-29 11:13:07 +13:00
c5dcb36452 fix: dev command working 2025-10-29 10:59:22 +13:00
37e0e18a28 [Fix] Fixed Incremental Prediction Typo (#176)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#176
2025-10-28 23:29:08 +13:00
de44c22606 [Migration] Added missing Migration for JobLog (#175)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#175
2025-10-27 22:41:16 +13:00
a952c08469 [Feature] Basic logging of Jobs, Model Evaluation (#169)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#169
2025-10-27 22:34:05 +13:00
551cfc7768 [Enhancement] Create ML Models (#173)
## Changes

- Ability to change the threshold from a command line argument.
- Names of data packages included in model name
- Names of data, rule and eval packages included in the model description
- EnviFormer models are now viewable on the admin site
- Ignore CO2 for training and evaluating EnviFormer

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#173
Reviewed-by: jebus <lorsbach@envipath.com>
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-10-23 06:20:22 +13:00
16a991220a Slim down Navbar 2025-10-22 12:10:46 +13:00
05c8e130b1 Add documentation link 2025-10-22 12:08:31 +13:00
4fd7856043 Remove fields from navbar 2025-10-22 12:07:55 +13:00
f5efaf1b3f Change to icon help button 2025-10-22 12:07:42 +13:00
4a2ef3a237 Add search icon mockup 2025-10-22 11:37:23 +13:00
d097013853 Add discourse API for better data retrieval 2025-10-22 11:37:13 +13:00
63cc7cf460 Change partners location 2025-10-22 11:36:25 +13:00
8fda2577ee [Feature] Dump/Restore of enviFormer Models (#170)
Dump:
`./manage.py  dump_enviformer d544303c-a1ca-439d-b036-5e3413ce4a48 --output test.tar.gz`

Restore:
`./manage.py load_enviformer test.tar.gz 1062eb09-5ec7-4bdd-a8f2-ae0252eb4b06`

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#170
2025-10-22 10:39:22 +13:00
819a94aced [Fix] Catch Exception for Adding Structures / Show PubChem Substances (#168)
Fixes #163
Fixes #165

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#168
2025-10-22 01:13:06 +13:00
9ca94eeb42 Add initial design suggestion index page 2025-10-21 17:09:40 +13:00
de1dc75a12 Add styled footer 2025-10-20 22:30:11 +13:00
43a034427d Add optimized logo components 2025-10-20 22:25:30 +13:00
d98b60fc57 Add better image 2025-10-17 23:25:55 +13:00
86c3f9b808 Improve search look 2025-10-17 22:45:16 +13:00
4408a09e82 Add basic index redesign 2025-10-17 22:33:06 +13:00
f5889b270a New Layout for parallel styling
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-17 20:25:50 +13:00
376fd65785 [Feature] ML model caching for reducing prediction overhead (#156)
The caching is now finished. The cache is created in `settings.py` giving us the most flexibility for using it in the future.

The cache is currently updated/accessed by `tasks.py/get_ml_model` which can be called from whatever task needs to access ml models in this way (currently, `predict` and `predict_simple`).

This implementation currently caches all ml models including the relative reasoning. If we don't want this and only want to cache enviFormer, i can change it to that. However, I don't think there is a harm in having the other models be cached as well.

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#156
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-10-16 08:58:36 +13:00
f76861a83f chore: migrate build commands to poe
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:24:51 +13:00
110fbe12c1 chore: update gitignore for pnpm
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:09:06 +13:00
d8dc9598ff chore: add makefile integration and documentation
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:07:20 +13:00
b7e4abae1c chore: add pnpm install to make
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:07:04 +13:00
2fa0be52ef Update commands to automatically run pnpm for relevant commands
Signed-off-by: Tobias O <tobias.olenyi@tum.de>
2025-10-13 21:07:04 +13:00
be5ff369e0 Setup TailwindCSS and DaisyUI
This commit introduces pnpm for build time dependency. While it does add
npm, it keeps it at a minimum and buildtime only to eventually allow for
automatic CI builds.

This is discussed in #133

Signed-off-by: Tobias O <tobias.olenyi@tum.de>
2025-10-13 21:07:04 +13:00
ee776e7d14 refactor: ALLOWED_HOSTS throws exception if not present
ALLOWED_HOSTS is now manfatory again. Also cleaned defunct SQLite
backend and autoformated fille to line lenght 100.

Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:06:42 +13:00
f27bc56379 chore: Setup pre-commit to automatically run ruff on commit.
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:06:14 +13:00
547eac3411 chore: add basic ruff configuration with line lenght 100
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:05:49 +13:00
9e66169d23 fix: fix docker compose command typo
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:05:28 +13:00
9a43942f12 docs: remove alternative install instructions for lower maintenance.
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:05:28 +13:00
1bad6e4b03 docs: add makefile and docker-compose for initial setup
Since sqlite is not a suitable backend due to array field switch to an
easy to setup postgres backend relying on docker. From cloning to run is
now just two commands.

Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:05:07 +13:00
ad38704ffe refactor: create local version of settings w/o postgres
This version allows installation using sqlite for local dev setup.

Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:04:31 +13:00
0775ad30d4 docs: add note on RDkit install painpoint
Signed-off-by: Tobias O <tobias.olenyi@envipath.com>
2025-10-13 21:03:13 +13:00
114 changed files with 6779 additions and 1951 deletions

View File

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

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

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

5
.gitignore vendored
View File

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

View File

@ -8,6 +8,7 @@ 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
@ -20,6 +21,15 @@ 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

11
.prettierrc.json Normal file
View File

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

View File

@ -7,11 +7,13 @@ 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) - A fast Python package installer and resolver.
- **Docker and Docker Compose** - Required for running the PostgreSQL database.
- [uv](https://github.com/astral-sh/uv) - Python package manager
- **Docker and Docker Compose** - Required for running 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
@ -23,7 +25,12 @@ Then, sync the project dependencies. This will create a virtual environment in `
uv sync --dev
```
> **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.
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).
### 2. Set Up Environment File
@ -44,6 +51,7 @@ 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).
@ -54,9 +62,12 @@ 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`.
#### Other useful Poe commands:
**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
You can list all available commands by running `uv run poe --help`.
@ -66,6 +77,7 @@ 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

@ -92,6 +92,8 @@ TEMPLATES = [
},
]
ALLOWED_HTML_TAGS = {"b", "i", "u", "br", "em", "mark", "p", "s", "strong"}
WSGI_APPLICATION = "envipath.wsgi.application"
# Database
@ -243,6 +245,7 @@ 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:
@ -343,6 +346,14 @@ LOGIN_EXEMPT_URLS = [
"/password_reset/",
"/reset/",
"/microsoft/",
"/terms",
"/privacy",
"/cookie-policy",
"/about",
"/contact",
"/jobs",
"/cite",
"/legal",
]
# MS AD/Entra
@ -357,3 +368,6 @@ 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

@ -7,6 +7,7 @@ from .models import (
GroupPackagePermission,
Package,
MLRelativeReasoning,
EnviFormer,
Compound,
CompoundStructure,
SimpleAmbitRule,
@ -19,11 +20,12 @@ from .models import (
Setting,
ExternalDatabase,
ExternalIdentifier,
JobLog,
)
class UserAdmin(admin.ModelAdmin):
pass
list_display = ["username", "email", "is_active"]
class UserPackagePermissionAdmin(admin.ModelAdmin):
@ -38,8 +40,14 @@ class GroupPackagePermissionAdmin(admin.ModelAdmin):
pass
class JobLogAdmin(admin.ModelAdmin):
pass
class EPAdmin(admin.ModelAdmin):
search_fields = ["name", "description"]
list_display = ["name", "url", "created"]
ordering = ["-created"]
class PackageAdmin(EPAdmin):
@ -50,6 +58,10 @@ class MLRelativeReasoningAdmin(EPAdmin):
pass
class EnviFormerAdmin(EPAdmin):
pass
class CompoundAdmin(EPAdmin):
pass
@ -102,8 +114,10 @@ admin.site.register(User, UserAdmin)
admin.site.register(UserPackagePermission, UserPackagePermissionAdmin)
admin.site.register(Group, GroupAdmin)
admin.site.register(GroupPackagePermission, GroupPackagePermissionAdmin)
admin.site.register(JobLog, JobLogAdmin)
admin.site.register(Package, PackageAdmin)
admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin)
admin.site.register(EnviFormer, EnviFormerAdmin)
admin.site.register(Compound, CompoundAdmin)
admin.site.register(CompoundStructure, CompoundStructureAdmin)
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)

View File

@ -4,6 +4,7 @@ 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
@ -185,6 +186,12 @@ 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
@ -262,8 +269,9 @@ class GroupManager(object):
@staticmethod
def create_group(current_user, name, description):
g = Group()
g.name = name
g.description = description
# 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.owner = current_user
g.save()
@ -518,8 +526,13 @@ class PackageManager(object):
@transaction.atomic
def create_package(current_user, name: str, description: str = None):
p = Package()
p.name = name
p.description = description
# Clean for potential XSS
p.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None and description.strip() != "":
p.description = nh3.clean(description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
p.save()
up = UserPackagePermission()
@ -1094,28 +1107,29 @@ class SettingManager(object):
model: EPModel = None,
model_threshold: float = None,
):
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 = 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.save()
new_s.save()
if rule_packages is not None:
for r in rule_packages:
s.rule_packages.add(r)
s.save()
new_s.rule_packages.add(r)
new_s.save()
usp = UserSettingPermission()
usp.user = user
usp.setting = s
usp.setting = new_s
usp.permission = Permission.ALL[0]
usp.save()
return s
return new_s
@staticmethod
def get_default_setting(user: User):
@ -1544,7 +1558,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]
@ -1577,9 +1591,7 @@ 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)[
0
]
self.prediction_setting.model.app_domain.assess(c)
)
self.smiles_to_node[c] = SNode(

View File

@ -7,10 +7,11 @@ from epdb.models import MLRelativeReasoning, EnviFormer, Package
class Command(BaseCommand):
"""This command can be run with
`python manage.py create_ml_models [model_names] -d [data_packages] OPTIONAL: -e [eval_packages]`
For example, to train both EnviFormer and MLRelativeReasoning on BBD and SOIL and evaluate them on SLUDGE
the below command would be used:
`python manage.py create_ml_models enviformer mlrr -d bbd soil -e sludge
`python manage.py create_ml_models [model_names] -d [data_packages] FOR MLRR ONLY: -r [rule_packages]
OPTIONAL: -e [eval_packages] -t threshold`
For example, to train both EnviFormer and MLRelativeReasoning on BBD and SOIL and evaluate them on SLUDGE with a
threshold of 0.6, the below command would be used:
`python manage.py create_ml_models enviformer mlrr -d bbd soil -e sludge -t 0.6
"""
def add_arguments(self, parser):
@ -34,6 +35,13 @@ class Command(BaseCommand):
help="Rule Packages mandatory for MLRR",
default=[],
)
parser.add_argument(
"-t",
"--threshold",
type=float,
help="Model prediction threshold",
default=0.5,
)
@transaction.atomic
def handle(self, *args, **options):
@ -67,7 +75,11 @@ class Command(BaseCommand):
return packages
# Iteratively create models in options["model_names"]
print(f"Creating models: {options['model_names']}")
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}")
data_packages = decode_packages(options["data_packages"])
eval_packages = decode_packages(options["eval_packages"])
rule_packages = decode_packages(options["rule_packages"])
@ -78,9 +90,10 @@ class Command(BaseCommand):
pack,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=0.5,
name="EnviFormer - T0.5",
description="EnviFormer transformer",
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']}.",
)
elif model_name == "mlrr":
model = MLRelativeReasoning.create(
@ -88,9 +101,10 @@ class Command(BaseCommand):
rule_packages=rule_packages,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=0.5,
name="ECC - BBD - T0.5",
description="ML Relative Reasoning",
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']}.",
)
else:
raise ValueError(f"Cannot create model of type {model_name}, unknown model type")
@ -100,6 +114,6 @@ class Command(BaseCommand):
print(f"Training {model_name}")
model.build_model()
print(f"Evaluating {model_name}")
model.evaluate_model()
model.evaluate_model(False, eval_packages=eval_packages)
print(f"Saving {model_name}")
model.save()

View File

@ -0,0 +1,59 @@
import json
import os
import tarfile
from tempfile import TemporaryDirectory
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import EnviFormer
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"model",
type=str,
help="Model UUID of the Model to Dump",
)
parser.add_argument("--output", type=str)
def package_dict_and_folder(self, dict_data, folder_path, output_path):
with TemporaryDirectory() as tmpdir:
dict_filename = os.path.join(tmpdir, "data.json")
with open(dict_filename, "w", encoding="utf-8") as f:
json.dump(dict_data, f, indent=2)
with tarfile.open(output_path, "w:gz") as tar:
tar.add(dict_filename, arcname="data.json")
tar.add(folder_path, arcname=os.path.basename(folder_path))
os.remove(dict_filename)
@transaction.atomic
def handle(self, *args, **options):
output = options["output"]
if os.path.exists(output):
raise ValueError(f"Output file {output} already exists")
model = EnviFormer.objects.get(uuid=options["model"])
data = {
"uuid": str(model.uuid),
"name": model.name,
"description": model.description,
"kv": model.kv,
"data_packages_uuids": [str(p.uuid) for p in model.data_packages.all()],
"eval_packages_uuids": [str(p.uuid) for p in model.data_packages.all()],
"threshold": model.threshold,
"eval_results": model.eval_results,
"multigen_eval": model.multigen_eval,
"model_status": model.model_status,
}
model_folder = os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid))
self.package_dict_and_folder(data, model_folder, output)

View File

@ -0,0 +1,81 @@
import json
import os
import shutil
import tarfile
from tempfile import TemporaryDirectory
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
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"input",
type=str,
help=".tar.gz file containing the Model dump.",
)
parser.add_argument(
"package",
type=str,
help="Package UUID where the Model should be loaded to.",
)
def read_dict_and_folder_from_archive(self, archive_path, extract_to="extracted_folder"):
with tarfile.open(archive_path, "r:gz") as tar:
tar.extractall(extract_to)
dict_path = os.path.join(extract_to, "data.json")
if not os.path.exists(dict_path):
raise FileNotFoundError("data.json not found in the archive.")
with open(dict_path, "r", encoding="utf-8") as f:
data_dict = json.load(f)
extracted_items = os.listdir(extract_to)
folders = [item for item in extracted_items if item != "data.json"]
folder_path = os.path.join(extract_to, folders[0]) if folders else None
return data_dict, folder_path
@transaction.atomic
def handle(self, *args, **options):
if not os.path.exists(options["input"]):
raise ValueError(f"Input file {options['input']} does not exist.")
target_package = Package.objects.get(uuid=options["package"])
with TemporaryDirectory() as tmpdir:
data, folder = self.read_dict_and_folder_from_archive(options["input"], tmpdir)
model = EnviFormer()
model.package = target_package
# model.uuid = data["uuid"]
model.name = data["name"]
model.description = data["description"]
model.kv = data["kv"]
model.threshold = float(data["threshold"])
model.eval_results = data["eval_results"]
model.multigen_eval = data["multigen_eval"]
model.model_status = data["model_status"]
model.save()
for p_uuid in data["data_packages_uuids"]:
p = Package.objects.get(uuid=p_uuid)
model.data_packages.add(p)
for p_uuid in data["eval_packages_uuids"]:
p = Package.objects.get(uuid=p_uuid)
model.eval_packages.add(p)
target_folder = os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid))
shutil.copytree(folder, target_folder)
os.rename(
os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid), f"{data['uuid']}.ckpt"),
os.path.join(s.MODEL_DIR, "enviformer", str(model.uuid), f"{model.uuid}.ckpt"),
)

View File

@ -0,0 +1,38 @@
from datetime import date, timedelta
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import JobLog
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
"--cleanup",
type=int,
default=None,
help="Remove all logs older than this number of days. Default is None, which does not remove any logs.",
)
@transaction.atomic
def handle(self, *args, **options):
if options["cleanup"] is not None:
cleanup_dt = date.today() - timedelta(days=options["cleanup"])
print(JobLog.objects.filter(created__lt=cleanup_dt).delete())
logs = JobLog.objects.filter(status="INITIAL")
print(f"Found {logs.count()} logs to update")
updated = 0
for log in logs:
res = log.check_for_update()
if res:
updated += 1
print(f"Updated {updated} logs")
from django.db.models import Count
qs = JobLog.objects.values("status").annotate(total=Count("status"))
for r in qs:
print(r["status"], r["total"])

View File

@ -0,0 +1,66 @@
# Generated by Django 5.2.7 on 2025-10-27 09:39
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0008_enzymelink"),
]
operations = [
migrations.CreateModel(
name="JobLog",
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"
),
),
("task_id", models.UUIDField(unique=True)),
("job_name", models.TextField()),
(
"status",
models.CharField(
choices=[
("INITIAL", "Initial"),
("SUCCESS", "Success"),
("FAILURE", "Failure"),
("REVOKED", "Revoked"),
("IGNORED", "Ignored"),
],
default="INITIAL",
max_length=20,
),
),
("done_at", models.DateTimeField(blank=True, default=None, null=True)),
("task_result", models.TextField(blank=True, default=None, null=True)),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
),
),
],
options={
"abstract": False,
},
),
]

View File

@ -11,6 +11,7 @@ from typing import Union, List, Optional, Dict, Tuple, Set, Any
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
@ -28,7 +29,14 @@ 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 Dataset, ApplicabilityDomainPCA, EnsembleClassifierChain, RelativeReasoning
from utilities.ml import (
RuleBasedDataset,
ApplicabilityDomainPCA,
EnsembleClassifierChain,
RelativeReasoning,
EnviFormerDataset,
Dataset,
)
logger = logging.getLogger(__name__)
@ -310,7 +318,7 @@ class ExternalDatabase(TimeStampedModel):
},
{
"database": ExternalDatabase.objects.get(name="ChEBI"),
"placeholder": "ChEBI ID without prefix e.g. 12345",
"placeholder": "ChEBI ID without prefix e.g. 10576",
},
],
"structure": [
@ -328,7 +336,7 @@ class ExternalDatabase(TimeStampedModel):
},
{
"database": ExternalDatabase.objects.get(name="ChEBI"),
"placeholder": "ChEBI ID without prefix e.g. 12345",
"placeholder": "ChEBI ID without prefix e.g. 10576",
},
],
"reaction": [
@ -342,7 +350,7 @@ class ExternalDatabase(TimeStampedModel):
},
{
"database": ExternalDatabase.objects.get(name="UniProt"),
"placeholder": "Query ID for UniPro e.g. rhea:12345",
"placeholder": "Query ID for UniProt e.g. rhea:12345",
},
],
}
@ -477,7 +485,7 @@ class ChemicalIdentifierMixin(ExternalIdentifierMixin):
return self.add_external_identifier("CAS", cas_number)
def get_pubchem_identifiers(self):
return self.get_external_identifier("PubChem Compound") or self.get_external_identifier(
return self.get_external_identifier("PubChem Compound") | self.get_external_identifier(
"PubChem Substance"
)
@ -802,14 +810,16 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
c = Compound()
c.package = package
if name is None or name.strip() == "":
if name is not None:
# Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Compound {Compound.objects.filter(package=package).count() + 1}"
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 = description.strip()
c.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
c.save()
@ -981,11 +991,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 = name
cs.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None:
cs.description = description
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
cs.smiles = smiles
cs.compound = compound
@ -1187,21 +1197,29 @@ class SimpleAmbitRule(SimpleRule):
r = SimpleAmbitRule()
r.package = package
if name is None or name.strip() == "":
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
r.name = name
if description is not None and description.strip() != "":
r.description = description
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r.smirks = smirks
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
r.reactant_filter_smarts = reactant_filter_smarts
if not FormatConverter.is_valid_smarts(reactant_filter_smarts.strip()):
raise ValueError(f'Reactant Filter SMARTS "{reactant_filter_smarts}" is invalid!')
else:
r.reactant_filter_smarts = reactant_filter_smarts.strip()
if product_filter_smarts is not None and product_filter_smarts.strip() != "":
r.product_filter_smarts = product_filter_smarts
if not FormatConverter.is_valid_smarts(product_filter_smarts.strip()):
raise ValueError(f'Product Filter SMARTS "{product_filter_smarts}" is invalid!')
else:
r.product_filter_smarts = product_filter_smarts.strip()
r.save()
return r
@ -1402,12 +1420,11 @@ 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 = name
r.name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if description is not None and name.strip() != "":
r.description = description
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r.multi_step = multi_step
@ -1715,14 +1732,15 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
):
pw = Pathway()
pw.package = package
if name is None or name.strip() == "":
if name is not None:
# Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Pathway {Pathway.objects.filter(package=package).count() + 1}"
pw.name = name
if description is not None and description.strip() != "":
pw.description = description
pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
pw.save()
try:
@ -2017,11 +2035,16 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
for node in end_nodes:
e.end_nodes.add(node)
if name is None:
# Clean for potential XSS
# Cleaning technically not needed as it is also done in Reaction.create, including it here for consistency
if name is not None:
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"Reaction {pathway.package.reactions.count() + 1}"
if description is None:
description = s.DEFAULT_VALUES["description"]
description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
r = Reaction.create(
pathway.package,
@ -2175,7 +2198,7 @@ class PackageBasedModel(EPModel):
applicable_rules = self.applicable_rules
reactions = list(self._get_reactions())
ds = Dataset.generate_dataset(reactions, applicable_rules, educts_only=True)
ds = RuleBasedDataset.generate_dataset(reactions, applicable_rules, educts_only=True)
end = datetime.now()
logger.debug(f"build_dataset took {(end - start).total_seconds()} seconds")
@ -2184,7 +2207,7 @@ class PackageBasedModel(EPModel):
ds.save(f)
return ds
def load_dataset(self) -> "Dataset":
def load_dataset(self) -> "Dataset | RuleBasedDataset | EnviFormerDataset":
ds_path = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.pkl")
return Dataset.load(ds_path)
@ -2225,10 +2248,18 @@ class PackageBasedModel(EPModel):
self.model_status = self.BUILT_NOT_EVALUATED
self.save()
def evaluate_model(self):
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
if self.model_status != self.BUILT_NOT_EVALUATED:
raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
if multigen:
self.multigen_eval = multigen
self.save()
if eval_packages is not None:
for p in eval_packages:
self.eval_packages.add(p)
self.model_status = self.EVALUATING
self.save()
@ -2335,37 +2366,39 @@ class PackageBasedModel(EPModel):
eval_reactions = list(
Reaction.objects.filter(package__in=self.eval_packages.all()).distinct()
)
ds = Dataset.generate_dataset(eval_reactions, self.applicable_rules, educts_only=True)
ds = RuleBasedDataset.generate_dataset(
eval_reactions, self.applicable_rules, educts_only=True
)
if isinstance(self, RuleBasedRelativeReasoning):
X = np.array(ds.X(exclude_id_col=False, na_replacement=None))
y = np.array(ds.y(na_replacement=np.nan))
X = ds.X(exclude_id_col=False, na_replacement=None).to_numpy()
y = ds.y(na_replacement=np.nan).to_numpy()
else:
X = np.array(ds.X(na_replacement=np.nan))
y = np.array(ds.y(na_replacement=np.nan))
X = ds.X(na_replacement=np.nan).to_numpy()
y = ds.y(na_replacement=np.nan).to_numpy()
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 = np.array(ds.X(exclude_id_col=False, na_replacement=None))
y = np.array(ds.y(na_replacement=np.nan))
X = ds.X(exclude_id_col=False, na_replacement=None).to_numpy()
y = ds.y(na_replacement=np.nan).to_numpy()
else:
X = np.array(ds.X(na_replacement=np.nan))
y = np.array(ds.y(na_replacement=np.nan))
X = ds.X(na_replacement=np.nan).to_numpy()
y = ds.y(na_replacement=np.nan).to_numpy()
n_splits = 20
n_splits = kwargs.get("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=10)(
models = Parallel(n_jobs=min(10, len(splits)))(
delayed(train_func)(X, y, train_index, self._model_args())
for train_index, _ in splits
)
evaluations = Parallel(n_jobs=10)(
evaluations = Parallel(n_jobs=min(10, len(splits)))(
delayed(evaluate_sg)(model, X, y, test_index, self.threshold)
for model, (_, test_index) in zip(models, splits)
)
@ -2525,7 +2558,6 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
package: "Package",
rule_packages: List["Package"],
data_packages: List["Package"],
eval_packages: List["Package"],
threshold: float = 0.5,
min_count: int = 10,
max_count: int = 0,
@ -2534,14 +2566,15 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
):
rbrr = RuleBasedRelativeReasoning()
rbrr.package = package
if name is None or name.strip() == "":
if name is not None:
# Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"RuleBasedRelativeReasoning {RuleBasedRelativeReasoning.objects.filter(package=package).count() + 1}"
rbrr.name = name
if description is not None and description.strip() != "":
rbrr.description = description
rbrr.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.")
@ -2574,19 +2607,15 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
for p in rule_packages:
rbrr.data_packages.add(p)
if eval_packages:
for p in eval_packages:
rbrr.eval_packages.add(p)
rbrr.save()
return rbrr
def _fit_model(self, ds: Dataset):
def _fit_model(self, ds: RuleBasedDataset):
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
@ -2596,7 +2625,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):
@ -2632,7 +2661,6 @@ class MLRelativeReasoning(PackageBasedModel):
package: "Package",
rule_packages: List["Package"],
data_packages: List["Package"],
eval_packages: List["Package"],
threshold: float = 0.5,
name: "str" = None,
description: str = None,
@ -2643,14 +2671,15 @@ class MLRelativeReasoning(PackageBasedModel):
):
mlrr = MLRelativeReasoning()
mlrr.package = package
if name is None or name.strip() == "":
if name is not None:
# Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"MLRelativeReasoning {MLRelativeReasoning.objects.filter(package=package).count() + 1}"
mlrr.name = name
if description is not None and description.strip() != "":
mlrr.description = description
mlrr.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.")
@ -2672,10 +2701,6 @@ class MLRelativeReasoning(PackageBasedModel):
for p in rule_packages:
mlrr.data_packages.add(p)
if eval_packages:
for p in eval_packages:
mlrr.eval_packages.add(p)
if build_app_domain:
ad = ApplicabilityDomain.create(
mlrr,
@ -2689,11 +2714,11 @@ class MLRelativeReasoning(PackageBasedModel):
return mlrr
def _fit_model(self, ds: Dataset):
def _fit_model(self, ds: RuleBasedDataset):
X, y = ds.X(na_replacement=np.nan), ds.y(na_replacement=np.nan)
model = EnsembleClassifierChain(**s.DEFAULT_MODEL_PARAMS)
model.fit(X, y)
model.fit(X.to_numpy(), y.to_numpy())
return model
def _model_args(self):
@ -2716,7 +2741,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())
pred = self.model.predict_proba(classify_ds.X().to_numpy())
res = MLRelativeReasoning.combine_products_and_probs(
self.applicable_rules, pred[0], classify_prods[0]
@ -2761,7 +2786,9 @@ class ApplicabilityDomain(EnviPathModel):
@cached_property
def training_set_probs(self):
return joblib.load(os.path.join(s.MODEL_DIR, f"{self.model.uuid}_train_probs.pkl"))
ds = self.model.load_dataset()
col_ids = ds.block_indices("prob")
return ds[:, col_ids]
def build(self):
ds = self.model.load_dataset()
@ -2769,9 +2796,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())
f = os.path.join(s.MODEL_DIR, f"{self.model.uuid}_train_probs.pkl")
joblib.dump(probs, f)
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"))
ad = ApplicabilityDomainPCA(num_neighbours=self.num_neighbours)
ad.build(ds)
@ -2794,15 +2821,20 @@ 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()
if isinstance(structure, CompoundStructure):
smiles = structure.smiles
else:
smiles = structure
smiles = []
for struct in structures:
if isinstance(struct, CompoundStructure):
smiles.append(structures.smiles)
else:
smiles.append(structures)
assessment_ds, assessment_prods = ds.classification_dataset(
[structure], self.model.applicable_rules
structures, self.model.applicable_rules
)
# qualified_neighbours_per_rule is a nested dictionary structured as:
@ -2816,82 +2848,61 @@ 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[int, Dict[int, List[int]]] = defaultdict(
lambda: defaultdict(list)
)
qualified_neighbours_per_rule: Dict = {}
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)
import polars as pl
probs = self.training_set_probs
# preds = self.model.model.predict_proba(assessment_ds.X())
# 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)}
preds = self.model.combine_products_and_probs(
self.model.applicable_rules,
self.model.model.predict_proba(assessment_ds.X())[0],
self.model.model.predict_proba(assessment_ds.X().to_numpy())[0],
assessment_prods[0],
)
assessments = list()
# loop through our assessment dataset
for i, instance in enumerate(assessment_ds):
for i, instance in enumerate(assessment_ds[:, assessment_ds.struct_features()]):
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_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
for rule_uuid, train_instances in qualified_neighbours_per_rule[i].items():
# compute tanimoto distance for all neighbours and add to dataset
dists = self._compute_distances(
instance.X()[0][sf[0] : sf[1]],
[ti[1].X()[0][sf[0] : sf[1]] for ti in train_instances],
assessment_ds[i, assessment_ds.struct_features()].to_numpy()[0],
train_instances[:, train_instances.struct_features()].to_numpy(),
)
dists_with_index = list()
for ti, dist in zip(train_instances, dists):
dists_with_index.append((ti[0], dist[1]))
train_instances = train_instances.with_columns(dist=pl.Series(dists))
# sort them in a descending way and take at most `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]
# 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
]
# compute average distance
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
rule_reliabilities[rule_uuid] = (
train_instances.select(pl.mean("dist")).fill_nan(0.0).item()
)
# for local_compatibility we'll need the datasets for the indices having the highest similarity
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
local_compatibilities[rule_uuid] = self._compute_compatibility(
rule_uuid, train_instances
)
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
]
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()
ad_res = {
"ad_params": {
@ -2902,23 +2913,21 @@ class ApplicabilityDomain(EnviPathModel):
"local_compatibility_threshold": self.local_compatibilty_threshold,
},
"assessment": {
"smiles": smiles,
"inside_app_domain": self.pca.is_applicable(instance)[0],
"smiles": smiles[i],
"inside_app_domain": self.pca.is_applicable(assessment_ds[i])[0],
},
}
transformations = list()
for rule_idx in rule_reliabilities.keys():
rule = Rule.objects.get(
uuid=instance.columns[instance.observed()[0] + rule_idx].replace("obs_", "")
)
for rule_uuid in rule_reliabilities.keys():
rule = Rule.objects.get(uuid=rule_uuid)
rule_data = rule.simple_json()
rule_data["image"] = f"{rule.url}?image=svg"
neighbors = []
for n, n_prob in zip(
neighbours_per_rule[rule_idx], neighbor_probs_per_rule[rule_idx]
neighbours_per_rule[rule_uuid], neighbor_probs_per_rule[rule_uuid]
):
neighbor = n.simple_json()
neighbor["image"] = f"{n.url}?image=svg"
@ -2935,14 +2944,14 @@ class ApplicabilityDomain(EnviPathModel):
transformation = {
"rule": rule_data,
"reliability": rule_reliabilities[rule_idx],
"reliability": rule_reliabilities[rule_uuid],
# 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_idx],
"probability": preds[rule_idx].probability,
"local_compatibility": local_compatibilities[rule_uuid],
"probability": preds[rule_to_i[rule_uuid]].probability,
"transformation_products": [
x.product_set for x in preds[rule_idx].product_sets
x.product_set for x in preds[rule_to_i[rule_uuid]].product_sets
],
"times_triggered": ds.times_triggered(str(rule.uuid)),
"neighbors": neighbors,
@ -2960,32 +2969,24 @@ class ApplicabilityDomain(EnviPathModel):
def _compute_distances(classify_instance: List[int], train_instances: List[List[int]]):
from utilities.ml import tanimoto_distance
distances = [
(i, tanimoto_distance(classify_instance, train))
for i, train in enumerate(train_instances)
]
distances = [tanimoto_distance(classify_instance, train) for train in train_instances]
return distances
@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
def _compute_compatibility(self, rule_idx: int, neighbours: "RuleBasedDataset"):
accuracy = 0.0
import polars as pl
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)
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
if tp + tn > 0.0:
accuracy = (tp + tn) / (tp + tn + fp + fn)
return accuracy
@ -2995,7 +2996,6 @@ class EnviFormer(PackageBasedModel):
def create(
package: "Package",
data_packages: List["Package"],
eval_packages: List["Package"],
threshold: float = 0.5,
name: "str" = None,
description: str = None,
@ -3006,14 +3006,15 @@ class EnviFormer(PackageBasedModel):
):
mod = EnviFormer()
mod.package = package
if name is None or name.strip() == "":
if name is not None:
# Clean for potential XSS
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
if name is None or name == "":
name = f"EnviFormer {EnviFormer.objects.filter(package=package).count() + 1}"
mod.name = name
if description is not None and description.strip() != "":
mod.description = description
mod.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if threshold is None or (threshold <= 0 or 1 <= threshold):
raise ValueError("Threshold must be a float between 0 and 1.")
@ -3028,10 +3029,6 @@ class EnviFormer(PackageBasedModel):
for p in data_packages:
mod.data_packages.add(p)
if eval_packages:
for p in eval_packages:
mod.eval_packages.add(p)
# if build_app_domain:
# ad = ApplicabilityDomain.create(mod, app_domain_num_neighbours, app_domain_reliability_threshold,
# app_domain_local_compatibility_threshold)
@ -3045,7 +3042,8 @@ class EnviFormer(PackageBasedModel):
from enviformer import load
ckpt = os.path.join(s.MODEL_DIR, "enviformer", str(self.uuid), f"{self.uuid}.ckpt")
return load(device=s.ENVIFORMER_DEVICE, ckpt_path=ckpt)
mod = load(device=s.ENVIFORMER_DEVICE, ckpt_path=ckpt)
return mod
def predict(self, smiles) -> List["PredictionResult"]:
return self.predict_batch([smiles])[0]
@ -3059,8 +3057,12 @@ class EnviFormer(PackageBasedModel):
for smiles in smiles_list
]
logger.info(f"Submitting {canon_smiles} to {self.name}")
start = datetime.now()
products_list = self.model.predict_batch(canon_smiles)
logger.info(f"Got results {products_list}")
end = datetime.now()
logger.info(
f"Prediction took {(end - start).total_seconds():.2f} seconds. Got results {products_list}"
)
results = []
for products in products_list:
@ -3086,42 +3088,24 @@ class EnviFormer(PackageBasedModel):
self.save()
start = datetime.now()
# Standardise reactions for the training data, EnviFormer ignores stereochemistry currently
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()
]
)
ds.append(f"{educts}>>{products}")
ds = EnviFormerDataset.generate_dataset(self._get_reactions())
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")
with open(f, "w") as d_file:
json.dump(ds, d_file)
ds.save(f)
return ds
def load_dataset(self) -> "Dataset":
def load_dataset(self):
ds_path = os.path.join(s.MODEL_DIR, f"{self.uuid}_ds.json")
with open(ds_path) as d_file:
ds = json.load(d_file)
return ds
return EnviFormerDataset.load(ds_path)
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, s.MODEL_DIR, str(self.uuid), device=s.ENVIFORMER_DEVICE)
model = fine_tune(ds.X(), ds.y(), 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
@ -3137,28 +3121,35 @@ class EnviFormer(PackageBasedModel):
args = {"clz": "EnviFormer"}
return args
def evaluate_model(self):
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
if self.model_status != self.BUILT_NOT_EVALUATED:
raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
if multigen:
self.multigen_eval = multigen
self.save()
if eval_packages is not None:
for p in eval_packages:
self.eval_packages.add(p)
self.model_status = self.EVALUATING
self.save()
def evaluate_sg(test_reactions, predictions, model_thresh):
def evaluate_sg(test_ds, 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_reactions:
reactant, true_product_set = r.split(">>")
for r in test_ds:
reactant, true_product_set = r
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, true_product = test_reactions[k].split(">>")
reactant, _ = test_ds[k, "educts"], test_ds[k, "products"]
pred_dict.setdefault(reactant, {"predict": [], "scores": []})
for smiles, proba in zip(pred_smiles, pred_proba):
smiles = set(smiles.split("."))
@ -3193,7 +3184,7 @@ class EnviFormer(PackageBasedModel):
break
# Recall is TP (correct) / TP + FN (len(test_reactions))
rec = {f"{k:.2f}": v / len(test_reactions) for k, v in correct.items()}
rec = {f"{k:.2f}": v / len(test_ds) 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()
@ -3272,47 +3263,35 @@ 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 = []
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()
]
)
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])
ds = EnviFormerDataset.generate_dataset(
Reaction.objects.filter(package__in=self.eval_packages.all()).distinct()
)
test_result = self.model.predict_batch(ds.X())
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 = 20
shuff = ShuffleSplit(n_splits=n_splits, test_size=0.25, random_state=42)
n_splits = kwargs.get("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[i] for i in train_index]
test = [ds[i] for i in test_index]
train = ds[train_index]
test = ds[test_index]
start = datetime.now()
model = fine_tune(train, s.MODEL_DIR, str(split_id), device=s.ENVIFORMER_DEVICE)
model = fine_tune(
train.X(), train.y(), s.MODEL_DIR, str(split_id), device=s.ENVIFORMER_DEVICE
)
end = datetime.now()
logger.debug(
f"EnviFormer finetuning took {(end - start).total_seconds():.2f} seconds"
)
model.to(s.ENVIFORMER_DEVICE)
test_result = model.predict_batch([smirk.split(">>")[0] for smirk in test])
test_result = model.predict_batch(test.X())
single_gen_results.append(evaluate_sg(test, test_result, self.threshold))
self.eval_results = self.compute_averages(single_gen_results)
@ -3365,7 +3344,7 @@ class EnviFormer(PackageBasedModel):
# Compute splits of the collected pathway and evaluate. Like single gen we train and evaluate in each
# iteration instead of storing all trained models.
for split_id, (train, test) in enumerate(
ShuffleSplit(n_splits=n_splits, test_size=0.25, random_state=42).split(pathways)
ShuffleSplit(n_splits=n_splits, test_size=0.1, random_state=42).split(pathways)
):
train_pathways = [pathways[i] for i in train]
test_pathways = [pathways[i] for i in test]
@ -3391,23 +3370,12 @@ class EnviFormer(PackageBasedModel):
):
overlap += 1
continue
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}")
train_reactions.append(reaction)
train_ds = EnviFormerDataset.generate_dataset(train_reactions)
logging.debug(
f"{overlap} compounds had to be removed from multigen split due to overlap within pathways"
)
model = fine_tune(train_reactions, s.MODEL_DIR, f"mg_{split_id}")
model = fine_tune(train_ds.X(), train_ds.y(), s.MODEL_DIR, f"mg_{split_id}")
multi_gen_results.append(evaluate_mg(model, test_pathways, self.threshold))
self.eval_results.update(
@ -3456,41 +3424,44 @@ class Scenario(EnviPathModel):
scenario_type: str,
additional_information: List["EnviPyModel"],
):
s = Scenario()
s.package = package
if name is None or name.strip() == "":
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 == "":
name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}"
s.name = name
new_s.name = name
if description is not None and description.strip() != "":
s.description = description
new_s.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
if scenario_date is not None and scenario_date.strip() != "":
s.scenario_date = scenario_date
new_s.scenario_date = nh3.clean(scenario_date).strip()
if scenario_type is not None and scenario_type.strip() != "":
s.scenario_type = scenario_type
new_s.scenario_type = scenario_type
add_inf = defaultdict(list)
for info in additional_information:
cls_name = info.__class__.__name__
ai_data = json.loads(info.model_dump_json())
# Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(info.model_dump_json()).strip())
ai_data["uuid"] = f"{uuid4()}"
add_inf[cls_name].append(ai_data)
s.additional_information = add_inf
new_s.additional_information = add_inf
s.save()
new_s.save()
return s
return new_s
@transaction.atomic
def add_additional_information(self, data: "EnviPyModel"):
cls_name = data.__class__.__name__
ai_data = json.loads(data.model_dump_json())
# Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(data.model_dump_json()).strip())
ai_data["uuid"] = f"{uuid4()}"
if cls_name not in self.additional_information:
@ -3525,7 +3496,8 @@ class Scenario(EnviPathModel):
new_ais = defaultdict(list)
for k, vals in data.items():
for v in vals:
ai_data = json.loads(v.model_dump_json())
# Clean for potential XSS hidden in the additional information fields.
ai_data = json.loads(nh3.clean(v.model_dump_json()).strip())
if hasattr(v, "uuid"):
ai_data["uuid"] = str(v.uuid)
else:
@ -3664,3 +3636,53 @@ class Setting(EnviPathModel):
self.public = True
self.global_default = True
self.save()
class JobLogStatus(models.TextChoices):
INITIAL = "INITIAL", "Initial"
SUCCESS = "SUCCESS", "Success"
FAILURE = "FAILURE", "Failure"
REVOKED = "REVOKED", "Revoked"
IGNORED = "IGNORED", "Ignored"
class JobLog(TimeStampedModel):
user = models.ForeignKey("epdb.User", models.CASCADE)
task_id = models.UUIDField(unique=True)
job_name = models.TextField(null=False, blank=False)
status = models.CharField(
max_length=20,
choices=JobLogStatus.choices,
default=JobLogStatus.INITIAL,
)
done_at = models.DateTimeField(null=True, blank=True, default=None)
task_result = models.TextField(null=True, blank=True, default=None)
def check_for_update(self):
async_res = self.get_result()
new_status = async_res.state
TERMINAL_STATES = [
"SUCCESS",
"FAILURE",
"REVOKED",
"IGNORED",
]
if new_status != self.status and new_status in TERMINAL_STATES:
self.status = new_status
self.done_at = async_res.date_done
if new_status == "SUCCESS":
self.task_result = async_res.result
self.save()
return True
return False
def get_result(self):
from celery.result import AsyncResult
return AsyncResult(str(self.task_id))

View File

@ -1,12 +1,58 @@
import csv
import io
import logging
from typing import Optional
from datetime import datetime
from typing import Any, Callable, List, Optional
from uuid import uuid4
from celery import shared_task
from epdb.models import Pathway, Node, EPModel, Setting
from epdb.logic import SPathway
from celery.utils.functional import LRUCache
from epdb.logic import SPathway
from epdb.models import EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User, Edge
logger = logging.getLogger(__name__)
ML_CACHE = LRUCache(3) # Cache the three most recent ML models to reduce load times.
def get_ml_model(model_pk: int):
if model_pk not in ML_CACHE:
ML_CACHE[model_pk] = EPModel.objects.get(id=model_pk)
return ML_CACHE[model_pk]
def dispatch_eager(user: "User", job: Callable, *args, **kwargs):
try:
x = job(*args, **kwargs)
log = JobLog()
log.user = user
log.task_id = uuid4()
log.job_name = job.__name__
log.status = "SUCCESS"
log.done_at = datetime.now()
log.task_result = str(x) if x else None
log.save()
return x
except Exception as e:
logger.exception(e)
raise e
def dispatch(user: "User", job: Callable, *args, **kwargs):
try:
x = job.delay(*args, **kwargs)
log = JobLog()
log.user = user
log.task_id = x.task_id
log.job_name = job.__name__
log.status = "INITIAL"
log.save()
return x.result
except Exception as e:
logger.exception(e)
raise e
@shared_task(queue="background")
@ -16,7 +62,7 @@ def mul(a, b):
@shared_task(queue="predict")
def predict_simple(model_pk: int, smiles: str):
mod = EPModel.objects.get(id=model_pk)
mod = get_ml_model(model_pk)
res = mod.predict(smiles)
return res
@ -26,17 +72,55 @@ def send_registration_mail(user_pk: int):
pass
@shared_task(queue="model")
def build_model(model_pk: int):
@shared_task(bind=True, queue="model")
def build_model(self, model_pk: int):
mod = EPModel.objects.get(id=model_pk)
mod.build_dataset()
mod.build_model()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="RUNNING", task_result=mod.url)
try:
mod.build_dataset()
mod.build_model()
except Exception as e:
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(
status="FAILED", task_result=mod.url
)
raise e
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=mod.url)
return mod.url
@shared_task(queue="model")
def evaluate_model(model_pk: int):
@shared_task(bind=True, queue="model")
def evaluate_model(self, model_pk: int, multigen: bool, package_pks: Optional[list] = None):
packages = None
if package_pks:
packages = Package.objects.filter(pk__in=package_pks)
mod = EPModel.objects.get(id=model_pk)
mod.evaluate_model()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="RUNNING", task_result=mod.url)
try:
mod.evaluate_model(multigen, eval_packages=packages)
except Exception as e:
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(
status="FAILED", task_result=mod.url
)
raise e
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=mod.url)
return mod.url
@shared_task(queue="model")
@ -45,16 +129,26 @@ def retrain(model_pk: int):
mod.retrain()
@shared_task(queue="predict")
@shared_task(bind=True, queue="predict")
def predict(
pw_pk: int, pred_setting_pk: int, limit: Optional[int] = None, node_pk: Optional[int] = None
self,
pw_pk: int,
pred_setting_pk: int,
limit: Optional[int] = None,
node_pk: Optional[int] = None,
) -> Pathway:
pw = Pathway.objects.get(id=pw_pk)
setting = Setting.objects.get(id=pred_setting_pk)
# If the setting has a model add/restore it from the cache
if setting.model is not None:
setting.model = get_ml_model(setting.model.pk)
pw.kv.update(**{"status": "running"})
pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="RUNNING", task_result=pw.url)
try:
# regular prediction
if limit is not None:
@ -79,7 +173,111 @@ def predict(
except Exception as e:
pw.kv.update({"status": "failed"})
pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(
status="FAILED", task_result=pw.url
)
raise e
pw.kv.update(**{"status": "completed"})
pw.save()
if JobLog.objects.filter(task_id=self.request.id).exists():
JobLog.objects.filter(task_id=self.request.id).update(status="SUCCESS", task_result=pw.url)
return pw.url
@shared_task(bind=True, queue="background")
def identify_missing_rules(
self,
pw_pks: List[int],
rule_package_pk: int,
):
from utilities.misc import PathwayUtils
rules = Package.objects.get(pk=rule_package_pk).get_applicable_rules()
rows: List[Any] = []
header = [
"Package Name",
"Pathway Name",
"Educt Name",
"Educt SMILES",
"Reaction Name",
"Reaction SMIRKS",
"Triggered Rules",
"Reactant SMARTS",
"Product SMARTS",
"Product Names",
"Product SMILES",
]
rows.append(header)
for pw in Pathway.objects.filter(pk__in=pw_pks):
pu = PathwayUtils(pw)
missing_rules = pu.find_missing_rules(rules)
package_name = pw.package.name
pathway_name = pw.name
for edge_url, rule_chain in missing_rules.items():
row: List[Any] = [package_name, pathway_name]
edge = Edge.objects.get(url=edge_url)
educts = edge.start_nodes.all()
for educt in educts:
row.append(educt.default_node_label.name)
row.append(educt.default_node_label.smiles)
row.append(edge.edge_label.name)
row.append(edge.edge_label.smirks())
rule_names = []
reactant_smarts = []
product_smarts = []
for r in rule_chain:
r = Rule.objects.get(url=r[0])
rule_names.append(r.name)
rs = r.reactants_smarts
if isinstance(rs, set):
rs = list(rs)
ps = r.products_smarts
if isinstance(ps, set):
ps = list(ps)
reactant_smarts.append(rs)
product_smarts.append(ps)
row.append(rule_names)
row.append(reactant_smarts)
row.append(product_smarts)
products = edge.end_nodes.all()
product_names = []
product_smiles = []
for product in products:
product_names.append(product.default_node_label.name)
product_smiles.append(product.default_node_label.smiles)
row.append(product_names)
row.append(product_smiles)
rows.append(row)
buffer = io.StringIO()
writer = csv.writer(buffer)
writer.writerows(rows)
buffer.seek(0)
return buffer.getvalue()

View File

@ -1,8 +1,21 @@
from django import template
from pydantic import AnyHttpUrl, ValidationError
from pydantic.type_adapter import TypeAdapter
register = template.Library()
url_adapter = TypeAdapter(AnyHttpUrl)
@register.filter
def classname(obj):
return obj.__class__.__name__
@register.filter
def is_url(value):
try:
url_adapter.validate_python(value)
return True
except ValidationError:
return False

View File

@ -190,6 +190,16 @@ urlpatterns = [
re_path(r"^indigo/dearomatize$", v.dearomatize, name="indigo_dearomatize"),
re_path(r"^indigo/layout$", v.layout, name="indigo_layout"),
re_path(r"^depict$", v.depict, name="depict"),
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

@ -10,6 +10,7 @@ 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
@ -47,6 +48,7 @@ from .models import (
ExternalDatabase,
ExternalIdentifier,
EnzymeLink,
JobLog,
)
logger = logging.getLogger(__name__)
@ -84,7 +86,11 @@ def login(request):
from django.contrib.auth import authenticate
from django.contrib.auth import login
username = request.POST.get("username")
username = request.POST.get("username").strip()
if username != request.POST.get("username"):
context["message"] = "Login failed!"
return render(request, "static/login.html", context)
password = request.POST.get("password")
# Get email for username and check if the account is active
@ -99,6 +105,7 @@ 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:
@ -136,9 +143,14 @@ def register(request):
context = get_base_context(request)
if request.method == "GET":
context["title"] = "enviPath"
context["next"] = request.GET.get("next", "")
return render(request, "static/register.html", context)
# 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)
elif request.method == "POST":
context["title"] = "enviPath"
if next := request.POST.get("next"):
@ -151,18 +163,18 @@ def register(request):
if not (username and email and password):
context["message"] = "Invalid username/email/password"
return render(request, "static/register.html", context)
return render(request, "static/login.html", context)
if password != rpassword or password == "":
context["message"] = "Registration failed, provided passwords differ!"
return render(request, "static/register.html", context)
return render(request, "static/login.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/register.html", context)
return render(request, "static/login.html", context)
if s.ADMIN_APPROVAL_REQUIRED:
context["success_message"] = (
@ -236,6 +248,7 @@ 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,
},
}
@ -668,7 +681,8 @@ def search(request):
if request.method == "GET":
package_urls = request.GET.getlist("packages")
searchterm = request.GET.get("search")
searchterm = request.GET.get("search", "").strip()
mode = request.GET.get("mode")
# add HTTP_ACCEPT check to differentiate between index and ajax call
@ -754,8 +768,8 @@ def package_models(request, package_uuid):
context["unreviewed_objects"] = unreviewed_model_qs
context["model_types"] = {
"ML Relative Reasoning": "ml-relative-reasoning",
"Rule Based Relative Reasoning": "rule-based-relative-reasoning",
"ML Relative Reasoning": "mlrr",
"Rule Based Relative Reasoning": "rbrr",
}
if s.FLAGS.get("ENVIFORMER", False):
@ -769,75 +783,72 @@ 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")
model_type = request.POST.get("model-type")
# Generic fields for ML and Rule Based
rule_packages = request.POST.getlist("model-rule-packages")
data_packages = request.POST.getlist("model-data-packages")
# Generic params
params = {
"package": current_package,
"name": name,
"description": description,
"data_packages": [
PackageManager.get_package_by_url(current_user, p) for p in data_packages
],
}
if model_type == "enviformer":
threshold = float(request.POST.get(f"{model_type}-threshold", 0.5))
threshold = float(request.POST.get("model-threshold", 0.5))
params["threshold"] = threshold
mod = EnviFormer.create(current_package, name, description, threshold)
mod = EnviFormer.create(**params)
elif model_type == "mlrr":
# ML Specific
threshold = float(request.POST.get("model-threshold", 0.5))
# TODO handle additional fingerprinter
# fingerprinter = request.POST.get("model-fingerprinter")
elif model_type == "ml-relative-reasoning" or model_type == "rule-based-relative-reasoning":
# Generic fields for ML and Rule Based
rule_packages = request.POST.getlist("package-based-relative-reasoning-rule-packages")
data_packages = request.POST.getlist("package-based-relative-reasoning-data-packages")
eval_packages = request.POST.getlist(
"package-based-relative-reasoning-evaluation-packages", []
)
params["rule_packages"] = [
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
]
# Generic params
params = {
"package": current_package,
"name": name,
"description": description,
"rule_packages": [
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
],
"data_packages": [
PackageManager.get_package_by_url(current_user, p) for p in data_packages
],
"eval_packages": [
PackageManager.get_package_by_url(current_user, p) for p in eval_packages
],
}
# App Domain related parameters
build_ad = request.POST.get("build-app-domain", False) == "on"
num_neighbors = request.POST.get("num-neighbors", 5)
reliability_threshold = request.POST.get("reliability-threshold", 0.5)
local_compatibility_threshold = request.POST.get("local-compatibility-threshold", 0.5)
if model_type == "ml-relative-reasoning":
# ML Specific
threshold = float(request.POST.get(f"{model_type}-threshold", 0.5))
# TODO handle additional fingerprinter
# fingerprinter = request.POST.get(f"{model_type}-fingerprinter")
params["threshold"] = threshold
# params['fingerprinter'] = fingerprinter
params["build_app_domain"] = build_ad
params["app_domain_num_neighbours"] = num_neighbors
params["app_domain_reliability_threshold"] = reliability_threshold
params["app_domain_local_compatibility_threshold"] = local_compatibility_threshold
# App Domain related parameters
build_ad = request.POST.get("build-app-domain", False) == "on"
num_neighbors = request.POST.get("num-neighbors", 5)
reliability_threshold = request.POST.get("reliability-threshold", 0.5)
local_compatibility_threshold = request.POST.get(
"local-compatibility-threshold", 0.5
)
mod = MLRelativeReasoning.create(**params)
elif model_type == "rbrr":
params["rule_packages"] = [
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
]
params["threshold"] = threshold
# params['fingerprinter'] = fingerprinter
params["build_app_domain"] = build_ad
params["app_domain_num_neighbours"] = num_neighbors
params["app_domain_reliability_threshold"] = reliability_threshold
params["app_domain_local_compatibility_threshold"] = local_compatibility_threshold
mod = MLRelativeReasoning.create(**params)
else:
mod = RuleBasedRelativeReasoning.create(**params)
from .tasks import build_model
build_model.delay(mod.pk)
mod = RuleBasedRelativeReasoning.create(**params)
elif s.FLAGS.get("PLUGINS", False) and model_type in s.CLASSIFIER_PLUGINS.values():
pass
else:
return error(
request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
)
return redirect(mod.url)
from .tasks import dispatch, build_model
dispatch(current_user, build_model, mod.pk)
return redirect(mod.url)
else:
return HttpResponseNotAllowed(["GET", "POST"])
@ -865,6 +876,10 @@ def package_model(request, package_uuid, model_uuid):
return JsonResponse({"error": f'"{smiles}" is not a valid SMILES'}, status=400)
if classify:
from epdb.tasks import dispatch_eager, predict_simple
res = dispatch_eager(current_user, predict_simple, current_model.pk, stand_smiles)
pred_res = current_model.predict(stand_smiles)
res = []
@ -888,7 +903,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)[0]
app_domain_assessment = current_model.app_domain.assess(stand_smiles)
return JsonResponse(app_domain_assessment, safe=False)
context = get_base_context(request)
@ -909,15 +924,37 @@ def package_model(request, package_uuid, model_uuid):
current_model.delete()
return redirect(current_package.url + "/model")
elif hidden == "evaluate":
from .tasks import evaluate_model
from .tasks import dispatch, evaluate_model
eval_type = request.POST.get("model-evaluation-type")
if eval_type not in ["sg", "mg"]:
return error(
request,
"Invalid evaluation type",
f'Evaluation type "{eval_type}" is not supported. Only "sg" and "mg" are supported.',
)
multigen = eval_type == "mg"
eval_packages = request.POST.getlist("model-evaluation-packages")
eval_package_ids = [
PackageManager.get_package_by_url(current_user, p).id for p in eval_packages
]
dispatch(current_user, evaluate_model, current_model.pk, multigen, eval_package_ids)
evaluate_model.delay(current_model.pk)
return redirect(current_model.url)
else:
return HttpResponseBadRequest()
else:
name = request.POST.get("model-name", "").strip()
description = request.POST.get("model-description", "").strip()
# 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()
if any([name, description]):
if name:
@ -1019,8 +1056,16 @@ 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"
@ -1129,7 +1174,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")
compound_smiles = request.POST.get("compound-smiles").strip()
compound_description = request.POST.get("compound-description")
c = Compound.create(current_package, compound_smiles, compound_name, compound_description)
@ -1182,8 +1227,16 @@ def package_compound(request, package_uuid, compound_uuid):
return JsonResponse({"success": current_compound.url})
new_compound_name = request.POST.get("compound-name", "").strip()
new_compound_description = request.POST.get("compound-description", "").strip()
# 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()
if new_compound_name:
current_compound.name = new_compound_name
@ -1248,10 +1301,19 @@ 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")
structure_smiles = request.POST.get("structure-smiles").strip()
structure_description = request.POST.get("structure-description")
cs = current_compound.add_structure(structure_smiles, structure_name, structure_description)
try:
cs = current_compound.add_structure(
structure_smiles, structure_name, structure_description
)
except ValueError:
return error(
request,
"Adding structure failed!",
"The structure could not be added as normalized structures don't match!",
)
return redirect(cs.url)
@ -1310,8 +1372,16 @@ def package_compound_structure(request, package_uuid, compound_uuid, structure_u
else:
return HttpResponseBadRequest()
new_structure_name = request.POST.get("compound-structure-name", "").strip()
new_structure_description = request.POST.get("compound-structure-description", "").strip()
# 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()
if new_structure_name:
current_structure.name = new_structure_name
@ -1413,11 +1483,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")
params["smirks"] = request.POST.get("rule-smirks").strip()
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")
params["reaction_smarts"] = request.POST.get("rule-reaction-smarts").strip()
elif rule_type == "ParallelRule":
pass
elif rule_type == "SequentialRule":
@ -1518,8 +1588,14 @@ def package_rule(request, package_uuid, rule_uuid):
return JsonResponse({"success": current_rule.url})
rule_name = request.POST.get("rule-name", "").strip()
rule_description = request.POST.get("rule-description", "").strip()
# 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()
if rule_name:
current_rule.name = rule_name
@ -1608,8 +1684,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(".")
@ -1670,8 +1746,16 @@ def package_reaction(request, package_uuid, reaction_uuid):
return JsonResponse({"success": current_reaction.url})
new_reaction_name = request.POST.get("reaction-name", "").strip()
new_reaction_description = request.POST.get("reaction-description", "").strip()
# 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()
if new_reaction_name:
current_reaction.name = new_reaction_name
@ -1748,8 +1832,9 @@ def package_pathways(request, package_uuid):
name = request.POST.get("name")
description = request.POST.get("description")
pw_mode = request.POST.get("predict", "predict").strip()
smiles = request.POST.get("smiles", "").strip()
pw_mode = request.POST.get("predict", "predict").strip()
if "smiles" in request.POST and smiles == "":
return error(
@ -1758,8 +1843,6 @@ 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:
@ -1800,9 +1883,9 @@ def package_pathways(request, package_uuid):
pw.setting = prediction_setting
pw.save()
from .tasks import predict
from .tasks import dispatch, predict
predict.delay(pw.pk, prediction_setting.pk, limit=limit)
dispatch(current_user, predict, pw.pk, prediction_setting.pk, limit=limit)
return redirect(pw.url)
@ -1838,6 +1921,25 @@ def package_pathway(request, package_uuid, pathway_uuid):
return response
if (
request.GET.get("identify-missing-rules", False) == "true"
and request.GET.get("rule-package") is not None
):
from .tasks import dispatch_eager, identify_missing_rules
rule_package = PackageManager.get_package_by_url(
current_user, request.GET.get("rule-package")
)
res = dispatch_eager(
current_user, identify_missing_rules, [current_pathway.pk], rule_package.pk
)
filename = f"{current_pathway.name.replace(' ', '_')}_{current_pathway.uuid}.csv"
response = HttpResponse(res, content_type="text/csv")
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
# Pathway d3_json() relies on a lot of related objects (Nodes, Structures, Edges, Reaction, Rules, ...)
# we will again fetch the current pathway identified by this url, but this time together with nearly all
# related objects
@ -1899,8 +2001,14 @@ 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() != "":
@ -1921,10 +2029,16 @@ def package_pathway(request, package_uuid, pathway_uuid):
if node_url:
n = current_pathway.get_node(node_url)
from .tasks import predict
from .tasks import dispatch, predict
dispatch(
current_user,
predict,
current_pathway.pk,
current_pathway.setting.pk,
node_pk=n.pk,
)
# Dont delay?
predict(current_pathway.pk, current_pathway.setting.pk, node_pk=n.pk)
return JsonResponse({"success": current_pathway.url})
return HttpResponseBadRequest()
@ -1982,8 +2096,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)
@ -2145,9 +2259,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")
@ -2234,7 +2348,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)
@ -2282,21 +2396,21 @@ def package_scenarios(request, package_uuid):
"name": "soil",
"widgets": [
HTMLGenerator.generate_html(ai, prefix=f"soil_{0}")
for ai in [x for s in SOIL_ADDITIONAL_INFORMATION.values() for x in s]
for ai in [x for sv in SOIL_ADDITIONAL_INFORMATION.values() for x in sv]
],
},
"Sludge Data": {
"name": "sludge",
"widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sludge_{0}")
for ai in [x for s in SLUDGE_ADDITIONAL_INFORMATION.values() for x in s]
for ai in [x for sv in SLUDGE_ADDITIONAL_INFORMATION.values() for x in sv]
],
},
"Water-Sediment System Data": {
"name": "sediment",
"widgets": [
HTMLGenerator.generate_html(ai, prefix=f"sediment_{0}")
for ai in [x for s in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in s]
for ai in [x for sv in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in sv]
],
},
}
@ -2311,6 +2425,7 @@ 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")
@ -2324,9 +2439,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 s in additional_information.values() for x in s]
additional_information = [x for sv in additional_information.values() for x in sv]
s = Scenario.create(
new_scen = Scenario.create(
current_package,
name=scenario_name,
description=scenario_description,
@ -2335,7 +2450,7 @@ def package_scenarios(request, package_uuid):
additional_information=additional_information,
)
return redirect(s.url)
return redirect(new_scen.url)
else:
return HttpResponseNotAllowed(
[
@ -2635,6 +2750,7 @@ 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(
@ -2696,6 +2812,24 @@ def setting(request, setting_uuid):
pass
def jobs(request):
current_user = _anonymous_or_real(request)
context = get_base_context(request)
if request.method == "GET":
context["object_type"] = "joblog"
context["breadcrumbs"] = [
{"Home": s.SERVER_URL},
{"Jobs": s.SERVER_URL + "/jobs"},
]
if current_user.is_superuser:
context["jobs"] = JobLog.objects.all().order_by("-created")
else:
context["jobs"] = JobLog.objects.filter(user=current_user).order_by("-created")
return render(request, "collections/joblog.html", context)
###########
# KETCHER #
###########
@ -2767,3 +2901,60 @@ 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)

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"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 Normal file
View File

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

View File

@ -27,10 +27,12 @@ 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.2" }
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" }
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.7"}
envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }
@ -65,22 +67,54 @@ 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 = { cmd = "python manage.py runserver", help = "Start the development server", deps = ["db-up"] }
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" }
# 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 = "python manage.py migrate", help = "Run database migrations" }
migrate = { cmd = "uv run python manage.py migrate", help = "Run database migrations" }
bootstrap = { shell = """
echo "Bootstrapping initial data..."
echo "This will take a bit . Get yourself some coffee..."
python manage.py bootstrap
uv run python manage.py bootstrap
echo " Bootstrap complete"
echo ""
echo "Default admin credentials:"
@ -88,4 +122,8 @@ echo " Username: admin"
echo " Email: admin@envipath.com"
echo " Password: SuperSafe"
""", help = "Bootstrap initial data (anonymous user, packages, models)" }
shell = { cmd = "python manage.py shell", help = "Open Django shell" }
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"] }

View File

@ -0,0 +1,84 @@
/**
* 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;
}

35
static/css/input.css Normal file
View File

@ -0,0 +1,35 @@
@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";

111
static/css/theme.css Normal file
View File

@ -0,0 +1,111 @@
/**
* 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.

After

Width:  |  Height:  |  Size: 252 KiB

BIN
static/images/hero.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
static/images/linkedin.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 21 KiB

View File

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

Before

Width:  |  Height:  |  Size: 41 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 22 KiB

View File

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

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.6 KiB

BIN
static/images/uoa-logo-small.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

170
static/js/discourse-api.js Normal file
View File

@ -0,0 +1,170 @@
/**
* 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

@ -22,6 +22,10 @@
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as Image</a>
</li>
{% if meta.can_edit %}
<li>
<a class="button" data-toggle="modal" data-target="#identify_missing_rules_modal">
<i class="glyphicon glyphicon-question-sign"></i> Identify Missing Rules</a>
</li>
<li role="separator" class="divider"></li>
<li>
<a class="button" data-toggle="modal" data-target="#edit_pathway_modal">

View File

@ -0,0 +1,70 @@
{% extends "framework.html" %}
{% load static %}
{% block content %}
<div class="panel-group" id="reviewListAccordion">
<div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
Jobs
</div>
<div class="panel-body">
<p>
Job Logs Desc
</p>
</div>
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="job-accordion-link" data-toggle="collapse" data-parent="#job-accordion" href="#jobs">
Jobs
</a>
</h4>
</div>
<div id="jobs"
class="panel-collapse collapse in">
<div class="panel-body list-group-item" id="job-content">
<table class="table table-bordered table-hover">
<tr style="background-color: rgba(0, 0, 0, 0.08);">
<th scope="col">ID</th>
<th scope="col">Name</th>
<th scope="col">Status</th>
<th scope="col">Queued</th>
<th scope="col">Done</th>
<th scope="col">Result</th>
</tr>
<tbody>
{% for job in jobs %}
<tr>
<td>{{ job.task_id }}</td>
<td>{{ job.job_name }}</td>
<td>{{ job.status }}</td>
<td>{{ job.created }}</td>
<td>{{ job.done_at }}</td>
{% if job.task_result and job.task_result|is_url == True %}
<td><a href="{{ job.task_result }}">Result</a></td>
{% elif job.task_result %}
<td>{{ job.task_result|slice:"40" }}...</td>
{% else %}
<td>Empty</td>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<!-- Unreviewable objects such as User / Group / Setting -->
<ul class='list-group'>
{% for obj in objects %}
{% if object_type == 'user' %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.username }}</a>
{% else %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a>
{% endif %}
{% endfor %}
</ul>
</div>
</div>
{% endblock content %}

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 }}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}
<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 }}{# <i>({{ obj.package.name }})</i> #}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}{# <i>({{ obj.package.name }})</i> #}
<span class="glyphicon glyphicon-star" aria-hidden="true"
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 }}</a>
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a>
{% endfor %}
{% else %}
{% for obj in unreviewed_objects|slice:":50" %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a>
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a>
{% endfor %}
{% 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 }}</a>
<a class="list-group-item" href="{{ obj.url }}">{{ obj.username|safe }}</a>
{% else %}
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a>
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a>
{% endif %}
{% endfor %}
</ul>

View File

@ -1,15 +1,17 @@
<!DOCTYPE html>
<html>
<html data-theme="envipath">
{% load static %}
<head>
<title>{{ title }}</title>
<style>
html, body {
height: 100%; /* ensure body fills viewport */
overflow-x: hidden; /* prevent horizontal scroll */
}
</style>
{# TODO use bundles from bootstrap 3.3.7 #}
<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 #}
<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>
@ -20,7 +22,16 @@
<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 -->
<meta name="csrf-token" content="{{ csrf_token }}">
{# Bootstrap compatibility styles #}
<style>
/* Ensure proper viewport behavior */
html, body {
height: 100%; /* ensure body fills viewport */
overflow-x: hidden; /* prevent horizontal scroll */
}
</style>
<script>
const csrftoken = document.querySelector('[name=csrf-token]').content;
@ -33,8 +44,7 @@
}
});
</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 #}-->
@ -56,7 +66,7 @@
(function () {
var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '10']);
_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';
@ -68,6 +78,8 @@
</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 -->
@ -146,8 +158,7 @@
</li>
{% if meta.user.username == 'anonymous' %}
<li>
<a href="#signup" id="loginButton" data-toggle="modal" data-target="#signupmodal"
style="margin-right:10px">Login</a>
<a href="{% url 'login' %}" id="loginButton" style="margin-right:10px">Login</a>
</li>
{% else %}
<li class="dropdown">
@ -177,6 +188,9 @@
</div>
</div>
</nav>
</div>
<!-- End legacy Bootstrap navbar -->
<div id="docContent" class="content container">
{% if breadcrumbs %}
<div id="bread">
@ -221,7 +235,8 @@
{% endif %}
</div>
<!-- FOOTER -->
<!-- FOOTER - Legacy Bootstrap -->
<div class="legacy-bootstrap">
<div class="container text-center">
<hr/>
<div class="row">
@ -258,6 +273,9 @@
</ul>
</div>
</div>
</div>
<!-- End legacy Bootstrap footer -->
<script>
$(function () {
// Hide actionsbutton if theres no action defined

View File

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

@ -0,0 +1,69 @@
{% 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

@ -0,0 +1,71 @@
{% 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,186 +1,360 @@
{% extends "framework.html" %}
{% extends "framework_modern.html" %}
{% load static %}
{% 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>
{% 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>
</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>
<fieldset class="fieldset transition-all duration-300 ease-in-out overflow-hidden">
<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>
<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>
</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>
<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>
<!-- 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>
<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 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>
<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>
</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>
<input type="hidden" id="index-form-smiles" name="smiles" value="smiles">
<input type="hidden" id="index-form-predict" name="predict" value="predict">
</form>
</div>
<p></p>
</div>
</section>
</div>
<script language="javascript">
var currentPackage = "{{ meta.current_package.url }}";
function goButtonClicked() {
$(this).prop("disabled", true);
// Discourse API integration is now handled by discourse-api.js
var action = $('#action-button').text().trim();
// Function to render Discourse topics into cards
function renderDiscourseTopics(topics) {
const container = document.getElementById('community-news-container');
if (!container) return;
var textSmiles = $('#index-form-text-input').val().trim();
// Clear container
container.innerHTML = '';
if (textSmiles === '') {
$(this).prop("disabled", false);
return;
}
// Create cards for each topic
topics.forEach(topic => {
const card = createDiscourseCard(topic);
container.insertAdjacentHTML('beforeend', card);
});
}
var ketcherSmiles = getKetcher('index-form-ketcher').getSmiles().trim();
// Function to create HTML card for a topic
function createDiscourseCard(topic) {
const date = new Date(topic.created_at).toLocaleDateString();
if (action !== 'Search' && ketcherSmiles !== '' && textSmiles !== ketcherSmiles) {
console.log("Ketcher and TextInput differ!");
}
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>
if (action === 'Search') {
var par = {};
par['search'] = textSmiles;
par['mode'] = 'text';
var queryString = $.param(par, true);
window.location.href = "/search?" + queryString;
<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>
`;
}
// Make render function globally available
window.renderDiscourseTopics = renderDiscourseTopics;
// Toggle functionality with smooth animations
function toggleInputMode() {
const toggle = $('input[type="checkbox"]');
const textContainer = $('#text-input-container');
const ketcherContainer = $('#ketcher-container');
const formCard = $('.card');
const fieldset = $('.fieldset');
if (toggle.is(':checked')) {
// Draw mode - show Ketcher, hide text input
textContainer.addClass('opacity-0 transform scale-95');
textContainer.removeClass('opacity-100 transform scale-100');
// Adjust fieldset padding for Ketcher mode - reduce padding and make more compact
fieldset.removeClass('p-8');
fieldset.addClass('p-4');
// Wait for fade out to complete, then hide and show new content
setTimeout(() => {
textContainer.addClass('hidden');
ketcherContainer.removeClass('hidden opacity-0 transform scale-95');
ketcherContainer.addClass('opacity-100 transform scale-100');
// Force re-evaluation of iframe size
const iframe = document.getElementById('index-ketcher');
if (iframe) {
iframe.style.height = '400px';
}
}, 300);
} else {
$('#index-form-smiles').val(textSmiles);
$('#index-form').submit();
// 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);
}
}
}
}
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());
// Ketcher integration
function indexKetcherToTextInput() {
$('#index-form-smiles').val(this.ketcher.getSmiles());
}
$(function () {
// Initialize fieldset with proper padding
$('.fieldset').addClass('p-8');
$('#index-form').on("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
goButtonClicked();
}
});
// Toggle event listener
$('input[type="checkbox"]').on('change', toggleInputMode);
// 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 () {
// Ketcher iframe load handler
$('#index-ketcher').on('load', function() {
const checkKetcherReady = () => {
win = this.contentWindow
const win = this.contentWindow;
if (win.ketcher && 'editor' in win.ketcher) {
window.indexKetcher = win.ketcher;
win.ketcher.editor.event.change.handlers.push({
once: false,
priority: 0,
f: ketcherToTextInput,
f: indexKetcherToTextInput,
ketcher: win.ketcher
});
} else {
setTimeout(checkKetcherReady, 100);
}
};
checkKetcherReady();
});
// Handle example link clicks
$('.example-link').on('click', function(e) {
e.preventDefault();
const smiles = $(this).data('smiles');
const title = $(this).attr('title');
// Check if we're in Ketcher mode or text input mode
if ($('input[type="checkbox"]').is(':checked')) {
// In Ketcher mode - set the SMILES in Ketcher
if (window.indexKetcher && window.indexKetcher.setMolecule) {
window.indexKetcher.setMolecule(smiles);
}
} else {
// In text input mode - set the SMILES in the text input
$('#index-form-text-input').val(smiles);
}
// Show a brief feedback
const originalText = $(this).text();
$(this).text(`loaded!`);
setTimeout(() => {
$(this).text(originalText);
}, 1000);
});
// Handle form submission on Enter
$('#index-form').on("submit", function (e) {
e.preventDefault();
var textSmiles = '';
// Check if we're in Ketcher mode and extract SMILES
if ($('input[type="checkbox"]').is(':checked') && window.indexKetcher) {
textSmiles = window.indexKetcher.getSmiles().trim();
} else {
textSmiles = $('#index-form-text-input').val().trim();
}
if (textSmiles === '') {
return;
}
$('#index-form-smiles').val(textSmiles);
$("#index-form").attr("action", currentPackage + "/pathway");
$("#index-form").attr("method", 'POST');
this.submit();
});
// Discourse topics are now loaded automatically by discourse-api.js
});
</script>
{% endblock content %}
{% endblock main_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 }}</a>
href="#{{ obj.id }}">{{ obj.name|safe }}</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 }} Migration Detail Page</a>
<a class="list-group-item" href="{{ obj.detail_url }}">{{ obj.name|safe }} 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 }}</a>
href="#{{ obj.id }}">{{ obj.name|safe }}</a>
</h4>
</div>
<div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}">

View File

@ -1,3 +1,4 @@
<div class="modal fade" tabindex="-1" id="new_model_modal" role="dialog" aria-labelledby="new_model_modal"
aria-hidden="true">
<div class="modal-dialog modal-lg">
@ -18,113 +19,117 @@
prediction. You just need to set a name and the packages
you want the object to be based on. There are multiple types of models available.
For additional information have a look at our
<a target="_blank" href="https://wiki.envipath.org/index.php/relative-reasoning" role="button">wiki &gt;&gt;</a>
<a target="_blank" href="https://wiki.envipath.org/index.php/relative-reasoning" role="button">wiki
&gt;&gt;</a>
</div>
<!-- Name -->
<label for="model-name">Name</label>
<input id="model-name" name="model-name" class="form-control" placeholder="Name"/>
<!-- Description -->
<label for="model-description">Description</label>
<input id="model-description" name="model-description" class="form-control"
placeholder="Description"/>
<!-- Model Type -->
<label for="model-type">Model Type</label>
<select id="model-type" name="model-type" class="form-control" data-width='100%'>
<option disabled selected>Select Model Type</option>
{% for k, v in model_types.items %}
<option value="{{ v }}">{{ k }}</option>
<option value="{{ v }}">{{ k }}</option>
{% endfor %}
</select>
<!-- ML and Rule Based Based Form-->
<div id="package-based-relative-reasoning-specific-form">
<!-- Rule Packages -->
<label for="package-based-relative-reasoning-rule-packages">Rule Packages</label>
<select id="package-based-relative-reasoning-rule-packages" name="package-based-relative-reasoning-rule-packages"
data-actions-box='true' class="form-control" multiple data-width='100%'>
<!-- Rule Packages -->
<div id="rule-packages" class="ep-model-param mlrr rbrr">
<label for="model-rule-packages">Rule Packages</label>
<select id="model-rule-packages" name="model-rule-packages" data-actions-box='true'
class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %}
{% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %}
{% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
<option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %}
{% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option>
{% endif %}
{% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
</select>
<!-- Data Packages -->
<label for="package-based-relative-reasoning-data-packages" >Data Packages</label>
<select id="package-based-relative-reasoning-data-packages" name="package-based-relative-reasoning-data-packages"
data-actions-box='true' class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %}
{% if obj.reviewed %}
<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 }}</option>
{% endif %}
{% endfor %}
</select>
<div id="ml-relative-reasoning-specific-form">
<!-- Fingerprinter -->
<label for="ml-relative-reasoning-fingerprinter">Fingerprinter</label>
<select id="ml-relative-reasoning-fingerprinter" name="ml-relative-reasoning-fingerprinter"
class="form-control">
<option value="MACCS" selected>MACCS Fingerprinter</option>
</select>
{% if meta.enabled_features.PLUGINS and additional_descriptors %}
<!-- Property Plugins go here -->
<label for="ml-relative-reasoning-additional-fingerprinter">Additional Fingerprinter /
Descriptors</label>
<select id="ml-relative-reasoning-additional-fingerprinter"
name="ml-relative-reasoning-additional-fingerprinter" class="form-control">
<option disabled selected>Select Additional Fingerprinter / Descriptor</option>
{% for k, v in additional_descriptors.items %}
<option value="{{ v }}">{{ k }}</option>
{% endfor %}
</select>
{% endif %}
<label for="ml-relative-reasoning-threshold">Threshold</label>
<input type="number" min="0" max="1" step="0.05" value="0.5"
id="ml-relative-reasoning-threshold"
name="ml-relative-reasoning-threshold" class="form-control">
</div>
{% if meta.enabled_features.APPLICABILITY_DOMAIN %}
<!-- Build AD? -->
<div class="checkbox">
<label>
<input type="checkbox" id="build-app-domain" name="build-app-domain">Also build an
Applicability Domain?
</label>
</div>
<div id="ad-params" style="display:none">
<!-- Num Neighbors -->
<label for="num-neighbors">Number of Neighbors</label>
<input id="num-neighbors" name="num-neighbors" type="number" class="form-control" value="5"
step="1" min="0" max="10">
<!-- Local Compatibility -->
<label for="local-compatibility-threshold">Local Compatibility Threshold</label>
<input id="local-compatibility-threshold" name="local-compatibility-threshold" type="number"
class="form-control" value="0.5" step="0.01" min="0" max="1">
<!-- Reliability -->
<label for="reliability-threshold">Reliability Threshold</label>
<input id="reliability-threshold" name="reliability-threshold" type="number"
class="form-control" value="0.5" step="0.01" min="0" max="1">
</div>
{% endif %}
</div>
<!-- EnviFormer-->
<div id="enviformer-specific-form">
<label for="enviformer-threshold">Threshold</label>
<input type="number" min="0" max="1" step="0.05" value="0.5" id="enviformer-threshold"
name="enviformer-threshold" class="form-control">
<!-- Data Packages -->
<div id="data-packages" class="ep-model-param mlrr rbrr enviformer">
<label for="model-data-packages">Data Packages</label>
<select id="model-data-packages" name="model-data-packages" data-actions-box='true'
class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %}
{% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name|safe }}</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>
{% endif %}
{% endfor %}
</select>
</div>
<!-- Fingerprinter -->
<div id="fingerprinter" class="ep-model-param mlrr">
<label for="model-fingerprinter">Fingerprinter</label>
<select id="model-fingerprinter" name="model-fingerprinter" data-actions-box='true'
class="form-control" multiple data-width='100%'>
<option value="MACCS" selected>MACCS Fingerprinter</option>
{% if meta.enabled_features.PLUGINS and additional_descriptors %}
<option disabled selected>Select Additional Fingerprinter / Descriptor</option>
{% for k, v in additional_descriptors.items %}
<option value="{{ v }}">{{ k }}</option>
{% endfor %}
{% endif %}
</select>
</div>
<!-- Threshold -->
<div id="threshold" class="ep-model-param mlrr enviformer">
<label for="model-threshold">Threshold</label>
<input type="number" min="0" max="1" step="0.05" value="0.5" id="model-threshold"
name="model-threshold" class="form-control">
</div>
<div id="appdomain" class="ep-model-param mlrr">
{% if meta.enabled_features.APPLICABILITY_DOMAIN %}
<!-- Build AD? -->
<div class="checkbox">
<label>
<input type="checkbox" id="build-app-domain" name="build-app-domain">Also build an
Applicability Domain?
</label>
</div>
<div id="ad-params" style="display:none">
<!-- Num Neighbors -->
<label for="num-neighbors">Number of Neighbors</label>
<input id="num-neighbors" name="num-neighbors" type="number" class="form-control"
value="5"
step="1" min="0" max="10">
<!-- Local Compatibility -->
<label for="local-compatibility-threshold">Local Compatibility Threshold</label>
<input id="local-compatibility-threshold" name="local-compatibility-threshold"
type="number"
class="form-control" value="0.5" step="0.01" min="0" max="1">
<!-- Reliability -->
<label for="reliability-threshold">Reliability Threshold</label>
<input id="reliability-threshold" name="reliability-threshold" type="number"
class="form-control" value="0.5" step="0.01" min="0" max="1">
</div>
{% endif %}
</div>
</form>
</div>
@ -137,53 +142,47 @@
</div>
<script>
$(function() {
// Initially hide all "specific" forms
$("div[id$='-specific-form']").each( function() {
$(this).hide();
});
$(function () {
// Built in Model Types
var nativeModelTypes = [
"mlrr",
"rbrr",
"enviformer",
]
$('#model-type').selectpicker();
$("#ml-relative-reasoning-fingerprinter").selectpicker();
$("#package-based-relative-reasoning-rule-packages").selectpicker();
$("#package-based-relative-reasoning-data-packages").selectpicker();
$("#package-based-relative-reasoning-evaluation-packages").selectpicker();
if ($('#ml-relative-reasoning-additional-fingerprinter').length > 0) {
$("#ml-relative-reasoning-additional-fingerprinter").selectpicker();
}
$("#build-app-domain").change(function () {
if ($(this).is(":checked")) {
$('#ad-params').show();
} else {
$('#ad-params').hide();
}
});
// On change hide all and show only selected
$("#model-type").change(function() {
$("div[id$='-specific-form']").each( function() {
// Initially hide all "specific" forms
$(".ep-model-param").each(function () {
$(this).hide();
});
val = $('option:selected', this).val();
if (val === 'ml-relative-reasoning' || val === 'rule-based-relative-reasoning') {
$("#package-based-relative-reasoning-specific-form").show();
if (val === 'ml-relative-reasoning') {
$("#ml-relative-reasoning-specific-form").show();
$('#model-type').selectpicker();
$("#model-fingerprinter").selectpicker();
$("#model-rule-packages").selectpicker();
$("#model-data-packages").selectpicker();
$("#build-app-domain").change(function () {
if ($(this).is(":checked")) {
$('#ad-params').show();
} else {
$('#ad-params').hide();
}
} else {
$("#" + val + "-specific-form").show();
}
});
// On change hide all and show only selected
$("#model-type").change(function () {
$('.ep-model-param').hide();
var modelType = $('#model-type').val();
if (nativeModelTypes.indexOf(modelType) !== -1) {
$('.' + modelType).show();
} else {
// do nothing
}
});
$('#new_model_modal_form_submit').on('click', function (e) {
e.preventDefault();
$('#new_model_form').submit();
});
});
$('#new_model_modal_form_submit').on('click', function(e){
e.preventDefault();
$('#new_model_form').submit();
});
});
</script>

View File

@ -1,3 +1,4 @@
{% 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;">
@ -111,7 +112,7 @@
<select id="settingSelect" name="settingSelect" class="form-control">
{% for setting in available_settings %}
<option value="{{ setting.id }}">{{ setting.name }}</option>
<option value="{{ setting.id }}">{{ setting.name|safe }}</option>
{% endfor %}
</select>
<p></p>

View File

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

View File

@ -1,3 +1,4 @@
{% 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"
@ -36,7 +37,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 }}</option>
<option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option>
{% endfor %}
</select>
</div>
@ -47,7 +48,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 }}</option>
<option data-smiles="{{ n.default_node_label.smiles }}" value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option>
{% endfor %}
</select>
</div>

View File

@ -1,3 +1,4 @@
{% load static %}
<!-- Delete Edge -->
<div id="delete_pathway_edge_modal" class="modal" tabindex="-1">
@ -19,7 +20,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 }}</option>
<option value="{{ e.url }}">{{ e.edge_label.name|safe }}</option>
{% endfor %}
</select>
<input type="hidden" id="hidden" name="hidden" value="delete"/>

View File

@ -1,4 +1,5 @@
{% load static %}
<!-- Delete Node -->
<div id="delete_pathway_node_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
@ -19,7 +20,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 }}</option>
<option value="{{ n.url }}">{{ n.default_node_label.name|safe }}</option>
{% endfor %}
</select>
<input type="hidden" id="hidden" name="hidden" value="delete"/>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,4 @@
{% load static %}
<!-- Edit Pathway -->
<div id="edit_pathway_modal" class="modal" tabindex="-1">
@ -15,12 +16,12 @@
{% csrf_token %}
<p>
<label for="pathway-name">Name</label>
<input id="pathway-name" class="form-control" name="pathway-name" value="{{ pathway.name }}">
<input id="pathway-name" class="form-control" name="pathway-name" value="{{ pathway.name|safe }}">
</p>
<p>
<label for="pathway-description">Description</label>
<textarea id="pathway-description" type="text" class="form-control" name="pathway-description"
rows="10">{{ pathway.description }}</textarea>
rows="10">{{ pathway.description|safe }}</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 }}</option>
<option value="{{ m.id }}" {% if user.prediction_settings.model.url == m.url %}selected{% endif %}>{{ m.name|safe }}</option>
{% endfor %}
</select>
</td>

View File

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

View File

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

View File

@ -1,3 +1,4 @@
{% load static %}
<!-- Edit User -->
<div id="edit_user_modal" class="modal" tabindex="-1">
@ -18,7 +19,7 @@
<select id="default-package" name="default-package" class="form-control" data-width='100%'>
<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 }}</option>
<option value="{{ p.url }}" {% if p.id == meta.user.default_package.id %}selected{% endif %}>{{ p.name|safe }}</option>
{% endfor %}
</select>
</p>
@ -27,7 +28,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 }}</option>
<option value="{{ g.url }}" {% if g.id == meta.user.default_group.id %}selected{% endif %}>{{ g.name|safe }}</option>
{% endfor %}
</select>
</p>
@ -36,7 +37,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 }}</option>
<option value="{{ s.url }}" {% if s.id == meta.user.default_setting.id %}selected{% endif %}>{{ s.name|safe }}</option>
{% endfor %}
</select>
</p>

View File

@ -1,3 +1,4 @@
<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">
@ -17,25 +18,34 @@
For evaluation, you need to select the packages you want to use.
While the model is evaluating, you can use the model for predictions.
</div>
<!-- Evaluation -->
<label for="relative-reasoning-evaluation-packages">Evaluation Packages</label>
<select id="relative-reasoning-evaluation-packages" name=relative-reasoning-evaluation-packages"
data-actions-box='true' class="form-control" multiple data-width='100%'>
<!-- Evaluation Packages -->
<label for="model-evaluation-packages">Evaluation Packages</label>
<select id="model-evaluation-packages" name="model-evaluation-packages" data-actions-box='true'
class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %}
{% if obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option>
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
<option disabled>Unreviewed Packages</option>
{% for obj in meta.readable_packages %}
{% if not obj.reviewed %}
<option value="{{ obj.url }}">{{ obj.name }}</option>
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endif %}
{% endfor %}
</select>
<input type="hidden" name="hidden" value="evaluate">
<!-- Eval Type -->
<label for="model-evaluation-type">Evaluation Type</label>
<select id="model-evaluation-type" name="model-evaluation-type" class="form-control">
<option disabled selected>Select evaluation type</option>
<option value="sg">Single Generation</option>
<option value="mg">Multiple Generations</option>
</select>
<input type="hidden" name="hidden" value="evaluate">
</form>
</div>
<div class="modal-footer">
@ -50,7 +60,7 @@
$(function () {
$("#relative-reasoning-evaluation-packages").selectpicker();
$("#model-evaluation-packages").selectpicker();
$('#evaluate_model_form_submit').on('click', function (e) {
e.preventDefault();

View File

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

View File

@ -1,3 +1,4 @@
{% load static %}
<style>
@ -55,7 +56,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 }}</h4>
<h4 class="modal-title">Set Aliases for {{ current_object.name|safe }}</h4>
</div>
<div class="modal-body">
<form id="set_aliases_modal_form" accept-charset="UTF-8" action="{{ current_object.url }}"

View File

@ -1,3 +1,4 @@
{% load static %}
<!-- Delete Object -->
<div id="generic_set_external_reference_modal" class="modal" tabindex="-1">
@ -23,7 +24,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 }}</option>`
value="{{ db.database.id }}">{{ db.database.name|safe }}</option>`
{% endfor %}
{% endif %}
{% endfor %}

View File

@ -1,3 +1,4 @@
{% 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">
@ -7,7 +8,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 }}</h4>
<h4 class="modal-title">Set Scenarios for {{ current_object.name|safe }}</h4>
</div>
<div class="modal-body">
<div id="loading_scenario_div" class="text-center"></div>

View File

@ -0,0 +1,54 @@
{% load static %}
<!-- Identify Missing Rules -->
<div id="identify_missing_rules_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Identify Missing Rules</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
By clicking on Download we'll search the Pathway for Reactions that are not backed by
a Rule or which can be assembled by chaining two rules.
<form id="identify-missing-rules-modal-form" accept-charset="UTF-8" action="{{ pathway.url }}"
data-remote="true" method="GET">
<label for="rule-package">Select the Rule Package</label>
<select id="rule-package" name="rule-package" data-actions-box='true' class="form-control"
data-width='100%'>
<option disabled>Reviewed Packages</option>
{% for obj in meta.readable_packages %}
{% if obj.reviewed %}
<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 }}</option>
{% endif %}
{% endfor %}
</select>
<input type="hidden" name="identify-missing-rules" value="true"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="identify-missing-rules-modal-submit">Download</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#identify-missing-rules-modal-submit').click(function (e) {
e.preventDefault();
$('#identify-missing-rules-modal-form').submit();
$('#identify_missing_rules_modal').modal('hide');
});
})
</script>

View File

@ -1,3 +1,4 @@
<div class="modal fade"
tabindex="-1"
id="manage_api_token_modal"
@ -41,7 +42,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 }}" disabled>
<input type="text" class="form-control" value="{{ t.name|safe }}" 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 }}{% if s.id == meta.user.default_setting.id %} <i>(User default)</i>{% endif %}
{{ s.name|safe }}{% if s.id == meta.user.default_setting.id %} <i>(User default)</i>{% endif %}
</option>
{% 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 }}
{{ rule.name|safe }}
<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 }}</a>
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</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 }} <i>({{ s.package.name }})</i></a>
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %}
</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 }}
{{ compound.name|safe }}
<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 }}
{{ compound.description|safe }}
</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 }} <i>({{ r.package.name }})</i></a>
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }} <i>({{ r.package.name|safe }})</i></a>
{% endfor %}
</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 }} <i>({{ r.package.name }})</i></a>
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }} <i>({{ r.package.name|safe }})</i></a>
{% endfor %}
</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 }} <i>({{ s.package.name }})</i></a>
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %}
</div>
</div>
@ -183,7 +183,7 @@
</div>
<div id="compound-external-identifier" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% if compound.get_pubchem_identifiers %}
{% if compound.get_pubchem_compound_identifiers %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">
<h4 class="panel-title">
@ -193,12 +193,28 @@
</h4>
</div>
<div id="compound-pubchem-identifier" class="panel-collapse collapse in">
{% for eid in compound.get_pubchem_identifiers %}
{% for eid in compound.get_pubchem_compound_identifiers %}
<a class="list-group-item"
href="{{ eid.external_url }}">CID{{ eid.identifier_value }}</a>
{% endfor %}
</div>
{% endif %}
{% if compound.get_pubchem_substance_identifiers %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">
<h4 class="panel-title">
<a id="compound-pubchem-identifier-link" data-toggle="collapse"
data-parent="#compound-external-identifier"
href="#compound-pubchem-identifier">PubChem Substance Identifier</a>
</h4>
</div>
<div id="compound-pubchem-identifier" class="panel-collapse collapse in">
{% for eid in compound.get_pubchem_substance_identifiers %}
<a class="list-group-item"
href="{{ eid.external_url }}">SID{{ eid.identifier_value }}</a>
{% endfor %}
</div>
{% endif %}
{% if compound.get_chebi_identifiers %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">

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 }}
{{ compound_structure.name|safe }}
<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 }} </p>
<p> {{ compound_structure.description|safe }} </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 }} <i>({{ s.package.name }})</i></a>
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %}
</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 }}
{{ edge.edge_label.name|safe }}
<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 }}
{{ edge.description|safe }}
</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 }}</a>
<a class="btn btn-default" href="{{ educt.url }}">{{ educt.name|safe }}</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 }}</a>
<a class="btn btn-default" href="{{ product.url }}">{{ product.name|safe }}</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 }}</a>
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</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 }} <i>({{ s.package.name }})</i></a>
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %}
</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 }}
{{ group.name|safe }}
<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 }} </p>
<p> {{ group.description|safe }} </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 }}</a>
<a class="list-group-item" href="{{ um.url }}">{{ um.username|safe }}</a>
{% endfor %}
{% for gm in group.group_member.all %}
<a class="list-group-item" href="{{ gm.url }}">{{ gm.name }}</a>
<a class="list-group-item" href="{{ gm.url }}">{{ gm.name|safe }}</a>
{% endfor %}
</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 }}</a>
<a class="list-group-item" href="{{ p.url }}">{{ p.name|safe }}</a>
{% endfor %}
</ul>
</div>

View File

@ -3,162 +3,262 @@
{% load envipytags %}
{% block content %}
{% block action_modals %}
{% include "modals/objects/edit_model_modal.html" %}
{% include "modals/objects/evaluate_model_modal.html" %}
{% include "modals/objects/retrain_model_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
{% block action_modals %}
{% include "modals/objects/edit_model_modal.html" %}
{% include "modals/objects/evaluate_model_modal.html" %}
{% include "modals/objects/retrain_model_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<!-- 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">
<!-- 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"
/>
<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 }}
<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>
<ul id="actionsList" class="dropdown-menu">
{% block actions %}
{% include "actions/objects/model.html" %}
{% endblock %}
</ul>
</div>
<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"
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>
<ul id="actionsList" class="dropdown-menu">
{% block actions %}
{% include "actions/objects/model.html" %}
{% endblock %}
</ul>
</div>
</div>
<div class="panel-body">
<p>{{ model.description|safe }}</p>
</div>
{% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning' %}
<!-- Rule Packages -->
<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
>
</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
>
{% endfor %}
</div>
</div>
<!-- Reaction Packages -->
<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
>
</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
>
{% 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"
>
<h4 class="panel-title">
<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
>
{% endfor %}
</div>
<div class="panel-body">
<p> {{ model.description }} </p>
</div>
{% endif %}
<!-- Model Status -->
<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
>
</h4>
</div>
<div id="model-status" class="panel-collapse collapse in">
<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"
>
<h4 class="panel-title">
<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"
/>
<span class="input-group-btn">
<button
class="btn btn-default"
type="submit"
id="predict-button"
>
Predict!
</button>
</span>
</div>
{% if model|classname == 'MLRelativeReasoning' or model|classname == 'RuleBasedRelativeReasoning'%}
<!-- Rule Packages -->
<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>
</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 }}</a>
{% endfor %}
</div>
</div>
<!-- Reaction Packages -->
<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>
</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 }}</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">
<h4 class="panel-title">
<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 }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Model Status -->
<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>
</h4>
</div>
<div id="model-status" class="panel-collapse collapse in">
<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">
<h4 class="panel-title">
<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">
<span class="input-group-btn">
<button class="btn btn-default" type="submit" id="predict-button">Predict!</button>
</span>
</div>
<div id="predictLoading"></div>
<div id="predictResultTable"></div>
</div>
</div>
<!-- End Predict Panel -->
{% endif %}
<div id="predictLoading"></div>
<div id="predictResultTable"></div>
</div>
</div>
<!-- End Predict Panel -->
{% endif %}
{% if model.app_domain %}
<!-- App Domain -->
<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>
</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">
<span class="input-group-btn">
<button class="btn btn-default" type="submit" id="assess-button">Assess!</button>
</span>
</div>
<div id="appDomainLoading"></div>
<div id="appDomainAssessmentResultTable"></div>
</div>
</div>
<!-- End App Domain -->
{% endif %}
{% 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"
>
<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
>
</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"
/>
<span class="input-group-btn">
<button
class="btn btn-default"
type="submit"
id="assess-button"
>
Assess!
</button>
</span>
</div>
<div id="appDomainLoading"></div>
<div id="appDomainAssessmentResultTable"></div>
</div>
</div>
<!-- End App Domain -->
{% endif %}
{% if model.model_status == 'FINISHED' %}
<!-- Single Gen Curve Panel -->
<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>
</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 id="sg-curve-plotdiv" class="chart">
<div id="sg-chart"></div>
</div>
</div>
</div>
</div>
{% if model.model_status == 'FINISHED' %}
<!-- Single Gen Curve Panel -->
<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
>
</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 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)) {
@ -265,138 +365,162 @@
});
});
</script>
<!-- End Single Gen Curve Panel -->
{% endif %}
</div>
{# prettier-ignore-end #}
<!-- End Single Gen Curve Panel -->
{% endif %}
</div>
</div>
<script>
<script>
function handlePredictionResponse(data) {
res = "<table class='table table-striped'>";
res += "<thead>";
res += "<th scope='col'>#</th>";
function handlePredictionResponse(data) {
res = "<table class='table table-striped'>"
res += "<thead>"
res += "<th scope='col'>#</th>"
columns = ["products", "image", "probability", "btrule"];
columns = ['products', 'image', 'probability', 'btrule']
for (col in columns) {
res += "<th scope='col'>" + columns[col] + "</th>";
}
for (col in columns) {
res += "<th scope='col'>" + columns[col] + "</th>"
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>";
} else {
res += "<th scope='row'>N/A</th>";
}
res += "</tr>";
cnt += 1;
}
res += "</tbody>";
res += "</table>";
$("#predictResultTable").append(res);
}
function clear(divid) {
$("#" + divid).removeClass("alert alert-danger");
$("#" + divid).empty();
}
if ($("#predict-button").length > 0) {
$("#predict-button").on("click", function (e) {
e.preventDefault();
clear("predictResultTable");
data = {
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!",
);
return;
}
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: "get",
data: data,
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 :/",
);
}
},
error: function (jqXHR, textStatus, errorThrown, x) {
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append(jqXHR.responseJSON.error);
},
});
});
}
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>"
} else {
res += "<th scope='row'>N/A</th>"
}
res += "</tr>"
cnt += 1;
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!",
};
if (data["smiles"].trim() === "") {
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append(
"Please enter a SMILES string to predict!",
);
return;
}
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: "get",
data: data,
url: "",
success: function (data, textStatus) {
try {
$("#appDomainLoading").empty();
handleAssessmentResponse("{% url 'depict' %}", data);
console.log(data);
} catch (error) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass(
"alert alert-danger",
);
$("#appDomainAssessmentResultTable").append(
"Error while processing response :/",
);
}
res += "</tbody>"
res += "</table>"
$("#predictResultTable").append(res);
}
function clear(divid) {
$("#" + divid).removeClass("alert alert-danger");
$("#" + divid).empty();
}
if ($('#predict-button').length > 0) {
$("#predict-button").on("click", function (e) {
e.preventDefault();
clear("predictResultTable");
data = {
"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!");
return;
}
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: 'get',
data: data,
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 :/");
}
},
error: function (jqXHR, textStatus, errorThrown, x) {
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append(jqXHR.responseJSON.error);
},
});
});
}
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!"
}
if (data["smiles"].trim() === "") {
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Please enter a SMILES string to predict!");
return;
}
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: 'get',
data: data,
url: '',
success: function (data, textStatus) {
try {
$("#appDomainLoading").empty();
handleAssessmentResponse("{% url 'depict' %}", data);
console.log(data);
} catch (error) {
$("#appDomainLoading").empty();
$("#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);
}
});
});
}
</script>
},
error: function (jqXHR, textStatus, errorThrown) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#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 }}
{{ node.name|safe }}
<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 }}
{{ node.description|safe }}
</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 }} <i>({{ s.package.name }})</i></a>
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %}
</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 }}
{{ package.name|safe }}
<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

@ -83,6 +83,7 @@
{% include "modals/objects/add_pathway_edge_modal.html" %}
{% include "modals/objects/download_pathway_csv_modal.html" %}
{% include "modals/objects/download_pathway_image_modal.html" %}
{% include "modals/objects/identify_missing_rules_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/edit_pathway_modal.html" %}
{% include "modals/objects/generic_set_aliases_modal.html" %}
@ -97,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 }}
{{ pathway.name|safe }}
</div>
</div>
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
@ -235,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 }} <i>({{ s.package.name }})</i></a>
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %}
</div>
</div>
@ -265,7 +266,7 @@
<td colspan="2">
<li class="list-group-item">
<a href="{{ pathway.setting.model.url }}">
{{ pathway.setting.model.name }}
{{ pathway.setting.model.name|safe }}
</a>
</li>
</td>
@ -298,7 +299,7 @@
{% for p in pathway.setting.rule_packages.all %}
<li class="list-group-item">
<a href="{{ p.url }}">
{{ p.name }}
{{ p.name|safe }}
</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 }}
{{ reaction.name|safe }}
<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 }}
{{ reaction.description|safe }}
</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 }}</a>
<a class="btn btn-default" href="{{ educt.url }}">{{ educt.name|safe }}</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 }}</a>
<a class="btn btn-default" href="{{ product.url }}">{{ product.name|safe }}</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 }}</a>
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</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 }}</a>
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</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 }} <i>({{ s.package.name }})</i></a>
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }} <i>({{ s.package.name|safe }})</i></a>
{% endfor %}
</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 }}
{{ scenario.name|safe }}
<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 }}
{{ scenario.description|safe }}
<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 }}
{{ rule.name|safe }}
<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 }}
{{ rule.description|safe }}
</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 }}</a>
<a class="list-group-item" href="{{ cr.url }}">{{ cr.name|safe }}</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 }}</a>
<a class="list-group-item" href="{{ s.url }}">{{ s.name|safe }}</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 }}</a>
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</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 }}</a>
<a class="list-group-item" href="{{ r.url }}">{{ r.name|safe }}</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 }}</a>
<a href="{{ user.default_package.url }}"> {{ user.default_package.name|safe }}</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 }}</a>
<a href="{{ g.url }}"> {{ g.name|safe }}</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 }}
{{ user.default_setting.model.name|safe }}
</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 }}
{{ p.name|safe }}
</a>
</li>
{% endfor %}

View File

@ -18,7 +18,7 @@
</head>
<body>
<p></p>
{{ pathway.name }}
{{ pathway.name|safe }}
<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 }}</option>
<option value="{{ obj.url }}" selected>{{ obj.name|safe }}</option>
{% endfor %}
{% if unreviewed_objects %}
<option disabled>Unreviewed Packages</option>
{% endif %}
{% for obj in unreviewed_objects %}
<option value="{{ obj.url }}">{{ obj.name }}</option>
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
{% endfor %}
</select>
</div>

View File

@ -0,0 +1,132 @@
{% 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 %}

View File

@ -0,0 +1,190 @@
{% 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>Jobs & Careers</li>
</ul>
</div>
<!-- Main Content -->
<div class="bg-base-100 shadow-xl rounded-lg p-8">
<h1 class="text-4xl font-bold mb-6">Jobs & Careers</h1>
<div class="prose max-w-none">
<p class="text-lg mb-6">
Join our team of passionate scientists and developers working at the intersection of environmental
chemistry, computational science, and machine learning. Help us build the future of biodegradation prediction.
</p>
<!-- Hero Card -->
<div class="card bg-gradient-to-br from-primary to-secondary text-primary-content mb-8">
<div class="card-body">
<h2 class="card-title text-2xl">Why Work With Us?</h2>
<p>
At enviPath, we're committed to advancing environmental science through innovative technology.
We offer a collaborative, research-focused environment where your work directly impacts
environmental protection and sustainability.
</p>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">What We Value</h2>
<div class="grid md:grid-cols-2 gap-4 mb-6 not-prose">
<div class="flex items-start gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary flex-shrink-0 mt-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 class="font-semibold">Scientific Excellence</h3>
<p class="text-sm">Rigorous research and peer-reviewed contributions</p>
</div>
</div>
<div class="flex items-start gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary flex-shrink-0 mt-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z" />
</svg>
<div>
<h3 class="font-semibold">Collaboration</h3>
<p class="text-sm">Work with leading institutions worldwide</p>
</div>
</div>
<div class="flex items-start gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary flex-shrink-0 mt-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
<div>
<h3 class="font-semibold">Innovation</h3>
<p class="text-sm">Push the boundaries of computational chemistry</p>
</div>
</div>
<div class="flex items-start gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-primary flex-shrink-0 mt-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3.055 11H5a2 2 0 012 2v1a2 2 0 002 2 2 2 0 012 2v2.945M8 3.935V5.5A2.5 2.5 0 0010.5 8h.5a2 2 0 012 2 2 2 0 104 0 2 2 0 012-2h1.064M15 20.488V18a2 2 0 012-2h3.064M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 class="font-semibold">Impact</h3>
<p class="text-sm">Contribute to environmental protection</p>
</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Current Opportunities</h2>
<div class="alert alert-info mb-6">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">No Current Openings</h3>
<div class="text-sm">We don't have any open positions at the moment, but we're always interested in hearing from talented individuals. Please check back later or reach out with your CV.</div>
</div>
</div>
<!-- Example Job Posting Structure (for when there are openings) -->
<!--
<div class="card bg-base-200 shadow-md mb-4">
<div class="card-body">
<div class="flex justify-between items-start">
<div>
<h3 class="card-title text-xl">Position Title</h3>
<div class="flex gap-2 mt-2">
<div class="badge badge-primary">Full-time</div>
<div class="badge badge-outline">Remote</div>
</div>
</div>
<div class="text-sm text-base-content/70">Posted: Date</div>
</div>
<p class="mt-4">Brief description of the role...</p>
<div class="card-actions justify-end mt-4">
<button class="btn btn-primary">Apply Now</button>
</div>
</div>
</div>
-->
<h2 class="text-2xl font-semibold mt-8 mb-4">Types of Roles</h2>
<p class="mb-4">We typically hire for the following types of positions:</p>
<div class="space-y-4 mb-6">
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg">Computational Chemists</h3>
<p>Develop and improve prediction models, curate chemical databases, and validate predictions against experimental data.</p>
</div>
</div>
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg">Machine Learning Engineers</h3>
<p>Build and optimize ML models for biotransformation prediction, feature engineering, and model deployment.</p>
</div>
</div>
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg">Software Developers</h3>
<p>Develop and maintain the enviPath platform, API, and user interfaces using Python, Django, and modern web technologies.</p>
</div>
</div>
<div class="card bg-base-200">
<div class="card-body">
<h3 class="card-title text-lg">Postdoctoral Researchers</h3>
<p>Conduct independent research, publish findings, and contribute to grant proposals in computational environmental chemistry.</p>
</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Student Opportunities</h2>
<p class="mb-4">
We regularly host students for internships, Master's theses, and PhD projects. If you're interested
in computational chemistry, machine learning, or environmental science, we'd love to hear from you.
</p>
<div class="card bg-secondary text-secondary-content mb-6">
<div class="card-body">
<h3 class="card-title">Academic Collaborations</h3>
<p>
We partner with universities including the University of Auckland, Eawag, and University of Zurich.
Check with your academic advisor about potential collaboration opportunities.
</p>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">How to Apply</h2>
<p class="mb-4">
Interested in joining our team? Here's how to get in touch:
</p>
<ol class="list-decimal list-inside mb-6 space-y-2">
<li>Review our <a href="/about" class="link link-primary">about page</a> to understand our mission and work</li>
<li>Prepare your CV/resume and a brief cover letter explaining your interest</li>
<li>Visit our <a href="https://community.envipath.org/" target="_blank" class="link link-primary">community forums</a> or reach out via <a href="https://www.linkedin.com/company/envipath/" target="_blank" class="link link-primary">LinkedIn</a></li>
<li>Include links to your publications, GitHub profile, or portfolio if relevant</li>
</ol>
<div class="alert alert-success">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 class="font-bold">We Value Diversity</h3>
<div class="text-sm">enviPath is an equal opportunity employer. We celebrate diversity and are committed to creating an inclusive environment for all team members.</div>
</div>
</div>
<div class="divider my-8"></div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Contact</h2>
<p class="mb-4">
Have questions about careers at enviPath? Visit our <a href="/contact" class="link link-primary">contact page</a>
or join our <a href="https://community.envipath.org/" target="_blank" class="link link-primary">community forums</a>.
</p>
</div>
</div>
</div>
{% endblock main_content %}

204
templates/static/cite.html Normal file
View File

@ -0,0 +1,204 @@
{% 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>How to Cite enviPath</li>
</ul>
</div>
<!-- Main Content -->
<div class="bg-base-100 shadow-xl rounded-lg p-8">
<h1 class="text-4xl font-bold mb-6">How to Cite enviPath</h1>
<div class="prose max-w-none">
<p class="text-lg mb-6">
If you use enviPath in your research, please cite our work. Citations help us demonstrate the
impact of our platform and support continued development and maintenance.
</p>
<div class="alert alert-info mb-8">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">Quick Reference</h3>
<div class="text-sm">The citation depends on which specific tools or models you used. Please cite the relevant publications below.</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Main Platform Citation</h2>
<p class="mb-4">
If you use the enviPath platform in general, please cite:
</p>
<div class="card bg-base-200 mb-6">
<div class="card-body">
<h3 class="font-semibold mb-2">Advancements in biotransformation pathway prediction: enhancements, datasets, and novel functionalities in enviPath</h3>
<p class="text-sm mb-4">
<strong>Authors:</strong> Hafner, J., Lorsbach, T., Schmidt, S., Brydon, L., Dost, K., Zhang, K., Fenner, K., and Wicker, J.<br>
<strong>Journal:</strong> Journal of Cheminformatics, 16(1), 93<br>
<strong>Year:</strong> 2024<br>
<strong>DOI:</strong> <a href="https://doi.org/10.1186/s13321-024-00881-6" target="_blank" class="link link-primary">10.1186/s13321-024-00881-6</a>
</p>
<div class="collapse collapse-arrow bg-base-300">
<input type="checkbox" />
<div class="collapse-title font-medium text-sm">
Show BibTeX
</div>
<div class="collapse-content">
<pre class="text-xs overflow-x-auto"><code>@ARTICLE{Hafner2024,
title = "Advancements in biotransformation pathway prediction:
enhancements, datasets, and novel functionalities in enviPath",
author = "Hafner, Jasmin and Lorsbach, Tim and Schmidt, Sebastian and
Brydon, Liam and Dost, Katharina and Zhang, Kunyang and Fenner,
Kathrin and Wicker, J{\"o}rg",
journal = "J. Cheminform.",
publisher = "Springer Science and Business Media LLC",
volume = 16,
number = 1,
pages = "93",
month = aug,
year = 2024,
doi = "10.1186/s13321-024-00881-6"
}</code></pre>
</div>
</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Pathway Prediction System</h2>
<p class="mb-4">
If you use the pathway prediction functionality, please also cite:
</p>
<div class="card bg-base-200 mb-6">
<div class="card-body">
<h3 class="font-semibold mb-2">Predicting Biodegradation Pathways Using a Hybrid Relative Reasoning Model</h3>
<p class="text-sm mb-4">
<strong>Authors:</strong> Wicker, J., Fenner, K., Ellis, L., Kramer, S.<br>
<strong>Journal:</strong> Biotechnology and Bioengineering, 110(3), 837-846<br>
<strong>Year:</strong> 2013<br>
<strong>DOI:</strong> <a href="https://doi.org/10.1002/bit.24744" target="_blank" class="link link-primary">10.1002/bit.24744</a>
</p>
<div class="collapse collapse-arrow bg-base-300">
<input type="checkbox" />
<div class="collapse-title font-medium text-sm">
Show BibTeX
</div>
<div class="collapse-content">
<pre class="text-xs overflow-x-auto"><code>@article{wicker2013predicting,
title={Predicting biodegradation pathways using a hybrid relative reasoning model},
author={Wicker, J{\"o}rg and Fenner, Kathrin and Ellis, Lynda and Kramer, Stefan},
journal={Biotechnology and Bioengineering},
volume={110},
number={3},
pages={837--846},
year={2013},
publisher={Wiley Online Library},
doi={10.1002/bit.24744}
}</code></pre>
</div>
</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Machine Learning Models</h2>
<h3 class="text-xl font-semibold mt-6 mb-3">enviPath-Transformer (Latest ML Model)</h3>
<div class="card bg-base-200 mb-6">
<div class="card-body">
<p class="text-sm mb-2">
If you use the Transformer-based prediction model, please cite:
</p>
<p class="text-sm mb-4">
<strong>Authors:</strong> Brydon, L., Zhang, K., Dobbie, G., Taškova, K., and Wicker, J.<br>
<strong>Journal:</strong> Journal of Cheminformatics, 17(1), 21<br>
<strong>Year:</strong> 2025<br>
<strong>DOI:</strong> <a href="https://doi.org/10.1186/s13321-025-00969-7" target="_blank" class="link link-primary">10.1186/s13321-025-00969-7</a>
</div>
</div>
<h3 class="text-xl font-semibold mt-6 mb-3">Relative Reasoning Models</h3>
<div class="card bg-base-200 mb-6">
<div class="card-body">
<p class="text-sm mb-2">
For relative reasoning and machine learning approaches:
</p>
<p class="text-sm mb-4">
<strong>Authors:</strong> Fenner, K., Gao, J., Kramer, S., Ellis, L., Wackett, L.<br>
<strong>Journal:</strong> Environmental Science & Technology, 42(15), 5761-5767<br>
<strong>Year:</strong> 2008<br>
<strong>DOI:</strong> <a href="https://doi.org/10.1021/es800408g" target="_blank" class="link link-primary">10.1021/es800408g</a>
</p>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Package-Specific Citations</h2>
<p class="mb-4">
If you use data from a specific package within enviPath, please also acknowledge the package creators
and cite any relevant publications associated with that package. Package-specific citation information
is available on each package's detail page.
</p>
<h2 class="text-2xl font-semibold mt-8 mb-4">Example Acknowledgment Text</h2>
<p class="mb-4">
You may use the following text in your acknowledgments section:
</p>
<div class="card bg-base-200 mb-6">
<div class="card-body">
<p class="italic text-sm">
"Biotransformation pathway predictions were performed using enviPath
(<a href="https://envipath.org" target="_blank" class="link link-primary">https://envipath.org</a>),
a database and prediction system for microbial biotransformation of organic environmental contaminants."
</p>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Additional Resources</h2>
<p class="mb-4">
For a complete list of publications related to enviPath, please visit:
</p>
<ul class="list-disc list-inside mb-6 space-y-2">
<li>
<a href="https://community.envipath.org/" target="_blank" class="link link-primary">
enviPath Community - Publications Section
</a>
</li>
<li>
<a href="https://wiki.envipath.org/" target="_blank" class="link link-primary">
enviPath Documentation - References
</a>
</li>
</ul>
<div class="alert alert-success mt-8">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 class="font-bold">Thank You!</h3>
<div class="text-sm">
Thank you for citing enviPath. Your citations help demonstrate the impact of our work and
enable us to continue providing this resource to the scientific community.
</div>
</div>
</div>
<div class="divider my-8"></div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Questions?</h2>
<p class="mb-4">
If you're unsure which papers to cite or have questions about citations, please
<a href="/contact" class="link link-primary">contact us</a> or ask on our
<a href="https://community.envipath.org/" target="_blank" class="link link-primary">community forums</a>.
</p>
</div>
</div>
</div>
{% endblock main_content %}

View File

@ -0,0 +1,169 @@
{% 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>Contact & Support</li>
</ul>
</div>
<!-- Main Content -->
<div class="bg-base-100 shadow-xl rounded-lg p-8">
<h1 class="text-4xl font-bold mb-6">Contact & Support</h1>
<div class="prose max-w-none">
<p class="text-lg mb-6">
We're here to help! Whether you have questions about using enviPath, need technical support,
or want to discuss collaboration opportunities, we'd love to hear from you.
</p>
<!-- Contact Methods Grid -->
<div class="grid md:grid-cols-2 gap-6 mb-8 not-prose">
<!-- Community Support -->
<div class="card bg-primary text-primary-content shadow-lg">
<div class="card-body">
<h2 class="card-title">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 8h2a2 2 0 012 2v6a2 2 0 01-2 2h-2v4l-4-4H9a1.994 1.994 0 01-1.414-.586m0 0L11 14h4a2 2 0 002-2V6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2v4l.586-.586z" />
</svg>
Community Forums
</h2>
<p>Get help from our active community of users and developers.</p>
<div class="card-actions justify-end">
<a href="https://community.envipath.org/" target="_blank" class="btn btn-secondary">Visit Forums</a>
</div>
</div>
</div>
<!-- Documentation -->
<div class="card bg-secondary text-secondary-content shadow-lg">
<div class="card-body">
<h2 class="card-title">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
Documentation
</h2>
<p>Browse our comprehensive documentation and tutorials.</p>
<div class="card-actions justify-end">
<a href="https://wiki.envipath.org/" target="_blank" class="btn btn-accent">Read Docs</a>
</div>
</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Get Support</h2>
<p class="mb-4">
For the fastest response, we recommend using our community forums where you can:
</p>
<ul class="list-disc list-inside mb-6 space-y-2">
<li>Ask questions and get answers from the community</li>
<li>Report bugs or technical issues</li>
<li>Request new features</li>
<li>Share your research and findings</li>
<li>Find tutorials and how-to guides</li>
</ul>
<div class="alert alert-info mb-8">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">Before Contacting Support</h3>
<div class="text-sm">Please check our documentation and search the community forums for existing answers.</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Business & Collaboration Inquiries</h2>
<p class="mb-4">
For business inquiries, partnership opportunities, or custom development requests, please reach out
through our community platform or LinkedIn.
</p>
<div class="card bg-base-200 mb-8">
<div class="card-body">
<h3 class="card-title">enviPath Ltd.</h3>
<p>Biodegradation prediction since 2015</p>
<div class="flex gap-4 mt-4">
<a href="https://www.linkedin.com/company/envipath/" target="_blank" class="btn btn-outline btn-sm">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 fill-current mr-2">
<path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/>
</svg>
LinkedIn
</a>
<a href="https://www.youtube.com/@envipath7231" target="_blank" class="btn btn-outline btn-sm">
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" class="w-4 h-4 fill-current mr-2">
<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.814z"/>
</svg>
YouTube
</a>
</div>
</div>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Report Security Issues</h2>
<p class="mb-4">
If you discover a security vulnerability, please report it responsibly. Do not post security issues
publicly. Instead, please contact us directly through the community forums using a private message
to the administrators.
</p>
<h2 class="text-2xl font-semibold mt-8 mb-4">Media & Press</h2>
<p class="mb-4">
For media inquiries, press releases, or interview requests, please reach out through our
community platform or LinkedIn.
</p>
<div class="divider my-8"></div>
<h2 class="text-2xl font-semibold mt-8 mb-4">Frequently Asked Questions</h2>
<div class="join join-vertical w-full">
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="radio" name="faq-accordion" />
<div class="collapse-title text-lg font-medium">
How do I get started with enviPath?
</div>
<div class="collapse-content">
<p>Simply visit our homepage and try the prediction tool! For full access to all features, create a free account. Check out our <a href="https://wiki.envipath.org/" target="_blank" class="link link-primary">documentation</a> for detailed guides.</p>
</div>
</div>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="radio" name="faq-accordion" />
<div class="collapse-title text-lg font-medium">
Is enviPath free to use?
</div>
<div class="collapse-content">
<p>Yes! enviPath is free for academic research and educational purposes. For commercial use or custom solutions, please contact us for licensing options.</p>
</div>
</div>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="radio" name="faq-accordion" />
<div class="collapse-title text-lg font-medium">
How do I cite enviPath in my research?
</div>
<div class="collapse-content">
<p>Please visit our <a href="/cite" class="link link-primary">citation page</a> for detailed information on how to properly cite enviPath in your publications.</p>
</div>
</div>
<div class="collapse collapse-arrow join-item border border-base-300">
<input type="radio" name="faq-accordion" />
<div class="collapse-title text-lg font-medium">
Can I contribute data to enviPath?
</div>
<div class="collapse-content">
<p>Yes! We welcome contributions from the scientific community. Please visit our <a href="https://community.envipath.org/" target="_blank" class="link link-primary">community forums</a> to learn about the contribution process.</p>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock main_content %}

View File

@ -0,0 +1,145 @@
{% 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>Cookie Policy</li>
</ul>
</div>
<!-- Main Content -->
<div class="bg-base-100 shadow-xl rounded-lg p-8">
<h1 class="text-4xl font-bold mb-6">Cookie Policy</h1>
<div class="prose max-w-none">
<p class="text-lg mb-6">
This Cookie Policy explains how enviPath uses cookies and similar technologies to recognize you when
you visit our platform. It explains what these technologies are and why we use them.
</p>
<h2 class="text-2xl font-semibold mt-8 mb-4">1. What Are Cookies?</h2>
<p class="mb-4">
Cookies are small data files that are placed on your computer or mobile device when you visit a website.
Cookies are widely used by website owners to make their websites work, or to work more efficiently, as
well as to provide reporting information.
</p>
<h2 class="text-2xl font-semibold mt-8 mb-4">2. Why We Use Cookies</h2>
<p class="mb-4">We use cookies for several reasons:</p>
<ul class="list-disc list-inside mb-4 space-y-2">
<li><strong>Essential Cookies:</strong> Required for the platform to function properly</li>
<li><strong>Analytics Cookies:</strong> Help us understand how visitors interact with our platform</li>
<li><strong>Functional Cookies:</strong> Enable enhanced functionality and personalization</li>
<li><strong>Security Cookies:</strong> Authenticate users and prevent fraudulent use</li>
</ul>
<h2 class="text-2xl font-semibold mt-8 mb-4">3. Types of Cookies We Use</h2>
<div class="overflow-x-auto mb-6">
<table class="table table-zebra w-full">
<thead>
<tr>
<th>Cookie Type</th>
<th>Purpose</th>
<th>Duration</th>
</tr>
</thead>
<tbody>
<tr>
<td><strong>Session Cookies</strong></td>
<td>Maintain your login state and session data</td>
<td>Session</td>
</tr>
<tr>
<td><strong>CSRF Token</strong></td>
<td>Security protection against cross-site request forgery</td>
<td>Session</td>
</tr>
<tr>
<td><strong>Matomo Analytics</strong></td>
<td>Track usage patterns and improve our services</td>
<td>13 months</td>
</tr>
<tr>
<td><strong>OAuth Tokens</strong></td>
<td>Authentication and authorization for API access</td>
<td>Varies</td>
</tr>
</tbody>
</table>
</div>
<h2 class="text-2xl font-semibold mt-8 mb-4">4. Matomo Analytics</h2>
<p class="mb-4">
We use Matomo, an open-source web analytics platform, to collect information about how visitors use enviPath.
Matomo uses cookies to collect standard internet log information and visitor behavior patterns. The information
generated by cookies about your use of the platform is transmitted to our servers.
</p>
<p class="mb-4">
We analyze this information to:
</p>
<ul class="list-disc list-inside mb-4 space-y-2">
<li>Understand how users interact with our platform</li>
<li>Identify popular features and areas for improvement</li>
<li>Detect and diagnose technical issues</li>
<li>Generate reports on platform usage</li>
</ul>
<h2 class="text-2xl font-semibold mt-8 mb-4">5. Third-Party Cookies</h2>
<p class="mb-4">
In addition to our own cookies, we may use various third-party cookies to report usage statistics and
provide integrated services:
</p>
<ul class="list-disc list-inside mb-4 space-y-2">
<li><strong>Discourse Community:</strong> For our community forums at community.envipath.org</li>
<li><strong>External CDNs:</strong> For loading libraries like jQuery and Font Awesome</li>
</ul>
<h2 class="text-2xl font-semibold mt-8 mb-4">6. Managing Cookies</h2>
<p class="mb-4">
Most web browsers allow you to control cookies through their settings. However, if you limit the ability
of websites to set cookies, you may worsen your overall user experience, as some features may not function
properly.
</p>
<div class="alert alert-warning mb-6">
<svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
<span><strong>Note:</strong> Disabling essential cookies will prevent you from using certain features of enviPath.</span>
</div>
<h3 class="text-xl font-semibold mt-6 mb-3">Browser Settings</h3>
<p class="mb-4">You can manage cookies in your browser settings:</p>
<ul class="list-disc list-inside mb-4 space-y-2">
<li><strong>Chrome:</strong> Settings → Privacy and security → Cookies and other site data</li>
<li><strong>Firefox:</strong> Options → Privacy & Security → Cookies and Site Data</li>
<li><strong>Safari:</strong> Preferences → Privacy → Cookies and website data</li>
<li><strong>Edge:</strong> Settings → Cookies and site permissions → Cookies and site data</li>
</ul>
<h2 class="text-2xl font-semibold mt-8 mb-4">7. Updates to This Policy</h2>
<p class="mb-4">
We may update this Cookie Policy from time to time to reflect changes in technology, legislation, or our
operations. Please check this page regularly for updates.
</p>
<h2 class="text-2xl font-semibold mt-8 mb-4">8. Contact Us</h2>
<p class="mb-4">
If you have questions about our use of cookies, please <a href="/contact" class="link link-primary">contact us</a>.
</p>
<div class="alert alert-info mt-8">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<span>Last updated: 2025</span>
</div>
</div>
</div>
</div>
{% endblock main_content %}

139
templates/static/legal.html Normal file
View File

@ -0,0 +1,139 @@
{% 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>Legal</li>
</ul>
</div>
<!-- Main Content -->
<div class="bg-base-100 shadow-xl rounded-lg p-8">
<h1 class="text-4xl font-bold mb-6">Legal Information</h1>
<div class="prose max-w-none">
<p class="text-lg mb-6">
Welcome to enviPath's legal information center. Here you can find all our legal documents,
policies, and terms that govern the use of our platform.
</p>
<!-- Legal Documents Grid -->
<div class="grid md:grid-cols-2 gap-6 mb-8">
<!-- Terms of Use -->
<div class="card bg-base-200 shadow-md">
<div class="card-body">
<h3 class="card-title text-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Terms of Use
</h3>
<p class="text-sm mb-4">
Our terms and conditions that govern the use of enviPath services, including
licensing, user responsibilities, and platform usage guidelines.
</p>
<div class="card-actions justify-end">
<a href="/terms" class="btn btn-primary btn-sm">Read Terms</a>
</div>
</div>
</div>
<!-- Privacy Policy -->
<div class="card bg-base-200 shadow-md">
<div class="card-body">
<h3 class="card-title text-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
Privacy Policy
</h3>
<p class="text-sm mb-4">
How we collect, use, and protect your personal information when you use
enviPath, including data handling practices and your privacy rights.
</p>
<div class="card-actions justify-end">
<a href="/privacy" class="btn btn-primary btn-sm">Read Policy</a>
</div>
</div>
</div>
<!-- Cookie Policy -->
<div class="card bg-base-200 shadow-md">
<div class="card-body">
<h3 class="card-title text-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
Cookie Policy
</h3>
<p class="text-sm mb-4">
Information about the cookies and tracking technologies we use on enviPath,
including analytics and essential functionality cookies.
</p>
<div class="card-actions justify-end">
<a href="/cookie-policy" class="btn btn-primary btn-sm">Read Policy</a>
</div>
</div>
</div>
<!-- Citation Guidelines -->
<div class="card bg-base-200 shadow-md">
<div class="card-body">
<h3 class="card-title text-primary">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Citation Guidelines
</h3>
<p class="text-sm mb-4">
How to properly cite enviPath in your research publications and academic work,
including recommended citation formats and acknowledgments.
</p>
<div class="card-actions justify-end">
<a href="/cite" class="btn btn-primary btn-sm">View Guidelines</a>
</div>
</div>
</div>
</div>
<!-- Quick Access Section -->
<h2 class="text-2xl font-semibold mt-8 mb-4">Quick Access</h2>
<div class="grid md:grid-cols-3 gap-4 mb-6">
<a href="/terms" class="btn btn-outline btn-sm w-full">Terms of Use</a>
<a href="/privacy" class="btn btn-outline btn-sm w-full">Privacy Policy</a>
<a href="/cookie-policy" class="btn btn-outline btn-sm w-full">Cookie Policy</a>
</div>
<!-- Important Information -->
<div class="alert alert-info mt-8">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="stroke-current shrink-0 w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<h3 class="font-bold">Important Notice</h3>
<p class="text-sm">
By using enviPath, you agree to be bound by our Terms of Use and Privacy Policy.
We recommend reviewing these documents regularly as they may be updated from time to time.
For questions about our legal policies, please <a href="/contact" class="link link-primary">contact us</a>.
</p>
</div>
</div>
<!-- Contact Information -->
<div class="card bg-primary text-primary-content mt-8">
<div class="card-body">
<h3 class="card-title">Questions About Our Legal Policies?</h3>
<p>If you have any questions or concerns about our legal documents, please don't hesitate to reach out to us.</p>
<div class="card-actions justify-end mt-4">
<a href="/contact" class="btn btn-secondary">Contact Us</a>
</div>
</div>
</div>
</div>
</div>
</div>
{% endblock main_content %}

View File

@ -1,59 +1,236 @@
{% extends "static/static_base.html" %}
{% extends "static/login_base.html" %}
{% block title %}enviPath - Sign In{% endblock %}
{% block extra_styles %}
/* Tab styling */ .tab-content { display: none; } .tab-content.active {
display: block; } input[type="radio"].tab-radio { display: none; } .tab-label
{ cursor: pointer; padding: 0.75rem 1.5rem; border-bottom: 2px solid
transparent; transition: all 0.3s ease; } .tab-label:hover { background-color:
rgba(0, 0, 0, 0.05); } input[type="radio"].tab-radio:checked + .tab-label {
border-bottom-color: #3b82f6; font-weight: 600; }
{% endblock %}
{% block content %}
{% if message %}
<div class="alert alert-danger" role="alert">
{{ message }}
</div>
{% elif success_message %}
<div class="alert alert-success" role="alert">
{{ success_message }}
</div>
{% else %}
<div class="alert alert-success" role="alert">
Kia ora! We are running our closed beta tests at the moment. It would be great to get your help as tester,
you
can apply to become tester by registering for this page, just hit the button below. More information on the
beta
test is available in our <a href="https://community.envipath.org/t/apply-to-join-our-closed-beta/95">
community
form</a>
</div>
{% endif %}
<div class="modal-dialog" style="margin:30px auto; z-index:9999;">
<div class="modal-content">
<div class="modal-body">
<form class="form-horizontal" method="post" action="{% url 'login' %}">
{% csrf_token %}
<fieldset>
<div class="control-group">
<label class="control-label" for="username">Username</label>
<div class="controls">
<input required id="username" name="username" type="text"
class="form-control" placeholder="username" autocomplete="username">
</div>
<label class="control-label" for="passwordinput">Password:</label>
<div class="controls">
<input required id="passwordinput" name="password" class="form-control"
type="password" placeholder="********" autocomplete="current-password">
</div>
<div class="form-group text-center" style="margin-top:15px;">
<a href="{% url 'password_reset' %}">Forgot your password?</a>
</div>
</div>
<!-- Tab Navigation -->
<div class="border-b border-base-300 mb-6">
<div class="flex justify-start">
<input
type="radio"
name="auth-tab"
id="tab-signin"
class="tab-radio"
checked
/>
<label for="tab-signin" class="tab-label">Sign In</label>
<div class="control-group">
<label class="control-label" for="signin"></label>
<div class="controls">
<button id="signin" name="signin" class="btn btn-success pull-right">Sign In
</button>
<a class="btn btn-primary" href="{% url 'register' %}">Create an Account</a>
</div>
</div>
<input type="hidden" name="next" value="{{ next }}"/>
</fieldset>
</form>
</div>
</div>
<input type="radio" name="auth-tab" id="tab-signup" class="tab-radio" />
<label for="tab-signup" class="tab-label">Register</label>
</div>
</div>
<!-- Sign In Tab -->
<div id="content-signin" class="tab-content active">
<form method="post" action="{% url 'login' %}" class="space-y-4">
{% csrf_token %}
<input type="hidden" name="login" value="true" />
<div class="form-control">
<label class="label" for="username">
<span class="label-text">Username</span>
</label>
<input
type="text"
id="username"
name="username"
placeholder="username"
class="input input-bordered w-full"
required
autocomplete="username"
/>
</div>
<div class="form-control">
<label class="label" for="passwordinput">
<span class="label-text">Password</span>
</label>
<input
type="password"
id="passwordinput"
name="password"
placeholder="••••••••"
class="input input-bordered w-full"
required
autocomplete="current-password"
/>
</div>
<div class="text-right">
<a href="{% url 'password_reset' %}" class="link link-primary text-sm"
>Forgot password?</a
>
</div>
<input type="hidden" name="next" value="{{ next }}" />
<button type="submit" name="signin" class="btn btn-primary w-full">
Sign In
</button>
</form>
</div>
<!-- Register Tab -->
<div id="content-signup" class="tab-content">
<div class="alert alert-info mb-4 text-sm">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
class="h-6 w-6 shrink-0 stroke-current"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
<div>
<div class="font-bold">Password Requirements</div>
<div class="text-xs mt-1">
• 8 to 30 characters<br />
• Upper and lower case letters<br />
• Digits and special characters (_, -, +)
</div>
</div>
</div>
<form method="post" action="{% url 'register' %}" class="space-y-4">
{% csrf_token %}
<input type="hidden" name="register" value="true" />
<div class="form-control">
<label class="label" for="userid">
<span class="label-text">Username</span>
</label>
<input
type="text"
id="userid"
name="username"
placeholder="username"
class="input input-bordered w-full"
required
autocomplete="username"
/>
</div>
<div class="form-control">
<label class="label" for="email">
<span class="label-text">Email</span>
</label>
<input
type="email"
id="email"
name="email"
placeholder="user@envipath.org"
class="input input-bordered w-full"
required
autocomplete="email"
/>
</div>
<div class="form-control">
<label class="label" for="password">
<span class="label-text">Password</span>
</label>
<input
type="password"
id="password"
name="password"
placeholder="••••••••"
class="input input-bordered w-full"
required
autocomplete="new-password"
/>
</div>
<div class="form-control">
<label class="label" for="rpassword">
<span class="label-text">Repeat Password</span>
</label>
<input
type="password"
id="rpassword"
name="rpassword"
placeholder="••••••••"
class="input input-bordered w-full"
required
autocomplete="new-password"
/>
</div>
<input type="hidden" name="next" value="{{ next }}" />
<button type="submit" name="confirmsignup" class="btn btn-success w-full">
Sign Up
</button>
</form>
<!-- Why Register Section -->
<div class="mt-6 p-4 bg-base-200 rounded-lg">
<h3 class="font-semibold mb-2">Why register?</h3>
<p class="text-sm mb-2">
enviPath is free for academic research and educational purposes.
However, we require registration to ensure the security of the platform
and to prevent abuse.
</p>
<p class="text-sm mt-3">
Questions? Check our
<a
href="https://wiki.envipath.org/"
target="_blank"
class="link link-primary"
>documentation</a
>
or the
<a
href="https://community.envipath.org/"
target="_blank"
class="link link-primary"
>community forums</a
>.
</p>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
// Tab switching functionality
document.querySelectorAll('input[name="auth-tab"]').forEach((radio) => {
radio.addEventListener("change", function () {
// Hide all content
document.querySelectorAll(".tab-content").forEach((content) => {
content.classList.remove("active");
});
// Show selected content
const contentId = "content-" + this.id.replace("tab-", "");
document.getElementById(contentId).classList.add("active");
});
});
// Check for hash in URL to auto-select tab
window.addEventListener("DOMContentLoaded", function () {
const hash = window.location.hash.substring(1); // Remove the # symbol
if (hash === "signup" || hash === "signin") {
const tabRadio = document.getElementById("tab-" + hash);
if (tabRadio) {
tabRadio.checked = true;
// Trigger change event to show correct content
tabRadio.dispatchEvent(new Event("change"));
}
}
});
</script>
{% endblock %}

View File

@ -0,0 +1,83 @@
{% load static %}
<!DOCTYPE html>
<html lang="en" data-theme="envipath">
<head>
<meta charset="UTF-8">
<title>{% block title %}enviPath{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="shortcut icon" type="image/png" href="{% static 'images/favicon.ico' %}"/>
<!-- Tailwind CSS Output -->
<link href="{% static 'css/output.css' %}" rel="stylesheet" type="text/css"/>
<style>
{% block extra_styles %}{% endblock %}
</style>
</head>
<body class="bg-base-100">
<div class="flex h-screen">
<!-- Left side - Hero Image -->
<div class="hidden lg:flex lg:w-1/2 bg-cover bg-center bg-no-repeat items-center justify-center p-12"
style="background-image: linear-gradient(135deg, color-mix(in oklab, var(--color-primary) 30%, transparent) 0%, color-mix(in oklab, var(--color-primary-600) 40%, transparent) 100%), url('{% static "/images/hero.png" %}');">
<div class="text-left text-white space-y-6">
<svg class="h-16 w-auto fill-white" viewBox="0 0 104 26" role="img">
<use href='{% static "/images/logo-name.svg" %}#ep-logo-name' />
</svg>
<p class="text-lg max-w-md mx-auto">
Predict and explore microbial biotransformation pathways for environmental contaminants
</p>
</div>
</div>
<!-- Right side - Content -->
<div class="w-full lg:w-1/2 flex flex-col items-center justify-center p-8 overflow-y-auto">
<div class="w-full max-w-md flex-1 flex flex-col justify-center">
<!-- Logo for mobile -->
<div class="lg:hidden text-center mb-8">
<svg class="h-12 w-auto mx-auto fill-current" viewBox="0 0 104 26" role="img">
<use href='{% static "/images/logo-name.svg" %}#ep-logo-name' />
</svg>
</div>
<!-- Messages -->
{% if message %}
<div class="alert alert-error mb-6">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{{ message }}</span>
</div>
{% elif success_message %}
<div class="alert alert-success mb-6">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span>{{ success_message }}</span>
</div>
{% endif %}
{% block content %}{% endblock %}
</div>
<!-- Footer with legal links - positioned at bottom of right column -->
<div class="w-full mt-auto pt-4">
<div class="flex justify-center items-center space-x-6 text-sm text-base-content/50">
<a href="/legal" class="link link-hover">Legal</a>
<span class="text-base-content/30"></span>
<a href="/terms" class="link link-hover">Terms of Use</a>
<span class="text-base-content/30"></span>
<a href="/privacy" class="link link-hover">Privacy Policy</a>
<span class="text-base-content/30"></span>
<a href="/cookie-policy" class="link link-hover">Cookie Policy</a>
</div>
</div>
</div>
</div>
{% block extra_scripts %}{% endblock %}
</body>
</html>

View File

@ -1,5 +1,30 @@
{% extends "static/static_base.html" %}
{% extends "static/login_base.html" %}
{% block title %}enviPath - Password Reset Complete{% endblock %}
{% block content %}
<p>Your password has been reset successfully. <a href="{% url 'login' %}">Login</a></p>
<!-- Success Icon -->
<div class="flex justify-center mb-6">
<div class="rounded-full bg-success/20 p-6">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 stroke-success" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
</div>
<!-- Title -->
<div class="text-center mb-8">
<h2 class="text-3xl font-bold mb-4">Password Reset Complete!</h2>
<p class="text-base-content/70 mb-4">
Your password has been successfully reset.
</p>
<p class="text-sm text-base-content/60">
You can now sign in with your new password.
</p>
</div>
<!-- Actions -->
<div class="space-y-4">
<a href="{% url 'login' %}" class="btn btn-primary w-full">Sign In</a>
</div>
{% endblock %}

View File

@ -1,31 +1,94 @@
{% extends "static/static_base.html" %}
{% extends "static/login_base.html" %}
{% block title %}enviPath - Set New Password{% endblock %}
{% block content %}
<div class="modal-dialog" style="margin:30px auto; z-index:9999;">
<div class="modal-content">
<div class="modal-body">
<h2>Enter new password</h2>
<form method="post">
{% csrf_token %}
<p>
<label for="id_new_password1">New password:</label>
<input type="password" class="form-control" name="new_password1" autocomplete="new-password"
required=""
aria-describedby="id_new_password1_helptext" id="id_new_password1">
<span class="helptext" id="id_new_password1_helptext"></span></p>
<!-- Title -->
<div class="mb-8">
<h2 class="text-3xl font-bold mb-2">Set New Password</h2>
<p class="text-base-content/70">Please enter your new password below.</p>
</div>
{{ form.new_password1.help_text|safe }}
<p>
<label for="id_new_password2">New password confirmation:</label>
<input type="password" class="form-control" name="new_password2" autocomplete="new-password"
required=""
aria-describedby="id_new_password2_helptext" id="id_new_password2">
{{ form.new_password2.help_text|safe }}
</p>
<button class="btn btn-primary" type="submit">Reset Password</button>
</form>
<!-- Messages -->
{% if validlink %}
<!-- Password Requirements Info -->
<div class="alert alert-info mb-4 text-sm">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="h-6 w-6 shrink-0 stroke-current">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
<div>
<div class="font-bold">Password Requirements</div>
<div class="text-xs mt-1">
• 8 to 30 characters<br>
• Upper and lower case letters<br>
• Digits and special characters (_, -, +)
</div>
</div>
</div>
<!-- Reset Password Form -->
<form method="post" class="space-y-4">
{% csrf_token %}
<div class="form-control">
<label class="label" for="id_new_password1">
<span class="label-text">New Password</span>
</label>
<input type="password" id="id_new_password1" name="new_password1" placeholder="••••••••"
class="input input-bordered w-full" required autocomplete="new-password">
{% if form.new_password1.help_text %}
<label class="label">
<span class="label-text-alt text-base-content/60">{{ form.new_password1.help_text }}</span>
</label>
{% endif %}
{% if form.new_password1.errors %}
<label class="label">
<span class="label-text-alt text-error">{{ form.new_password1.errors|join:", " }}</span>
</label>
{% endif %}
</div>
<div class="form-control">
<label class="label" for="id_new_password2">
<span class="label-text">Confirm New Password</span>
</label>
<input type="password" id="id_new_password2" name="new_password2" placeholder="••••••••"
class="input input-bordered w-full" required autocomplete="new-password">
{% if form.new_password2.help_text %}
<label class="label">
<span class="label-text-alt text-base-content/60">{{ form.new_password2.help_text }}</span>
</label>
{% endif %}
{% if form.new_password2.errors %}
<label class="label">
<span class="label-text-alt text-error">{{ form.new_password2.errors|join:", " }}</span>
</label>
{% endif %}
</div>
<button type="submit" class="btn btn-primary w-full">Reset Password</button>
</form>
{% else %}
<!-- Invalid Link -->
<div class="alert alert-error mb-6">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<div class="font-bold">Invalid Reset Link</div>
<div class="text-sm">This password reset link is invalid or has expired.</div>
</div>
</div>
<div class="space-y-4">
<a href="{% url 'password_reset' %}" class="btn btn-primary w-full">Request New Reset Link</a>
</div>
{% endif %}
<!-- Back to Sign In -->
<div class="mt-6 text-center text-sm text-base-content/70">
<p>Remember your password?
<a href="{% url 'login' %}" class="link link-primary">Sign in</a>
</p>
</div>
{% endblock %}

View File

@ -1,7 +1,30 @@
{% extends "static/static_base.html" %}
{% extends "static/login_base.html" %}
{% block title %}enviPath - Reset Email Sent{% endblock %}
{% block content %}
<div class="alert alert-success" role="alert">
An email has been sent with instructions to reset your password.
<!-- Success Icon -->
<div class="flex justify-center mb-6">
<div class="rounded-full bg-success/20 p-6">
<svg xmlns="http://www.w3.org/2000/svg" class="h-16 w-16 stroke-success" fill="none" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 19v-8.93a2 2 0 01.89-1.664l7-4.666a2 2 0 012.22 0l7 4.666A2 2 0 0121 10.07V19M3 19a2 2 0 002 2h14a2 2 0 002-2M3 19l6.75-4.5M21 19l-6.75-4.5M3 10l6.75 4.5M21 10l-6.75 4.5m0 0l-1.14.76a2 2 0 01-2.22 0l-1.14-.76" />
</svg>
</div>
</div>
<!-- Title -->
<div class="text-center mb-8">
<h2 class="text-3xl font-bold mb-4">Check Your Email</h2>
<p class="text-base-content/70 mb-4">
We've sent an email with instructions to reset your password.
</p>
<p class="text-sm text-base-content/60">
If you don't see it in your inbox, please check your spam folder.
</p>
</div>
<!-- Actions -->
<div class="space-y-4">
<a href="{% url 'login' %}" class="btn btn-primary w-full">Back to Sign In</a>
</div>
{% endblock %}

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