17 Commits

Author SHA1 Message Date
d5ebb23622 [Fix] AppDomain Leftovers (#161)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#161
2025-10-16 08:17:39 +13:00
93dd811e39 [Fix] Pathway SVG Export (#157)
Fixes #103

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#157
2025-10-16 02:25:30 +13:00
9a4735246f [Fix] Fix for sending mails (#160)
Captured by Sentry: https://envipath-limited.sentry.io/issues/66662009/?project=4509569727922256

```
SMTPSenderRefused
Level: Error
(504, b'5.5.2 <webmaster@localhost>: Sender address rejected: need fully-qualified address', 'webmaster@localhost')
```

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#160
2025-10-16 02:24:51 +13:00
1f863fdcd6 [Fix] Remove Scenarios from Objects (#159)
Fixes #155

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#159
2025-10-15 20:23:52 +13:00
1effaeb342 [Migration] EnzymeLink Migration (#158)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#158
2025-10-15 19:57:03 +13:00
386098b8a6 [Feature] EnzymeLink Annotations (#152)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#152
2025-10-15 19:35:26 +13:00
ef697ac5f5 [Fix] Added UZH Affiliation, Update UoA Images (#153)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#153
2025-10-15 19:25:23 +13:00
68a3f3b982 [Feature] Alias Support (#151)
Fixes #149

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#151
2025-10-09 23:14:34 +13:00
afeb56622c [Chore] Linted Files (#150)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#150
2025-10-09 07:25:13 +13:00
22f0bbe10b [Feature] Eval package evaluation
`evaluate_model` in `PackageBasedModel` and `EnviFormer` now use evaluation packages if any are present instead of the random splits.

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#148
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-10-08 19:03:21 +13:00
36879c266b [Feature] Documentation for development setup
## Summary

This PR improves the local development setup experience by adding Docker Compose and Makefile for streamlined setup.

## Changes

- **Added `docker-compose.yml`**: for one-command PostgreSQL database setup
- **Added `Makefile`**: Convenient shortcuts for common dev tasks (\`make setup\`, \`make dev\`, etc.)
- **Updated `README.md`**: Quick development setup instructions using Make
-
- **Added**: RDkit installation pain point documentation
- **Fixed**: Made Java feature properly dependent

## Why these changes?

The application uses PostgreSQL-specific features (\`ArrayField\`) and requires an anonymous user created by the bootstrap command. This PR makes the setup process trivial for new developers:

```bash
cp .env.local.example .env
make setup  # Starts DB, runs migrations, bootstraps data
make dev    # Starts development server
```

Java fix:
Moved global Java import to inline to avoid everyone having to configure the Java path.

Numerous changes to view and settings.
- Applied ruff-formatting

## Testing

Verified complete setup from scratch works with:
- PostgreSQL running in Docker
- All migrations applied
- Bootstrap data loaded successfully
- Anonymous user created
- The development server starts correctly.

Co-authored-by: Tobias O <tobias.olenyi@tum.de>
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-authored-by: Liam <62733830+limmooo@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#143
Reviewed-by: jebus <lorsbach@envipath.com>
Reviewed-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-authored-by: t03i <mail+envipath@t03i.net>
Co-committed-by: t03i <mail+envipath@t03i.net>
2025-10-08 18:51:50 +13:00
c2c46fbfa7 [Migration] Added missing Migration for #141 (#147)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#147
2025-10-07 21:22:13 +13:00
d2f4fdc58a [Feature] Enviformer fine tuning and evaluation
## Changes
- I have finished the backend integration of EnviFormer (#19), this includes, dataset building, model finetuning, model evaluation and model prediction with the finetuned model.
- `PackageBasedModel` has been adjusted to be more abstract, this includes making the `_save_model` method and making `compute_averages` a static class function.
- I had to bump the python-version in `pyproject.toml` to >=3.12 from >=3.11 otherwise uv failed to install EnviFormer.
- The default EnviFormer loading during `settings.py` has been removed.

## Future Fix
I noticed you have a little bit of code in `PackageBasedModel` -> `evaluate_model` for using the `eval_packages` during evaluation instead of train/test splits on `data_packages`. It doesn't seem finished, I presume we want this for all models, so I will take care of that in a new branch/pullrequest after this request is merged.

Also, I haven't done anything for a POST request to finetune the model, I'm not sure if that is something we want now.

Co-authored-by: Liam Brydon <62733830+MyCreativityOutlet@users.noreply.github.com>
Reviewed-on: enviPath/enviPy#141
Reviewed-by: jebus <lorsbach@envipath.com>
Co-authored-by: liambrydon <lbry121@aucklanduni.ac.nz>
Co-committed-by: liambrydon <lbry121@aucklanduni.ac.nz>
2025-10-07 21:14:10 +13:00
3f2b046bd6 [Feature] More on Legacy API (#142)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#142
2025-10-03 00:07:30 +13:00
7ad4112343 [Feature] External Identifier/References
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#139
2025-10-02 00:40:00 +13:00
3f5bb76633 [Fix] Remove all Scenarios, catch empty SMILES, prevent default Package delete (#134)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#134
2025-09-30 19:10:57 +13:00
b757a07f91 [Misc] Performance improvements, SMIRKS Coverage, Minor Bugfixes (#132)
Bump Python Version to 3.12
Make use of "epauth" optional
Cache `srs` property of rules to speed up apply
Adjust view names for use of `reverse()`
Fix Views for Scenario Attachments
Added Simply Compare View/Template to identify differences between rdkit and ambit
Make migrations consistent with tests + compare
Fixes #76
Set default year for Scenario Modal
Fix html tags for package description
Added Tests for Pathway / Rule
Added remove stereo for apply

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#132
2025-09-26 19:33:03 +12:00
106 changed files with 19578 additions and 5750 deletions

22
.env.local.example Normal file
View File

@ -0,0 +1,22 @@
# Django settings
SECRET_KEY='a-secure-secret-key-for-development'
DEBUG=True
ALLOWED_HOSTS=*
# Database settings (using PostgreSQL for local development)
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=envipath
POSTGRES_SERVICE_NAME=localhost
POSTGRES_PORT=5432
# Celery settings
CELERY_BROKER_URL='redis://localhost:6379/0'
CELERY_RESULT_BACKEND='redis://localhost:6379/0'
FLAG_CELERY_PRESENT=False
# Other settings
LOG_LEVEL='INFO'
SERVER_URL='http://localhost:8000'
PLUGINS_ENABLED=True
EP_DATA_DIR='data'

View File

@ -16,4 +16,3 @@ POSTGRES_PORT=
# MAIL
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=

6
.gitignore vendored
View File

@ -5,4 +5,8 @@ static/admin/
static/django_extensions/
.env
debug.log
scratches/
scratches/
data/
.DS_Store

30
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,30 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.13.3
hooks:
# Run the linter.
- id: ruff-check
types_or: [python, pyi]
args: [--fix]
# Run the formatter.
- id: ruff-format
types_or: [python, pyi]
# - repo: local
# hooks:
# - id: django-check
# name: Run Django Check
# entry: uv run python manage.py check
# language: system
# pass_filenames: false
# types: [python]

View File

@ -1 +1 @@
3.10
3.12

View File

@ -1,2 +1,91 @@
# enviPy
## Local Development Setup
These instructions will guide you through setting up the project for local development.
### 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.
- Git
> **Note:** This application requires PostgreSQL, which uses `ArrayField`. Docker is the recommended way to run PostgreSQL locally.
### 1. Install Dependencies
This project uses `uv` to manage dependencies and `poe-the-poet` for task running. First, [install `uv` if you don't have it yet](https://docs.astral.sh/uv/guides/install-python/).
Then, sync the project dependencies. This will create a virtual environment in `.venv` and install all necessary packages, including `poe-the-poet`.
```bash
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.
### 2. Set Up Environment File
Copy the example environment file for local setup:
```bash
cp .env.local.example .env
```
This file contains the necessary environment variables for local development.
### 3. Quick Setup with Poe
The easiest way to set up the development environment is by using the `poe` task runner, which is executed via `uv run`.
```bash
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).
After setup, start the development server:
```bash
uv run poe dev
```
The application will be available at `http://localhost:8000`.
#### Other useful Poe commands:
You can list all available commands by running `uv run poe --help`.
```bash
uv run poe db-up # Start PostgreSQL only
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 clean # Remove database volumes (WARNING: destroys all data)
```
### Troubleshooting
* **Docker Connection Error:** If you see an error like `open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified` (on Windows), it likely means your Docker Desktop application is not running. Please start Docker Desktop and try the command again.
* **SSH Keys for Git Dependencies:** Some dependencies are installed from private git repositories and require SSH authentication. Ensure your SSH keys are configured correctly for Git.
* For a general guide, see [GitHub's official documentation](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent).
* **Windows Users:** If `uv sync` hangs while fetching git dependencies, you may need to explicitly configure Git to use the Windows OpenSSH client and use the `ssh-agent` to manage your key's passphrase.
1. **Point Git to the correct SSH executable:**
```powershell
git config --global core.sshCommand "C:/Windows/System32/OpenSSH/ssh.exe"
```
2. **Enable and use the SSH agent:**
```powershell
# Run these commands in an administrator PowerShell
Get-Service ssh-agent | Set-Service -StartupType Automatic -PassThru | Start-Service
# Add your key to the agent. It will prompt for the passphrase once.
ssh-add
```

20
docker-compose.dev.yml Normal file
View File

@ -0,0 +1,20 @@
services:
db:
image: postgres:15
container_name: envipath-postgres
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: envipath
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_data:

View File

@ -2,4 +2,4 @@
# Django starts so that shared_task will use this app.
from .celery import app as celery_app
__all__ = ('celery_app',)
__all__ = ("celery_app",)

View File

@ -4,8 +4,6 @@ from ninja import NinjaAPI
api = NinjaAPI()
from ninja import NinjaAPI
api_v1 = NinjaAPI(title="API V1 Docs", urls_namespace="api-v1")
api_legacy = NinjaAPI(title="Legacy API Docs", urls_namespace="api-legacy")

View File

@ -11,6 +11,6 @@ import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'envipath.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "envipath.settings")
application = get_asgi_application()

View File

@ -4,15 +4,15 @@ from celery import Celery
from celery.signals import setup_logging
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'envipath.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "envipath.settings")
app = Celery('envipath')
app = Celery("envipath")
# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
# should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')
app.config_from_object("django.conf:settings", namespace="CELERY")
@setup_logging.connect

View File

@ -9,6 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/4.2/ref/settings/
"""
import os
from pathlib import Path
@ -20,34 +21,35 @@ from sklearn.tree import DecisionTreeClassifier
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
load_dotenv(BASE_DIR / '.env', override=False)
load_dotenv(BASE_DIR / ".env", override=False)
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/4.2/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '7!VTW`aZqg/UBLsM.P=m)2]lWqg>{+:xUgG1"WO@bCyaHR2Up8XW&g<*3.F4l2gi9c.E3}dHyA0D`&z?u#U%^7HYbj],eP"g_MS|3BNMD[mI>s#<i/%2ngZ~Oy+/w&@]'
SECRET_KEY = os.environ.get("SECRET_KEY", "secret-key")
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
ALLOWED_HOSTS = os.environ['ALLOWED_HOSTS'].split(',')
DEBUG = os.environ.get("DEBUG", "False") == "True"
ALLOWED_HOSTS = os.environ["ALLOWED_HOSTS"].split(",")
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
"django.contrib.admin",
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
# 3rd party
'django_extensions',
'oauth2_provider',
"django_extensions",
"oauth2_provider",
# Custom
'epdb',
'migration',
'epauth',
"epdb",
"migration",
]
AUTHENTICATION_BACKENDS = [
@ -55,42 +57,42 @@ AUTHENTICATION_BACKENDS = [
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'oauth2_provider.middleware.OAuth2TokenMiddleware',
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
"django.middleware.clickjacking.XFrameOptionsMiddleware",
"oauth2_provider.middleware.OAuth2TokenMiddleware",
]
OAUTH2_PROVIDER = {
"PKCE_REQUIRED": False, # Accept PKCE requests but dont require them
}
if os.environ.get('REGISTRATION_MANDATORY', False) == 'True':
MIDDLEWARE.append('epdb.middleware.login_required_middleware.LoginRequiredMiddleware')
if os.environ.get("REGISTRATION_MANDATORY", False) == "True":
MIDDLEWARE.append("epdb.middleware.login_required_middleware.LoginRequiredMiddleware")
ROOT_URLCONF = 'envipath.urls'
ROOT_URLCONF = "envipath.urls"
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': (os.path.join(BASE_DIR, 'templates'),),
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
"BACKEND": "django.template.backends.django.DjangoTemplates",
"DIRS": (os.path.join(BASE_DIR, "templates"),),
"APP_DIRS": True,
"OPTIONS": {
"context_processors": [
"django.template.context_processors.debug",
"django.template.context_processors.request",
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
],
},
},
]
WSGI_APPLICATION = 'envipath.wsgi.application'
WSGI_APPLICATION = "envipath.wsgi.application"
# Database
# https://docs.djangoproject.com/en/4.2/ref/settings/#databases
@ -98,11 +100,11 @@ WSGI_APPLICATION = 'envipath.wsgi.application'
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql",
"USER": os.environ['POSTGRES_USER'],
"NAME": os.environ['POSTGRES_DB'],
"PASSWORD": os.environ['POSTGRES_PASSWORD'],
"HOST": os.environ['POSTGRES_SERVICE_NAME'],
"PORT": os.environ['POSTGRES_PORT']
"USER": os.environ["POSTGRES_USER"],
"NAME": os.environ["POSTGRES_DB"],
"PASSWORD": os.environ["POSTGRES_PASSWORD"],
"HOST": os.environ["POSTGRES_SERVICE_NAME"],
"PORT": os.environ["POSTGRES_PORT"],
}
}
@ -110,96 +112,87 @@ DATABASES = {
# https://docs.djangoproject.com/en/4.2/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
{"NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator"},
{"NAME": "django.contrib.auth.password_validation.MinimumLengthValidator"},
{"NAME": "django.contrib.auth.password_validation.CommonPasswordValidator"},
{"NAME": "django.contrib.auth.password_validation.NumericPasswordValidator"},
]
# Internationalization
# https://docs.djangoproject.com/en/4.2/topics/i18n/
LANGUAGE_CODE = 'en-us'
LANGUAGE_CODE = "en-us"
TIME_ZONE = 'UTC'
TIME_ZONE = "UTC"
USE_I18N = True
USE_TZ = True
# Default primary key field type
# https://docs.djangoproject.com/en/4.2/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
EMAIL_SUBJECT_PREFIX = "[enviPath] "
if DEBUG:
EMAIL_BACKEND = 'django.core.mail.backends.console.EmailBackend'
EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend"
else:
EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend"
EMAIL_USE_TLS = True
EMAIL_HOST = 'mail.gandi.net'
EMAIL_HOST_USER = os.environ['EMAIL_HOST_USER']
EMAIL_HOST_PASSWORD = os.environ['EMAIL_HOST_PASSWORD']
EMAIL_HOST = "mail.gandi.net"
EMAIL_HOST_USER = os.environ["EMAIL_HOST_USER"]
EMAIL_HOST_PASSWORD = os.environ["EMAIL_HOST_PASSWORD"]
EMAIL_PORT = 587
DEFAULT_FROM_EMAIL = os.environ["DEFAULT_FROM_EMAIL"]
SERVER_EMAIL = os.environ["SERVER_EMAIL"]
AUTH_USER_MODEL = "epdb.User"
ADMIN_APPROVAL_REQUIRED = os.environ.get('ADMIN_APPROVAL_REQUIRED', 'False') == 'True'
ADMIN_APPROVAL_REQUIRED = os.environ.get("ADMIN_APPROVAL_REQUIRED", "False") == "True"
# # SESAME
# SESAME_MAX_AGE = 300
# # TODO set to "home"
# LOGIN_REDIRECT_URL = "/"
LOGIN_URL = '/login/'
LOGIN_URL = "/login/"
SERVER_URL = os.environ.get('SERVER_URL', 'http://localhost:8000')
SERVER_URL = os.environ.get("SERVER_URL", "http://localhost:8000")
CSRF_TRUSTED_ORIGINS = [SERVER_URL]
AMBIT_URL = 'http://localhost:9001'
DEFAULT_VALUES = {
'description': 'no description'
}
AMBIT_URL = "http://localhost:9001"
DEFAULT_VALUES = {"description": "no description"}
EP_DATA_DIR = os.environ['EP_DATA_DIR']
MODEL_DIR = os.path.join(EP_DATA_DIR, 'models')
EP_DATA_DIR = os.environ["EP_DATA_DIR"]
if not os.path.exists(EP_DATA_DIR):
os.mkdir(EP_DATA_DIR)
MODEL_DIR = os.path.join(EP_DATA_DIR, "models")
if not os.path.exists(MODEL_DIR):
os.mkdir(MODEL_DIR)
STATIC_DIR = os.path.join(EP_DATA_DIR, 'static')
STATIC_DIR = os.path.join(EP_DATA_DIR, "static")
if not os.path.exists(STATIC_DIR):
os.mkdir(STATIC_DIR)
LOG_DIR = os.path.join(EP_DATA_DIR, 'log')
LOG_DIR = os.path.join(EP_DATA_DIR, "log")
if not os.path.exists(LOG_DIR):
os.mkdir(LOG_DIR)
PLUGIN_DIR = os.path.join(EP_DATA_DIR, 'plugins')
PLUGIN_DIR = os.path.join(EP_DATA_DIR, "plugins")
if not os.path.exists(PLUGIN_DIR):
os.mkdir(PLUGIN_DIR)
# Set this as our static root dir
STATIC_ROOT = STATIC_DIR
STATIC_URL = '/static/'
STATIC_URL = "/static/"
# Where the sources are stored...
STATICFILES_DIRS = (
BASE_DIR / 'static',
)
STATICFILES_DIRS = (BASE_DIR / "static",)
FIXTURE_DIRS = (
BASE_DIR / 'fixtures',
)
FIXTURE_DIRS = (BASE_DIR / "fixtures",)
# Logging
LOGGING = {
@ -207,8 +200,8 @@ LOGGING = {
"disable_existing_loggers": True,
"formatters": {
"simple": {
'format': '[%(asctime)s] %(levelname)s %(module)s - %(message)s',
'datefmt': '%Y-%m-%d %H:%M:%S',
"format": "[%(asctime)s] %(levelname)s %(module)s - %(message)s",
"datefmt": "%Y-%m-%d %H:%M:%S",
},
},
"handlers": {
@ -221,7 +214,7 @@ LOGGING = {
"level": "DEBUG", # Or higher
"class": "logging.FileHandler",
"filename": os.path.join(LOG_DIR, "debug.log"),
"formatter": "simple"
"formatter": "simple",
},
},
"loggers": {
@ -229,72 +222,66 @@ LOGGING = {
"epdb": {
"handlers": ["file"], # "console",
"propagate": True,
"level": os.environ.get('LOG_LEVEL', 'INFO')
"level": os.environ.get("LOG_LEVEL", "INFO"),
},
# For everything under envipath/ loaded via getlogger(__name__)
'envipath': {
'handlers': ['file', 'console'],
'propagate': True,
'level': os.environ.get('LOG_LEVEL', 'INFO')
"envipath": {
"handlers": ["file", "console"],
"propagate": True,
"level": os.environ.get("LOG_LEVEL", "INFO"),
},
# For everything under utilities/ loaded via getlogger(__name__)
'utilities': {
'handlers': ['file', 'console'],
'propagate': True,
'level': os.environ.get('LOG_LEVEL', 'INFO')
"utilities": {
"handlers": ["file", "console"],
"propagate": True,
"level": os.environ.get("LOG_LEVEL", "INFO"),
},
},
}
# Flags
ENVIFORMER_PRESENT = os.environ.get('ENVIFORMER_PRESENT', 'False') == 'True'
if ENVIFORMER_PRESENT:
print("Loading enviFormer")
device = os.environ.get('ENVIFORMER_DEVICE', 'cpu')
from enviformer import load
ENVIFORMER_INSTANCE = load(device=device)
print("loaded")
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'
FLAG_CELERY_PRESENT = os.environ.get("FLAG_CELERY_PRESENT", "False") == "True"
if not FLAG_CELERY_PRESENT:
CELERY_TASK_ALWAYS_EAGER = True
# Celery Configuration Options
CELERY_TIMEZONE = "Europe/Berlin"
# Celery Configuration
CELERY_BROKER_URL = 'redis://localhost:6379/0' # Use Redis as message broker
CELERY_RESULT_BACKEND = 'redis://localhost:6379/1'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
CELERY_BROKER_URL = "redis://localhost:6379/0" # Use Redis as message broker
CELERY_RESULT_BACKEND = "redis://localhost:6379/1"
CELERY_ACCEPT_CONTENT = ["json"]
CELERY_TASK_SERIALIZER = "json"
MODEL_BUILDING_ENABLED = os.environ.get('MODEL_BUILDING_ENABLED', 'False') == 'True'
APPLICABILITY_DOMAIN_ENABLED = os.environ.get('APPLICABILITY_DOMAIN_ENABLED', 'False') == 'True'
MODEL_BUILDING_ENABLED = os.environ.get("MODEL_BUILDING_ENABLED", "False") == "True"
APPLICABILITY_DOMAIN_ENABLED = os.environ.get("APPLICABILITY_DOMAIN_ENABLED", "False") == "True"
DEFAULT_RF_MODEL_PARAMS = {
'base_clf': RandomForestClassifier(
"base_clf": RandomForestClassifier(
n_estimators=100,
max_features='log2',
max_features="log2",
random_state=42,
criterion='entropy',
criterion="entropy",
ccp_alpha=0.0,
max_depth=3,
min_samples_leaf=1
min_samples_leaf=1,
),
'num_chains': 10,
"num_chains": 10,
}
DEFAULT_MODEL_PARAMS = {
'base_clf': DecisionTreeClassifier(
criterion='entropy',
"base_clf": DecisionTreeClassifier(
criterion="entropy",
max_depth=3,
min_samples_split=5,
# min_samples_leaf=5,
max_features='sqrt',
max_features="sqrt",
# class_weight='balanced',
random_state=42
random_state=42,
),
'num_chains': 10,
"num_chains": 10,
}
DEFAULT_MAX_NUMBER_OF_NODES = 30
@ -302,9 +289,10 @@ DEFAULT_MAX_DEPTH = 5
DEFAULT_MODEL_THRESHOLD = 0.25
# Loading Plugins
PLUGINS_ENABLED = os.environ.get('PLUGINS_ENABLED', 'False') == 'True'
PLUGINS_ENABLED = os.environ.get("PLUGINS_ENABLED", "False") == "True"
if PLUGINS_ENABLED:
from utilities.plugin import discover_plugins
CLASSIFIER_PLUGINS = discover_plugins(_cls=Classifier)
PROPERTY_PLUGINS = discover_plugins(_cls=Property)
DESCRIPTOR_PLUGINS = discover_plugins(_cls=Descriptor)
@ -313,56 +301,59 @@ else:
PROPERTY_PLUGINS = {}
DESCRIPTOR_PLUGINS = {}
SENTRY_ENABLED = os.environ.get('SENTRY_ENABLED', 'False') == 'True'
SENTRY_ENABLED = os.environ.get("SENTRY_ENABLED", "False") == "True"
if SENTRY_ENABLED:
import sentry_sdk
def before_send(event, hint):
# Check if was a handled exception by one of our loggers
if event.get('logger'):
for log_path in LOGGING.get('loggers').keys():
if event['logger'].startswith(log_path):
if event.get("logger"):
for log_path in LOGGING.get("loggers").keys():
if event["logger"].startswith(log_path):
return None
return event
sentry_sdk.init(
dsn=os.environ.get('SENTRY_DSN'),
dsn=os.environ.get("SENTRY_DSN"),
# Add data like request headers and IP for users,
# see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
send_default_pii=True,
environment=os.environ.get('SENTRY_ENVIRONMENT', 'development'),
environment=os.environ.get("SENTRY_ENVIRONMENT", "development"),
before_send=before_send,
)
# compile into digestible flags
FLAGS = {
'MODEL_BUILDING': MODEL_BUILDING_ENABLED,
'CELERY': FLAG_CELERY_PRESENT,
'PLUGINS': PLUGINS_ENABLED,
'SENTRY': SENTRY_ENABLED,
'ENVIFORMER': ENVIFORMER_PRESENT,
'APPLICABILITY_DOMAIN': APPLICABILITY_DOMAIN_ENABLED,
"MODEL_BUILDING": MODEL_BUILDING_ENABLED,
"CELERY": FLAG_CELERY_PRESENT,
"PLUGINS": PLUGINS_ENABLED,
"SENTRY": SENTRY_ENABLED,
"ENVIFORMER": ENVIFORMER_PRESENT,
"APPLICABILITY_DOMAIN": APPLICABILITY_DOMAIN_ENABLED,
}
# path of the URL are checked via "startswith"
# -> /password_reset/done is covered as well
LOGIN_EXEMPT_URLS = [
'/register',
'/api/legacy/',
'/o/token/',
'/o/userinfo/',
'/password_reset/',
'/reset/',
'/microsoft/',
"/register",
"/api/legacy/",
"/o/token/",
"/o/userinfo/",
"/password_reset/",
"/reset/",
"/microsoft/",
]
# MS AD/Entra
MS_ENTRA_ENABLED = os.environ.get('MS_ENTRA_ENABLED', 'False') == 'True'
MS_ENTRA_ENABLED = os.environ.get("MS_ENTRA_ENABLED", "False") == "True"
if MS_ENTRA_ENABLED:
MS_ENTRA_CLIENT_ID = os.environ['MS_CLIENT_ID']
MS_ENTRA_CLIENT_SECRET = os.environ['MS_CLIENT_SECRET']
MS_ENTRA_TENANT_ID = os.environ['MS_TENANT_ID']
# Add app to installed apps
INSTALLED_APPS.append("epauth")
# Set vars required by app
MS_ENTRA_CLIENT_ID = os.environ["MS_CLIENT_ID"]
MS_ENTRA_CLIENT_SECRET = os.environ["MS_CLIENT_SECRET"]
MS_ENTRA_TENANT_ID = os.environ["MS_TENANT_ID"]
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(',')
MS_ENTRA_REDIRECT_URI = os.environ["MS_REDIRECT_URI"]
MS_ENTRA_SCOPES = os.environ.get("MS_SCOPES", "").split(",")

View File

@ -14,13 +14,14 @@ Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.conf import settings as s
from django.contrib import admin
from django.urls import include, path
from .api import api_v1, api_legacy
urlpatterns = [
path("", include("epauth.urls")),
path("", include("epdb.urls")),
path("", include("migration.urls")),
path("admin/", admin.site.urls),
@ -28,3 +29,6 @@ urlpatterns = [
path("api/legacy/", api_legacy.urls),
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
]
if s.MS_ENTRA_ENABLED:
urlpatterns.append(path("", include("epauth.urls")))

View File

@ -11,6 +11,6 @@ import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'envipath.settings')
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "envipath.settings")
application = get_wsgi_application()

View File

@ -16,7 +16,9 @@ from .models import (
Node,
Edge,
Scenario,
Setting
Setting,
ExternalDatabase,
ExternalIdentifier,
)
@ -37,12 +39,13 @@ class GroupPackagePermissionAdmin(admin.ModelAdmin):
class EPAdmin(admin.ModelAdmin):
search_fields = ['name', 'description']
search_fields = ["name", "description"]
class PackageAdmin(EPAdmin):
pass
class MLRelativeReasoningAdmin(EPAdmin):
pass
@ -87,6 +90,14 @@ class SettingAdmin(EPAdmin):
pass
class ExternalDatabaseAdmin(admin.ModelAdmin):
pass
class ExternalIdentifierAdmin(admin.ModelAdmin):
pass
admin.site.register(User, UserAdmin)
admin.site.register(UserPackagePermission, UserPackagePermissionAdmin)
admin.site.register(Group, GroupAdmin)
@ -103,3 +114,5 @@ admin.site.register(Node, NodeAdmin)
admin.site.register(Edge, EdgeAdmin)
admin.site.register(Setting, SettingAdmin)
admin.site.register(Scenario, ScenarioAdmin)
admin.site.register(ExternalDatabase, ExternalDatabaseAdmin)
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)

View File

@ -21,7 +21,7 @@ class BearerTokenAuth(HttpBearer):
def _anonymous_or_real(request):
if request.user.is_authenticated and not request.user.is_anonymous:
return request.user
return get_user_model().objects.get(username='anonymous')
return get_user_model().objects.get(username="anonymous")
router = Router(auth=BearerTokenAuth())
@ -85,7 +85,9 @@ def get_package(request, package_uuid):
try:
return PackageManager.get_package_by_id(request.auth, package_id=package_uuid)
except ValueError:
return 403, {'message': f'Getting Package with id {package_uuid} failed due to insufficient rights!'}
return 403, {
"message": f"Getting Package with id {package_uuid} failed due to insufficient rights!"
}
@router.get("/compound", response={200: List[CompoundSchema], 403: Error})
@ -97,7 +99,9 @@ def get_compounds(request):
return qs
@router.get("/package/{uuid:package_uuid}/compound", response={200: List[CompoundSchema], 403: Error})
@router.get(
"/package/{uuid:package_uuid}/compound", response={200: List[CompoundSchema], 403: Error}
)
@paginate
def get_package_compounds(request, package_uuid):
try:
@ -105,4 +109,5 @@ def get_package_compounds(request, package_uuid):
return Compound.objects.filter(package=p)
except ValueError:
return 403, {
'message': f'Getting Compounds for Package with id {package_uuid} failed due to insufficient rights!'}
"message": f"Getting Compounds for Package with id {package_uuid} failed due to insufficient rights!"
}

View File

@ -2,8 +2,8 @@ from django.apps import AppConfig
class EPDBConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'epdb'
default_auto_field = "django.db.models.BigAutoField"
name = "epdb"
def ready(self):
import epdb.signals # noqa: F401

View File

@ -1,5 +0,0 @@
from django import forms
class EmailLoginForm(forms.Form):
email = forms.EmailField()

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -5,32 +5,49 @@ from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.logic import UserManager, GroupManager, PackageManager, SettingManager
from epdb.models import UserSettingPermission, MLRelativeReasoning, EnviFormer, Permission, User, ExternalDatabase
from epdb.models import (
UserSettingPermission,
MLRelativeReasoning,
EnviFormer,
Permission,
User,
ExternalDatabase,
)
class Command(BaseCommand):
def create_users(self):
# Anonymous User
if not User.objects.filter(email='anon@envipath.com').exists():
anon = UserManager.create_user("anonymous", "anon@envipath.com", "SuperSafe",
is_active=True, add_to_group=False, set_setting=False)
if not User.objects.filter(email="anon@envipath.com").exists():
anon = UserManager.create_user(
"anonymous",
"anon@envipath.com",
"SuperSafe",
is_active=True,
add_to_group=False,
set_setting=False,
)
else:
anon = User.objects.get(email='anon@envipath.com')
anon = User.objects.get(email="anon@envipath.com")
# Admin User
if not User.objects.filter(email='admin@envipath.com').exists():
admin = UserManager.create_user("admin", "admin@envipath.com", "SuperSafe",
is_active=True, add_to_group=False, set_setting=False)
if not User.objects.filter(email="admin@envipath.com").exists():
admin = UserManager.create_user(
"admin",
"admin@envipath.com",
"SuperSafe",
is_active=True,
add_to_group=False,
set_setting=False,
)
admin.is_staff = True
admin.is_superuser = True
admin.save()
else:
admin = User.objects.get(email='admin@envipath.com')
admin = User.objects.get(email="admin@envipath.com")
# System Group
g = GroupManager.create_group(admin, 'enviPath Users', 'All enviPath Users')
g = GroupManager.create_group(admin, "enviPath Users", "All enviPath Users")
g.public = True
g.save()
@ -43,14 +60,20 @@ class Command(BaseCommand):
admin.default_group = g
admin.save()
if not User.objects.filter(email='user0@envipath.com').exists():
user0 = UserManager.create_user("user0", "user0@envipath.com", "SuperSafe",
is_active=True, add_to_group=False, set_setting=False)
if not User.objects.filter(email="user0@envipath.com").exists():
user0 = UserManager.create_user(
"user0",
"user0@envipath.com",
"SuperSafe",
is_active=True,
add_to_group=False,
set_setting=False,
)
user0.is_staff = True
user0.is_superuser = True
user0.save()
else:
user0 = User.objects.get(email='user0@envipath.com')
user0 = User.objects.get(email="user0@envipath.com")
g.user_member.add(user0)
g.save()
@ -61,18 +84,20 @@ class Command(BaseCommand):
return anon, admin, g, user0
def import_package(self, data, owner):
return PackageManager.import_legacy_package(data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True)
return PackageManager.import_legacy_package(
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
)
def create_default_setting(self, owner, packages):
s = SettingManager.create_setting(
owner,
name='Global Default Setting',
description='Global Default Setting containing BBD Rules and Max 30 Nodes and Max Depth of 8',
name="Global Default Setting",
description="Global Default Setting containing BBD Rules and Max 30 Nodes and Max Depth of 8",
max_nodes=30,
max_depth=5,
rule_packages=packages,
model=None,
model_threshold=None
model_threshold=None,
)
return s
@ -84,54 +109,51 @@ class Command(BaseCommand):
"""
databases = [
{
'name': 'PubChem Compound',
'full_name': 'PubChem Compound Database',
'description': 'Chemical database of small organic molecules',
'base_url': 'https://pubchem.ncbi.nlm.nih.gov',
'url_pattern': 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'
"name": "PubChem Compound",
"full_name": "PubChem Compound Database",
"description": "Chemical database of small organic molecules",
"base_url": "https://pubchem.ncbi.nlm.nih.gov",
"url_pattern": "https://pubchem.ncbi.nlm.nih.gov/compound/{id}",
},
{
'name': 'PubChem Substance',
'full_name': 'PubChem Substance Database',
'description': 'Database of chemical substances',
'base_url': 'https://pubchem.ncbi.nlm.nih.gov',
'url_pattern': 'https://pubchem.ncbi.nlm.nih.gov/substance/{id}'
"name": "PubChem Substance",
"full_name": "PubChem Substance Database",
"description": "Database of chemical substances",
"base_url": "https://pubchem.ncbi.nlm.nih.gov",
"url_pattern": "https://pubchem.ncbi.nlm.nih.gov/substance/{id}",
},
{
'name': 'ChEBI',
'full_name': 'Chemical Entities of Biological Interest',
'description': 'Dictionary of molecular entities',
'base_url': 'https://www.ebi.ac.uk/chebi',
'url_pattern': 'https://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:{id}'
"name": "ChEBI",
"full_name": "Chemical Entities of Biological Interest",
"description": "Dictionary of molecular entities",
"base_url": "https://www.ebi.ac.uk/chebi",
"url_pattern": "https://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:{id}",
},
{
'name': 'RHEA',
'full_name': 'RHEA Reaction Database',
'description': 'Comprehensive resource of biochemical reactions',
'base_url': 'https://www.rhea-db.org',
'url_pattern': 'https://www.rhea-db.org/rhea/{id}'
"name": "RHEA",
"full_name": "RHEA Reaction Database",
"description": "Comprehensive resource of biochemical reactions",
"base_url": "https://www.rhea-db.org",
"url_pattern": "https://www.rhea-db.org/rhea/{id}",
},
{
'name': 'KEGG Reaction',
'full_name': 'KEGG Reaction Database',
'description': 'Database of biochemical reactions',
'base_url': 'https://www.genome.jp',
'url_pattern': 'https://www.genome.jp/entry/reaction+{id}'
"name": "KEGG Reaction",
"full_name": "KEGG Reaction Database",
"description": "Database of biochemical reactions",
"base_url": "https://www.genome.jp",
"url_pattern": "https://www.genome.jp/entry/{id}",
},
{
'name': 'UniProt',
'full_name': 'MetaCyc Metabolic Pathway Database',
'description': 'UniProt is a freely accessible database of protein sequence and functional information',
'base_url': 'https://www.uniprot.org',
'url_pattern': 'https://www.uniprot.org/uniprotkb?query="{id}"'
}
"name": "UniProt",
"full_name": "MetaCyc Metabolic Pathway Database",
"description": "UniProt is a freely accessible database of protein sequence and functional information",
"base_url": "https://www.uniprot.org",
"url_pattern": 'https://www.uniprot.org/uniprotkb?query="{id}"',
},
]
for db_info in databases:
ExternalDatabase.objects.get_or_create(
name=db_info['name'],
defaults=db_info
)
ExternalDatabase.objects.get_or_create(name=db_info["name"], defaults=db_info)
@transaction.atomic
def handle(self, *args, **options):
@ -142,20 +164,24 @@ class Command(BaseCommand):
# Import Packages
packages = [
'EAWAG-BBD.json',
'EAWAG-SOIL.json',
'EAWAG-SLUDGE.json',
'EAWAG-SEDIMENT.json',
"EAWAG-BBD.json",
"EAWAG-SOIL.json",
"EAWAG-SLUDGE.json",
"EAWAG-SEDIMENT.json",
]
mapping = {}
for p in packages:
print(f"Importing {p}...")
package_data = json.loads(open(s.BASE_DIR / 'fixtures' / 'packages' / '2025-07-18' / p).read())
package_data = json.loads(
open(
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
).read()
)
imported_package = self.import_package(package_data, admin)
mapping[p.replace('.json', '')] = imported_package
mapping[p.replace(".json", "")] = imported_package
setting = self.create_default_setting(admin, [mapping['EAWAG-BBD']])
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
setting.public = True
setting.save()
setting.make_global_default()
@ -171,26 +197,28 @@ class Command(BaseCommand):
usp.save()
# Create Model Package
pack = PackageManager.create_package(admin, "Public Prediction Models",
"Package to make Prediction Models publicly available")
pack = PackageManager.create_package(
admin,
"Public Prediction Models",
"Package to make Prediction Models publicly available",
)
pack.reviewed = True
pack.save()
# Create RR
ml_model = MLRelativeReasoning.create(
package=pack,
rule_packages=[mapping['EAWAG-BBD']],
data_packages=[mapping['EAWAG-BBD']],
rule_packages=[mapping["EAWAG-BBD"]],
data_packages=[mapping["EAWAG-BBD"]],
eval_packages=[],
threshold=0.5,
name='ECC - BBD - T0.5',
description='ML Relative Reasoning',
name="ECC - BBD - T0.5",
description="ML Relative Reasoning",
)
ml_model.build_dataset()
ml_model.build_model()
# ml_model.evaluate_model()
# If available, create EnviFormerModel
if s.ENVIFORMER_PRESENT:
enviFormer_model = EnviFormer.create(pack, 'EnviFormer - T0.5', 'EnviFormer Model with Threshold 0.5', 0.5)
EnviFormer.create(pack, "EnviFormer - T0.5", "EnviFormer Model with Threshold 0.5", 0.5)

View File

@ -0,0 +1,105 @@
from django.conf import settings as s
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import MLRelativeReasoning, EnviFormer, Package
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
"""
def add_arguments(self, parser):
parser.add_argument(
"model_names",
nargs="+",
type=str,
help="The names of models to train. Options are: enviformer, mlrr",
)
parser.add_argument(
"-d", "--data-packages", nargs="+", type=str, help="Packages for training"
)
parser.add_argument(
"-e", "--eval-packages", nargs="*", type=str, help="Packages for evaluation", default=[]
)
parser.add_argument(
"-r",
"--rule-packages",
nargs="*",
type=str,
help="Rule Packages mandatory for MLRR",
default=[],
)
@transaction.atomic
def handle(self, *args, **options):
# Find Public Prediction Models package to add new models to
try:
pack = Package.objects.filter(name="Public Prediction Models")[0]
bbd = Package.objects.filter(name="EAWAG-BBD")[0]
soil = Package.objects.filter(name="EAWAG-SOIL")[0]
sludge = Package.objects.filter(name="EAWAG-SLUDGE")[0]
sediment = Package.objects.filter(name="EAWAG-SEDIMENT")[0]
except IndexError:
raise IndexError(
"Can't find correct packages. They should be created with the bootstrap command"
)
def decode_packages(package_list):
"""Decode package strings into their respective packages"""
packages = []
for p in package_list:
p = p.lower()
if p == "bbd":
packages.append(bbd)
elif p == "soil":
packages.append(soil)
elif p == "sludge":
packages.append(sludge)
elif p == "sediment":
packages.append(sediment)
else:
raise ValueError(f"Unknown package {p}")
return packages
# Iteratively create models in options["model_names"]
print(f"Creating models: {options['model_names']}")
data_packages = decode_packages(options["data_packages"])
eval_packages = decode_packages(options["eval_packages"])
rule_packages = decode_packages(options["rule_packages"])
for model_name in options["model_names"]:
model_name = model_name.lower()
if model_name == "enviformer" and s.ENVIFORMER_PRESENT:
model = EnviFormer.create(
pack,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=0.5,
name="EnviFormer - T0.5",
description="EnviFormer transformer",
)
elif model_name == "mlrr":
model = MLRelativeReasoning.create(
package=pack,
rule_packages=rule_packages,
data_packages=data_packages,
eval_packages=eval_packages,
threshold=0.5,
name="ECC - BBD - T0.5",
description="ML Relative Reasoning",
)
else:
raise ValueError(f"Cannot create model of type {model_name}, unknown model type")
# Build the dataset for the model, train it, evaluate it and save it
print(f"Building dataset for {model_name}")
model.build_dataset()
print(f"Training {model_name}")
model.build_model()
print(f"Evaluating {model_name}")
model.evaluate_model()
print(f"Saving {model_name}")
model.save()

View File

@ -0,0 +1,58 @@
from csv import DictReader
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.models import Compound, CompoundStructure, Reaction, ExternalDatabase, ExternalIdentifier
class Command(BaseCommand):
STR_TO_MODEL = {
"Compound": Compound,
"CompoundStructure": CompoundStructure,
"Reaction": Reaction,
}
STR_TO_DATABASE = {
"ChEBI": ExternalDatabase.objects.get(name="ChEBI"),
"RHEA": ExternalDatabase.objects.get(name="RHEA"),
"KEGG Reaction": ExternalDatabase.objects.get(name="KEGG Reaction"),
"PubChem Compound": ExternalDatabase.objects.get(name="PubChem Compound"),
"PubChem Substance": ExternalDatabase.objects.get(name="PubChem Substance"),
}
def add_arguments(self, parser):
parser.add_argument(
"--data",
type=str,
help="Path of the ID Mapping file.",
required=True,
)
parser.add_argument(
"--replace-host",
type=str,
help="Replace https://envipath.org/ with this host, e.g. http://localhost:8000/",
)
@transaction.atomic
def handle(self, *args, **options):
with open(options["data"]) as fh:
reader = DictReader(fh)
for row in reader:
clz = self.STR_TO_MODEL[row["model"]]
url = row["url"]
if options["replace_host"]:
url = url.replace("https://envipath.org/", options["replace_host"])
instance = clz.objects.get(url=url)
db = self.STR_TO_DATABASE[row["identifier_type"]]
ExternalIdentifier.objects.create(
content_object=instance,
database=db,
identifier_value=row["identifier_value"],
url=db.url_pattern.format(id=row["identifier_value"]),
is_primary=False,
)

View File

@ -1,27 +1,29 @@
import json
from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.logic import PackageManager
from epdb.models import *
from epdb.models import User
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--data',
"--data",
type=str,
help='Path of the Package to import.',
help="Path of the Package to import.",
required=True,
)
parser.add_argument(
'--owner',
"--owner",
type=str,
help='Username of the desired Owner.',
help="Username of the desired Owner.",
required=True,
)
@transaction.atomic
def handle(self, *args, **options):
owner = User.objects.get(username=options['owner'])
package_data = json.load(open(options['data']))
owner = User.objects.get(username=options["owner"])
package_data = json.load(open(options["data"]))
PackageManager.import_legacy_package(package_data, owner)

View File

@ -1,51 +1,64 @@
from django.apps import apps
from django.core.management.base import BaseCommand
from django.db.models import F, Value
from django.db.models.functions import Replace
from django.db.models import F, Value, TextField, JSONField
from django.db.models.functions import Replace, Cast
from epdb.models import EnviPathModel
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--old',
"--old",
type=str,
help='Old Host, most likely https://envipath.org/',
help="Old Host, most likely https://envipath.org/",
required=True,
)
parser.add_argument(
'--new',
"--new",
type=str,
help='New Host, most likely http://localhost:8000/',
help="New Host, most likely http://localhost:8000/",
required=True,
)
def handle(self, *args, **options):
MODELS = [
'User',
'Group',
'Package',
'Compound',
'CompoundStructure',
'Pathway',
'Edge',
'Node',
'Reaction',
'SimpleAmbitRule',
'SimpleRDKitRule',
'ParallelRule',
'SequentialRule',
'Scenario',
'Setting',
'MLRelativeReasoning',
'RuleBasedRelativeReasoning',
'EnviFormer',
'ApplicabilityDomain',
"User",
"Group",
"Package",
"Compound",
"CompoundStructure",
"Pathway",
"Edge",
"Node",
"Reaction",
"SimpleAmbitRule",
"SimpleRDKitRule",
"ParallelRule",
"SequentialRule",
"Scenario",
"Setting",
"MLRelativeReasoning",
"RuleBasedRelativeReasoning",
"EnviFormer",
"ApplicabilityDomain",
"EnzymeLink",
]
for model in MODELS:
obj_cls = apps.get_model("epdb", model)
print(f"Localizing urls for {model}")
obj_cls.objects.update(
url=Replace(F('url'), Value(options['old']), Value(options['new']))
url=Replace(F("url"), Value(options["old"]), Value(options["new"]))
)
if issubclass(obj_cls, EnviPathModel):
obj_cls.objects.update(
kv=Cast(
Replace(
Cast(F("kv"), output_field=TextField()),
Value(options["old"]),
Value(options["new"]),
),
output_field=JSONField(),
)
)

View File

@ -3,22 +3,25 @@ from django.shortcuts import redirect
from django.urls import reverse
from urllib.parse import quote
class LoginRequiredMiddleware:
def __init__(self, get_response):
self.get_response = get_response
self.exempt_urls = [
reverse('login'),
reverse('logout'),
reverse('admin:login'),
reverse('admin:index'),
] + getattr(settings, 'LOGIN_EXEMPT_URLS', [])
reverse("login"),
reverse("logout"),
reverse("admin:login"),
reverse("admin:index"),
] + getattr(settings, "LOGIN_EXEMPT_URLS", [])
def __call__(self, request):
if not request.user.is_authenticated:
path = request.path_info
if not any(path.startswith(url) for url in self.exempt_urls):
if request.method == 'GET':
if request.get_full_path() and request.get_full_path() != '/':
return redirect(f"{settings.LOGIN_URL}?next={quote(request.get_full_path())}")
if request.method == "GET":
if request.get_full_path() and request.get_full_path() != "/":
return redirect(
f"{settings.LOGIN_URL}?next={quote(request.get_full_path())}"
)
return redirect(settings.LOGIN_URL)
return self.get_response(request)

View File

@ -0,0 +1,53 @@
# Generated by Django 5.2.1 on 2025-10-07 08:19
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('epdb', '0006_mlrelativereasoning_multigen_eval_and_more'),
]
operations = [
migrations.AlterModelOptions(
name='enviformer',
options={},
),
migrations.AddField(
model_name='enviformer',
name='app_domain',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
),
migrations.AddField(
model_name='enviformer',
name='data_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_data_packages', to='epdb.package', verbose_name='Data Packages'),
),
migrations.AddField(
model_name='enviformer',
name='eval_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_eval_packages', to='epdb.package', verbose_name='Evaluation Packages'),
),
migrations.AddField(
model_name='enviformer',
name='eval_results',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AddField(
model_name='enviformer',
name='model_status',
field=models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL'),
),
migrations.AddField(
model_name='enviformer',
name='multigen_eval',
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name='enviformer',
name='rule_packages',
field=models.ManyToManyField(related_name='%(app_label)s_%(class)s_rule_packages', to='epdb.package', verbose_name='Rule Packages'),
),
]

View File

@ -0,0 +1,64 @@
# Generated by Django 5.2.7 on 2025-10-10 06:58
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("epdb", "0007_alter_enviformer_options_enviformer_app_domain_and_more"),
]
operations = [
migrations.CreateModel(
name="EnzymeLink",
fields=[
(
"id",
models.BigAutoField(
auto_created=True, primary_key=True, serialize=False, verbose_name="ID"
),
),
(
"created",
model_utils.fields.AutoCreatedField(
default=django.utils.timezone.now, editable=False, verbose_name="created"
),
),
(
"modified",
model_utils.fields.AutoLastModifiedField(
default=django.utils.timezone.now, editable=False, verbose_name="modified"
),
),
(
"uuid",
models.UUIDField(
default=uuid.uuid4, unique=True, verbose_name="UUID of this object"
),
),
("name", models.TextField(default="no name", verbose_name="Name")),
(
"description",
models.TextField(default="no description", verbose_name="Descriptions"),
),
("url", models.TextField(null=True, unique=True, verbose_name="URL")),
("kv", models.JSONField(blank=True, default=dict, null=True)),
("ec_number", models.TextField(verbose_name="EC Number")),
("classification_level", models.IntegerField(verbose_name="Classification Level")),
("linking_method", models.TextField(verbose_name="Linking Method")),
("edge_evidence", models.ManyToManyField(to="epdb.edge")),
("reaction_evidence", models.ManyToManyField(to="epdb.reaction")),
(
"rule",
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="epdb.rule"),
),
],
options={
"abstract": False,
},
),
]

File diff suppressed because it is too large Load Diff

View File

@ -2,55 +2,57 @@ import logging
from typing import Optional
from celery import shared_task
from epdb.models import Pathway, Node, Edge, EPModel, Setting
from epdb.models import Pathway, Node, EPModel, Setting
from epdb.logic import SPathway
logger = logging.getLogger(__name__)
@shared_task(queue='background')
@shared_task(queue="background")
def mul(a, b):
return a * b
@shared_task(queue='predict')
@shared_task(queue="predict")
def predict_simple(model_pk: int, smiles: str):
mod = EPModel.objects.get(id=model_pk)
res = mod.predict(smiles)
return res
@shared_task(queue='background')
@shared_task(queue="background")
def send_registration_mail(user_pk: int):
pass
@shared_task(queue='model')
@shared_task(queue="model")
def build_model(model_pk: int):
mod = EPModel.objects.get(id=model_pk)
mod.build_dataset()
mod.build_model()
@shared_task(queue='model')
@shared_task(queue="model")
def evaluate_model(model_pk: int):
mod = EPModel.objects.get(id=model_pk)
mod.evaluate_model()
@shared_task(queue='model')
@shared_task(queue="model")
def retrain(model_pk: int):
mod = EPModel.objects.get(id=model_pk)
mod.retrain()
@shared_task(queue='predict')
def predict(pw_pk: int, pred_setting_pk: int, limit: Optional[int] = None, node_pk: Optional[int] = None) -> Pathway:
@shared_task(queue="predict")
def predict(
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)
pw.kv.update(**{'status': 'running'})
pw.kv.update(**{"status": "running"})
pw.save()
try:
@ -74,12 +76,10 @@ def predict(pw_pk: int, pred_setting_pk: int, limit: Optional[int] = None, node_
else:
raise ValueError("Neither limit nor node_pk given!")
except Exception as e:
pw.kv.update({'status': 'failed'})
pw.kv.update({"status": "failed"})
pw.save()
raise e
pw.kv.update(**{'status': 'completed'})
pw.save()
pw.kv.update(**{"status": "completed"})
pw.save()

View File

@ -2,6 +2,7 @@ from django import template
register = template.Library()
@register.filter
def classname(obj):
return obj.__class__.__name__
return obj.__class__.__name__

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,99 +1,195 @@
from django.urls import path, re_path
from django.contrib.auth import views as auth_views
from django.urls import path, re_path
from . import views as v
UUID = '[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
UUID = "[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
urlpatterns = [
# Home
re_path(r'^$', v.index, name='index'),
re_path(r"^$", v.index, name="index"),
# Login
re_path(r'^login', v.login, name='login'),
re_path(r'^logout', v.logout, name='logout'),
re_path(r'^register', v.register, name='register'),
# Built In views
path('password_reset/', auth_views.PasswordResetView.as_view(
template_name='static/password_reset_form.html'
), name='password_reset'),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view(
template_name='static/password_reset_done.html'
), name='password_reset_done'),
path('reset/<uidb64>/<token>/', auth_views.PasswordResetConfirmView.as_view(
template_name='static/password_reset_confirm.html'
), name='password_reset_confirm'),
path('reset/done/', auth_views.PasswordResetCompleteView.as_view(
template_name='static/password_reset_complete.html'
), name='password_reset_complete'),
re_path(r"^login", v.login, name="login"),
re_path(r"^logout", v.logout, name="logout"),
re_path(r"^register", v.register, name="register"),
# Built-In views
path(
"password_reset/",
auth_views.PasswordResetView.as_view(template_name="static/password_reset_form.html"),
name="password_reset",
),
path(
"password_reset/done/",
auth_views.PasswordResetDoneView.as_view(template_name="static/password_reset_done.html"),
name="password_reset_done",
),
path(
"reset/<uidb64>/<token>/",
auth_views.PasswordResetConfirmView.as_view(
template_name="static/password_reset_confirm.html"
),
name="password_reset_confirm",
),
path(
"reset/done/",
auth_views.PasswordResetCompleteView.as_view(
template_name="static/password_reset_complete.html"
),
name="password_reset_complete",
),
# Top level urls
re_path(r'^package$', v.packages, name='packages'),
re_path(r'^compound$', v.compounds, name='compounds'),
re_path(r'^rule$', v.rules, name='rules'),
re_path(r'^reaction$', v.reactions, name='reactions'),
re_path(r'^pathway$', v.pathways, name='pathways'),
re_path(r'^scenario$', v.scenarios, name='scenarios'),
re_path(r'^model$', v.models, name='model'),
re_path(r'^user$', v.users, name='users'),
re_path(r'^group$', v.groups, name='groups'),
re_path(r'^search$', v.search, name='search'),
re_path(r"^package$", v.packages, name="packages"),
re_path(r"^compound$", v.compounds, name="compounds"),
re_path(r"^rule$", v.rules, name="rules"),
re_path(r"^reaction$", v.reactions, name="reactions"),
re_path(r"^pathway$", v.pathways, name="pathways"),
re_path(r"^scenario$", v.scenarios, name="scenarios"),
re_path(r"^model$", v.models, name="model"),
re_path(r"^user$", v.users, name="users"),
re_path(r"^group$", v.groups, name="groups"),
re_path(r"^search$", v.search, name="search"),
# User Detail
re_path(rf'^user/(?P<user_uuid>{UUID})', v.user, name='user'),
re_path(rf"^user/(?P<user_uuid>{UUID})", v.user, name="user"),
# Group Detail
re_path(rf'^group/(?P<group_uuid>{UUID})$', v.group, name='group_detail'),
re_path(rf"^group/(?P<group_uuid>{UUID})$", v.group, name="group detail"),
# "in package" urls
re_path(rf'^package/(?P<package_uuid>{UUID})$', v.package, name='package_detail'),
re_path(rf"^package/(?P<package_uuid>{UUID})$", v.package, name="package detail"),
# Compound
re_path(rf'^package/(?P<package_uuid>{UUID})/compound$', v.package_compounds, name='package compound list'),
re_path(rf'^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})$', v.package_compound, name='package compound detail'),
re_path(
rf"^package/(?P<package_uuid>{UUID})/compound$",
v.package_compounds,
name="package compound list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})$",
v.package_compound,
name="package compound detail",
),
# Compound Structure
re_path(rf'^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})/structure$', v.package_compound_structures, name='package compound structure list'),
re_path(rf'^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})/structure/(?P<structure_uuid>{UUID})$', v.package_compound_structure, name='package compound structure detail'),
re_path(
rf"^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})/structure$",
v.package_compound_structures,
name="package compound structure list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})/structure/(?P<structure_uuid>{UUID})$",
v.package_compound_structure,
name="package compound structure detail",
),
# Rule
re_path(rf'^package/(?P<package_uuid>{UUID})/rule$', v.package_rules, name='package rule list'),
re_path(rf'^package/(?P<package_uuid>{UUID})/rule/(?P<rule_uuid>{UUID})$', v.package_rule, name='package rule detail'),
re_path(rf'^package/(?P<package_uuid>{UUID})/simple-ambit-rule/(?P<rule_uuid>{UUID})$', v.package_rule, name='package rule detail'),
re_path(rf'^package/(?P<package_uuid>{UUID})/simple-rdkit-rule/(?P<rule_uuid>{UUID})$', v.package_rule, name='package rule detail'),
re_path(rf'^package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})$', v.package_rule, name='package rule detail'),
re_path(rf'^package/(?P<package_uuid>{UUID})/sequential-rule/(?P<rule_uuid>{UUID})$', v.package_rule, name='package rule detail'),
re_path(rf"^package/(?P<package_uuid>{UUID})/rule$", v.package_rules, name="package rule list"),
re_path(
rf"^package/(?P<package_uuid>{UUID})/rule/(?P<rule_uuid>{UUID})$",
v.package_rule,
name="package rule detail",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/simple-ambit-rule/(?P<rule_uuid>{UUID})$",
v.package_rule,
name="package rule detail",
),
# re_path(
# rf"^package/(?P<package_uuid>{UUID})/simple-rdkit-rule/(?P<rule_uuid>{UUID})$",
# v.package_rule,
# name="package rule detail",
# ),
re_path(
rf"^package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})$",
v.package_rule,
name="package rule detail",
),
# re_path(
# rf"^package/(?P<package_uuid>{UUID})/sequential-rule/(?P<rule_uuid>{UUID})$",
# v.package_rule,
# name="package rule detail",
# ),
# EnzymeLinks
re_path(
rf"^package/(?P<package_uuid>{UUID})/rule/(?P<rule_uuid>{UUID})/enzymelink/(?P<enzymelink_uuid>{UUID})$",
v.package_rule_enzymelink,
name="package rule enzymelink detail",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/simple-ambit-rule/(?P<rule_uuid>{UUID})/enzymelink/(?P<enzymelink_uuid>{UUID})$",
v.package_rule_enzymelink,
name="package rule enzymelink detail",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})/enzymelink/(?P<enzymelink_uuid>{UUID})$",
v.package_rule_enzymelink,
name="package rule enzymelink detail",
),
# Reaction
re_path(rf'^package/(?P<package_uuid>{UUID})/reaction$', v.package_reactions, name='package reaction list'),
re_path(rf'^package/(?P<package_uuid>{UUID})/reaction/(?P<reaction_uuid>{UUID})$', v.package_reaction, name='package reaction detail'),
re_path(
rf"^package/(?P<package_uuid>{UUID})/reaction$",
v.package_reactions,
name="package reaction list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/reaction/(?P<reaction_uuid>{UUID})$",
v.package_reaction,
name="package reaction detail",
),
# # Pathway
re_path(rf'^package/(?P<package_uuid>{UUID})/pathway$', v.package_pathways, name='package pathway list'),
re_path(rf'^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})$', v.package_pathway, name='package pathway detail'),
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway$",
v.package_pathways,
name="package pathway list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})$",
v.package_pathway,
name="package pathway detail",
),
# Pathway Nodes
re_path(rf'^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node$', v.package_pathway_nodes, name='package pathway node list'),
re_path(rf'^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node/(?P<node_uuid>{UUID})$', v.package_pathway_node, name='package pathway node detail'),
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node$",
v.package_pathway_nodes,
name="package pathway node list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/node/(?P<node_uuid>{UUID})$",
v.package_pathway_node,
name="package pathway node detail",
),
# Pathway Edges
re_path(rf'^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/edge$', v.package_pathway_edges, name='package pathway edge list'),
re_path(rf'^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/edge/(?P<edge_uuid>{UUID})$', v.package_pathway_edge, name='package pathway edge detail'),
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/edge$",
v.package_pathway_edges,
name="package pathway edge list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})/edge/(?P<edge_uuid>{UUID})$",
v.package_pathway_edge,
name="package pathway edge detail",
),
# Scenario
re_path(rf'^package/(?P<package_uuid>{UUID})/scenario$', v.package_scenarios, name='package scenario list'),
re_path(rf'^package/(?P<package_uuid>{UUID})/scenario/(?P<scenario_uuid>{UUID})$', v.package_scenario, name='package scenario detail'),
re_path(
rf"^package/(?P<package_uuid>{UUID})/scenario$",
v.package_scenarios,
name="package scenario list",
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/scenario/(?P<scenario_uuid>{UUID})$",
v.package_scenario,
name="package scenario detail",
),
# Model
re_path(rf'^package/(?P<package_uuid>{UUID})/model$', v.package_models, name='package model list'),
re_path(rf'^package/(?P<package_uuid>{UUID})/model/(?P<model_uuid>{UUID})$', v.package_model,name='package model detail'),
re_path(r'^setting$', v.settings, name='settings'),
re_path(rf'^setting/(?P<setting_uuid>{UUID})', v.setting, name='setting'),
re_path(r'^indigo/info$', v.indigo, name='indigo_info'),
re_path(r'^indigo/aromatize$', v.aromatize, name='indigo_aromatize'),
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(
rf"^package/(?P<package_uuid>{UUID})/model$", v.package_models, name="package model list"
),
re_path(
rf"^package/(?P<package_uuid>{UUID})/model/(?P<model_uuid>{UUID})$",
v.package_model,
name="package model detail",
),
re_path(r"^setting$", v.settings, name="settings"),
re_path(rf"^setting/(?P<setting_uuid>{UUID})", v.setting, name="setting"),
re_path(r"^indigo/info$", v.indigo, name="indigo_info"),
re_path(r"^indigo/aromatize$", v.aromatize, name="indigo_aromatize"),
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"),
# OAuth Stuff
path("o/userinfo/", v.userinfo, name="oauth_userinfo"),
]

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@ -12,4 +12,5 @@ urlpatterns = [
re_path(rf'^migration/package/(?P<package_uuid>{UUID})/simple-rdkit-rule/(?P<rule_uuid>{UUID})$', v.migration_detail, name='migration detail'),
re_path(rf'^migration/package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})$', v.migration_detail, name='migration detail'),
re_path(rf'^migration/package/(?P<package_uuid>{UUID})/sequential-rule/(?P<rule_uuid>{UUID})$', v.migration_detail, name='migration detail'),
re_path(rf'^migration/compare$', v.compare, name='compare'),
]

View File

@ -1,80 +1,123 @@
import gzip
import json
import logging
import os.path
from datetime import datetime
from django.conf import settings as s
from django.http import HttpResponseNotAllowed
from django.shortcuts import render
from epdb.logic import PackageManager
from epdb.models import Rule
from epdb.models import Rule, SimpleAmbitRule, Package, CompoundStructure
from epdb.views import get_base_context, _anonymous_or_real
from utilities.chem import FormatConverter
from rdkit import Chem
from rdkit.Chem.MolStandardize import rdMolStandardize
logger = logging.getLogger(__name__)
def normalize_smiles(smiles):
m1 = Chem.MolFromSmiles(smiles)
if m1 is None:
print("Couldnt read smi: ", smiles)
return smiles
Chem.RemoveStereochemistry(m1)
# Normalizer takes care of charge/tautomer/resonance standardization
normalizer = rdMolStandardize.Normalizer()
return Chem.MolToSmiles(normalizer.normalize(m1), canonical=True)
def run_both_engines(SMILES, SMIRKS):
from envipy_ambit import apply
ambit_res = apply(SMIRKS, SMILES)
# ambit_res, ambit_errors = FormatConverter.sanitize_smiles([str(s) for s in ambit_res])
ambit_res = list(
set(
[
normalize_smiles(str(x))
for x in FormatConverter.sanitize_smiles([str(s) for s in ambit_res])[0]
]
)
)
products = FormatConverter.apply(SMILES, SMIRKS)
all_rdkit_prods = []
for ps in products:
for p in ps:
all_rdkit_prods.append(p)
all_rdkit_prods = list(set(all_rdkit_prods))
# all_rdkit_res, rdkit_errors = FormatConverter.sanitize_smiles(all_rdkit_prods)
all_rdkit_res = list(
set(
[
normalize_smiles(str(x))
for x in FormatConverter.sanitize_smiles(
[str(s) for s in all_rdkit_prods]
)[0]
]
)
)
# return ambit_res, ambit_errors, all_rdkit_res, rdkit_errors
return ambit_res, 0, all_rdkit_res, 0
def migration(request):
if request.method == 'GET':
if request.method == "GET":
context = get_base_context(request)
if os.path.exists(s.BASE_DIR / 'fixtures' / 'migration_status_per_rule.json') and request.GET.get(
"force") is None:
migration_status = json.load(open(s.BASE_DIR / 'fixtures' / 'migration_status_per_rule.json'))
if (
os.path.exists(s.BASE_DIR / "fixtures" / "migration_status_per_rule.json")
and request.GET.get("force") is None
):
migration_status = json.load(
open(s.BASE_DIR / "fixtures" / "migration_status_per_rule.json")
)
else:
data = json.load(gzip.open(s.BASE_DIR / 'fixtures' / 'ambit_rules.json.gz', 'rb'))
results = []
BBD = Package.objects.get(
url="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1"
)
ALL_SMILES = [
cs.smiles
for cs in CompoundStructure.objects.filter(compound__package=BBD)
]
RULES = SimpleAmbitRule.objects.filter(package=BBD)
results = list()
num_rules = len(RULES)
success = 0
error = 0
total = 0
num_keys = len(data.keys())
for i, bt_rule_name in enumerate(data.keys()):
print(f"{i + 1}/{num_keys}")
bt_rule = data[bt_rule_name]
smirks = bt_rule['smirks']
all_prods = set()
for i, r in enumerate(RULES):
logger.debug(f"\r{i + 1:03d}/{num_rules}")
res = True
for smiles in ALL_SMILES:
try:
ambit_res, _, rdkit_res, _ = run_both_engines(smiles, r.smirks)
for comp, ambit_prod in zip(bt_rule['compounds'], bt_rule['products']):
products = FormatConverter.apply(comp['smiles'], smirks)
all_rdkit_prods = []
for ps in products:
for p in ps:
all_rdkit_prods.append(p)
all_rdkit_prods = list(set(all_rdkit_prods))
ambit_smiles, ambit_errors = FormatConverter.sanitize_smiles(ambit_prod)
rdkit_smiles, rdkit_errors = FormatConverter.sanitize_smiles(all_rdkit_prods)
for x in ambit_smiles:
all_prods.add(x)
# TODO mode "intersection"
# partial_res = (len(set(ambit_smiles).intersection(set(rdkit_smiles))) > 0) or (len(ambit_smiles) == 0)
# FAILED (failures=37)
# TODO mode = "full ambit"
# partial_res = len(set(ambit_smiles).intersection(set(rdkit_smiles))) == len(ambit_smiles)
# FAILED (failures=46)
# TODO mode = "equality"
partial_res = set(ambit_smiles) == set(rdkit_smiles)
# FAILED (failures=69)
res &= partial_res
res &= set(ambit_res) == set(rdkit_res)
except Exception as e:
logger.error(e)
results.append(
{
'name': bt_rule_name,
'id': bt_rule['id'].split('/')[-1],
'url': bt_rule['id'],
'status': res,
'detail_url': s.SERVER_URL + '/migration/' + bt_rule['id'].replace('https://envipath.org/', '')
"name": r.name,
"detail_url": s.SERVER_URL
+ "/migration/"
+ r.url.replace("https://envipath.org/", "").replace(
"http://localhost:8000/", ""
),
"id": str(r.uuid),
"url": r.url,
"status": res,
}
)
@ -84,95 +127,82 @@ def migration(request):
error += 1
total += 1
results = sorted(results, key=lambda x: (x['status'], x['name']))
results = sorted(results, key=lambda x: (x["status"], x["name"]))
migration_status = {
'results': results,
'success': success,
'error': error,
'total': total
"results": results,
"success": success,
"error": error,
"total": total,
}
json.dump(migration_status, open(s.BASE_DIR / 'fixtures' / 'migration_status_per_rule.json', 'w'))
json.dump(
migration_status,
open(s.BASE_DIR / "fixtures" / "migration_status_per_rule.json", "w"),
)
for r in migration_status['results']:
r['detail_url'] = r['detail_url'].replace('http://localhost:8000', s.SERVER_URL)
for r in migration_status["results"]:
r["detail_url"] = r["detail_url"].replace(
"http://localhost:8000", s.SERVER_URL
)
context.update(**migration_status)
return render(request, 'migration.html', context)
return render(request, "migration.html", context)
def migration_detail(request, package_uuid, rule_uuid):
current_user = _anonymous_or_real(request)
if request.method == 'GET':
if request.method == "GET":
context = get_base_context(request)
p = PackageManager.get_package_by_id(current_user, package_uuid)
rule = Rule.objects.get(package=p, uuid=rule_uuid)
BBD = Package.objects.get(name="EAWAG-BBD")
STRUCTURES = CompoundStructure.objects.filter(compound__package=BBD)
rule = Rule.objects.get(package=BBD, uuid=rule_uuid)
bt_rule_name = rule.name
data = json.load(gzip.open(s.BASE_DIR / 'fixtures' / 'ambit_rules.json.gz', 'rb'))
bt_rule = data[bt_rule_name]
smirks = bt_rule['smirks']
results = []
smirks = rule.smirks
res = True
results = []
all_prods = set()
for comp, ambit_prod in zip(bt_rule['compounds'], bt_rule['products']):
# if comp['smiles'] != 'CC1=C(C(=C(C=N1)CO)C=O)O':
# continue
products = FormatConverter.apply(comp['smiles'], smirks)
all_rdkit_prods = []
for ps in products:
for p in ps:
all_rdkit_prods.append(p)
all_rdkit_prods = list(set(all_rdkit_prods))
ambit_smiles, ambit_errors = FormatConverter.sanitize_smiles(ambit_prod)
rdkit_smiles, rdkit_errors = FormatConverter.sanitize_smiles(all_rdkit_prods)
for structure in STRUCTURES:
ambit_smiles, ambit_errors, rdkit_smiles, rdkit_errors = run_both_engines(
structure.smiles, smirks
)
for x in ambit_smiles:
all_prods.add(x)
# TODO mode "intersection"
# partial_res = (len(set(ambit_smiles).intersection(set(rdkit_smiles))) > 0) or (len(ambit_smiles) == 0)
# FAILED (failures=37)
# FAILED (failures=18)
# TODO mode = "full ambit"
# partial_res = len(set(ambit_smiles).intersection(set(rdkit_smiles))) == len(ambit_smiles)
# FAILED (failures=46)
# partial_res = len(set(ambit_smiles).intersection(set(rdkit_smiles))) == len(set(ambit_smiles))
# FAILED (failures=34)
# TODO mode = "equality"
partial_res = set(ambit_smiles) == set(rdkit_smiles)
# FAILED (failures=69)
# FAILED (failures=30)
#
if len(ambit_smiles) or len(rdkit_smiles):
temp = {
'url': comp['id'],
'id': comp['id'].split('/')[-1],
'name': comp['name'],
'initial_smiles': comp['smiles'],
'ambit_smiles': sorted(list(ambit_smiles)),
'rdkit_smiles': sorted(list(rdkit_smiles)),
'status': set(ambit_smiles) == set(rdkit_smiles),
"url": structure.url,
"id": str(structure.uuid),
"name": structure.name,
"initial_smiles": structure.smiles,
"ambit_smiles": sorted(list(ambit_smiles)),
"rdkit_smiles": sorted(list(rdkit_smiles)),
"status": set(ambit_smiles) == set(rdkit_smiles),
}
if set(ambit_smiles) != set(rdkit_smiles):
detail = f"""
detail = f"""
BT: {bt_rule_name}
SMIRKS: {bt_rule['smirks']}
Compound: {comp['smiles']}
Compound URL: {comp['id']}
SMIRKS: {smirks}
Compound: {structure.smiles}
Compound URL: {structure.url}
Num ambit: {len(set(ambit_smiles))}
Num rdkit: {len(set(rdkit_smiles))}
Num Intersection A: {len(set(ambit_smiles).intersection(set(rdkit_smiles)))}
@ -185,15 +215,63 @@ def migration_detail(request, package_uuid, rule_uuid):
rdkit_errors: {rdkit_errors}
"""
temp['detail'] = '\n'.join([x.strip() for x in detail.split('\n')])
# print(detail.strip())
temp["detail"] = "\n".join([x.strip() for x in detail.split("\n")])
results.append(temp)
res &= partial_res
results = sorted(results, key=lambda x: x['status'])
context['results'] = results
context['res'] = res
context['bt_rule_name'] = bt_rule_name
return render(request, 'migration_detail.html', context)
results = sorted(results, key=lambda x: x["status"])
context["results"] = results
context["res"] = res
context["bt_rule_name"] = bt_rule_name
return render(request, "migration_detail.html", context)
def compare(request):
context = get_base_context(request)
if request.method == "GET":
context["smirks"] = (
"[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([#1,#6:7])[H:3].[#6;A:2]=O"
)
context["smiles"] = (
"C(CC(=O)N[C@@H](CS[Se-])C(=O)NCC(=O)[O-])[C@@H](C(=O)[O-])N"
)
return render(request, "compare.html", context)
elif request.method == "POST":
smiles = request.POST.get("smiles")
smirks = request.POST.get("smirks")
from envipy_ambit import apply
ambit_res = apply(smirks, smiles)
ambit_res, _ = FormatConverter.sanitize_smiles([str(x) for x in ambit_res])
products = FormatConverter.apply(smiles, smirks)
all_rdkit_prods = []
for ps in products:
for p in ps:
all_rdkit_prods.append(p)
all_rdkit_prods = list(set(all_rdkit_prods))
rdkit_res, _ = FormatConverter.sanitize_smiles(all_rdkit_prods)
context["result"] = True
context["ambit_res"] = sorted(set(ambit_res))
context["rdkit_res"] = sorted(set(rdkit_res))
context["diff"] = sorted(set(ambit_res).difference(set(rdkit_res)))
context["smirks"] = smirks
context["smiles"] = smiles
r = SimpleAmbitRule.objects.filter(smirks=smirks)
if r.exists():
context["rule"] = r.first()
return render(request, "compare.html", context)
else:
return HttpResponseNotAllowed(["GET", "POST"])

View File

@ -3,7 +3,7 @@ name = "envipy"
version = "0.1.0"
description = "Add your description here"
readme = "README.md"
requires-python = ">=3.10"
requires-python = ">=3.12"
dependencies = [
"celery>=5.5.2",
"django>=5.2.1",
@ -12,9 +12,9 @@ dependencies = [
"django-ninja>=1.4.1",
"django-oauth-toolkit>=3.0.1",
"django-polymorphic>=4.1.0",
"django-stubs>=5.2.4",
"enviformer",
"envipy-additional-information",
"envipy-ambit>=0.1.0",
"envipy-plugins",
"epam-indigo>=1.30.1",
"gunicorn>=23.0.0",
@ -30,9 +30,62 @@ dependencies = [
]
[tool.uv.sources]
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.0" }
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.2" }
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.4"}
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" }
[project.optional-dependencies]
ms-login = ["msal>=1.33.0"]
dev = [
"celery-stubs==0.1.3",
"django-stubs>=5.2.4",
"poethepoet>=0.37.0",
"pre-commit>=4.3.0",
"ruff>=0.13.3",
]
[tool.ruff]
line-length = 100
[tool.ruff.lint]
# Allow fix for all enabled rules (when `--fix`) is provided.
fixable = ["ALL"]
# Allow unused variables when underscore-prefixed.
dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$"
[tool.ruff.format]
docstring-code-format = true
# 4. Ignore `E402` (import violations) in all `__init__.py` files, and in selected subdirectories.
[tool.ruff.lint.per-file-ignores]
"__init__.py" = ["E402"]
"**/{tests,docs,tools}/*" = ["E402"]
[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"] }
# 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" }
# 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" }
bootstrap = { shell = """
echo "Bootstrapping initial data..."
echo "This will take a bit . Get yourself some coffee..."
python manage.py bootstrap
echo " Bootstrap complete"
echo ""
echo "Default admin credentials:"
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" }

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

488
static/images/uzh-logo.svg Normal file
View File

@ -0,0 +1,488 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1 Tiny//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11-tiny.dtd">
<svg version="1.1" baseProfile="tiny" id="Universität_Zürich"
xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="-0.499 -0.501 142.73 49.19" xml:space="preserve">
<path d="M6.818,32.721C6.296,32.944,6.2,32.986,6.093,33.04c-0.113,0.058-0.156,0.108-0.133,0.231
c0.005,0.029,0.026,0.091,0.047,0.139c0.017,0.038,0.019,0.062-0.006,0.071c-0.023,0.011-0.041-0.01-0.062-0.062
c-0.078-0.182-0.161-0.401-0.205-0.506c-0.035-0.082-0.138-0.295-0.191-0.418c-0.021-0.052-0.024-0.079-0.001-0.089
c0.024-0.011,0.04,0.007,0.056,0.042c0.016,0.037,0.027,0.057,0.053,0.095c0.057,0.086,0.127,0.088,0.25,0.043
c0.113-0.04,0.209-0.081,0.731-0.305l0.481-0.206c0.499-0.215,0.666-0.404,0.729-0.631c0.062-0.21,0.008-0.373-0.043-0.49
c-0.063-0.151-0.191-0.315-0.389-0.395c-0.27-0.108-0.584,0.014-0.938,0.166l-0.431,0.184c-0.521,0.225-0.619,0.266-0.726,0.319
c-0.114,0.058-0.157,0.107-0.132,0.23c0.005,0.031,0.025,0.091,0.042,0.129c0.016,0.037,0.018,0.062-0.006,0.071
c-0.024,0.011-0.041-0.011-0.062-0.06C5.083,31.425,5.001,31.204,5,31.201c-0.018-0.042-0.121-0.254-0.178-0.387
c-0.021-0.048-0.024-0.075,0-0.085c0.024-0.012,0.04,0.007,0.057,0.048c0.017,0.038,0.029,0.058,0.054,0.096
c0.057,0.085,0.127,0.088,0.25,0.043c0.112-0.04,0.209-0.082,0.73-0.306l0.368-0.157c0.382-0.164,0.803-0.3,1.175-0.117
c0.314,0.154,0.458,0.387,0.554,0.608c0.078,0.183,0.21,0.517,0.091,0.865c-0.083,0.242-0.281,0.481-0.78,0.695L6.818,32.721z
M6.676,28.899c0.283-0.069,0.368-0.139,0.378-0.217c0.008-0.066-0.004-0.138-0.017-0.195c-0.009-0.04-0.007-0.063,0.017-0.068
c0.028-0.006,0.043,0.025,0.055,0.076c0.052,0.237,0.073,0.386,0.088,0.455c0.007,0.033,0.056,0.202,0.098,0.393
c0.011,0.048,0.015,0.081-0.019,0.089c-0.022,0.005-0.034-0.017-0.042-0.053c-0.01-0.048-0.028-0.111-0.054-0.16
c-0.051-0.089-0.159-0.079-0.479-0.015l-2.169,0.442c-0.073,0.016-0.127,0.016-0.135-0.021c-0.009-0.04,0.031-0.083,0.083-0.16
c0.038-0.054,0.511-0.739,0.924-1.39c0.194-0.303,0.612-0.911,0.657-0.986l-0.004-0.018l-1.633,0.396
c-0.222,0.053-0.283,0.102-0.301,0.196c-0.01,0.061,0.009,0.146,0.021,0.198c0.01,0.044,0.002,0.061-0.021,0.064
c-0.028,0.008-0.042-0.032-0.054-0.088C4.028,27.651,4,27.474,3.984,27.397c-0.009-0.041-0.05-0.178-0.09-0.356
c-0.011-0.048-0.016-0.085,0.015-0.091c0.022-0.005,0.038,0.015,0.047,0.059c0.008,0.036,0.015,0.065,0.035,0.106
c0.051,0.097,0.135,0.108,0.34,0.067l2.313-0.467c0.08-0.018,0.115-0.01,0.124,0.022c0.009,0.041-0.021,0.094-0.054,0.143
c-0.168,0.277-0.544,0.852-0.837,1.312c-0.308,0.484-0.675,0.99-0.729,1.071l0.002,0.011L6.676,28.899z M4.871,25.556
c-0.566,0.035-0.672,0.042-0.791,0.058c-0.126,0.016-0.188,0.057-0.201,0.14C3.87,25.796,3.87,25.844,3.873,25.89
c0.002,0.036-0.004,0.06-0.034,0.062c-0.021,0.002-0.031-0.027-0.035-0.087c-0.01-0.142-0.014-0.378-0.021-0.486
c-0.006-0.093-0.031-0.312-0.04-0.455c-0.003-0.048,0.003-0.079,0.025-0.081c0.029-0.002,0.039,0.021,0.041,0.059
c0.002,0.037,0.008,0.066,0.018,0.111c0.025,0.1,0.09,0.126,0.222,0.123c0.12,0,0.225-0.006,0.792-0.042l0.657-0.042
c0.362-0.021,0.657-0.041,0.816-0.065c0.1-0.019,0.166-0.048,0.173-0.161c0.004-0.053,0.006-0.135,0.003-0.191
c-0.002-0.042,0.007-0.061,0.026-0.062c0.025-0.001,0.039,0.028,0.042,0.069c0.016,0.246,0.02,0.48,0.025,0.583
c0.005,0.086,0.031,0.319,0.041,0.47c0.003,0.048-0.006,0.075-0.033,0.076c-0.019,0.002-0.03-0.014-0.033-0.059
c-0.003-0.056-0.015-0.1-0.023-0.133c-0.02-0.074-0.084-0.093-0.193-0.097c-0.157-0.009-0.452,0.01-0.814,0.032L4.871,25.556z
M4.071,23.877c-0.21,0.073-0.256,0.134-0.302,0.267c-0.02,0.055-0.022,0.114-0.024,0.145c-0.002,0.034-0.014,0.044-0.037,0.043
c-0.029-0.002-0.031-0.043-0.026-0.099c0.013-0.198,0.038-0.41,0.045-0.541c0.006-0.093,0.006-0.273,0.019-0.46
c0.003-0.045,0.013-0.085,0.039-0.084c0.026,0.002,0.032,0.024,0.03,0.059c-0.003,0.061-0.003,0.116,0.017,0.147
c0.018,0.026,0.042,0.041,0.076,0.042c0.049,0.003,0.155-0.024,0.293-0.067l1.736-0.534l0.001-0.015
c-0.4-0.188-1.571-0.749-1.812-0.854c-0.047-0.021-0.102-0.041-0.136-0.042c-0.03-0.002-0.061,0.012-0.074,0.044
c-0.018,0.045-0.024,0.101-0.027,0.148c-0.003,0.034-0.009,0.063-0.034,0.062c-0.03-0.002-0.035-0.036-0.031-0.106
c0.012-0.188,0.033-0.343,0.036-0.391c0.004-0.063,0.004-0.24,0.011-0.353c0.003-0.048,0.013-0.078,0.039-0.076
c0.026,0.001,0.032,0.024,0.029,0.062C3.935,21.311,3.93,21.389,3.97,21.46C4,21.51,4.06,21.571,4.293,21.687
c0.342,0.168,0.537,0.281,0.983,0.513c0.53,0.274,0.926,0.476,1.106,0.569c0.21,0.11,0.27,0.137,0.266,0.188
c-0.003,0.048-0.058,0.067-0.236,0.127L4.071,23.877z M5.128,20.567c-0.556-0.12-0.657-0.143-0.776-0.16
c-0.126-0.019-0.191-0.002-0.242,0.113c-0.014,0.026-0.031,0.089-0.042,0.14c-0.009,0.041-0.021,0.062-0.046,0.055
c-0.026-0.006-0.027-0.032-0.017-0.087c0.021-0.099,0.047-0.204,0.067-0.295c0.024-0.095,0.045-0.178,0.057-0.229
c0.025-0.117,0.183-0.846,0.193-0.916c0.007-0.071,0.013-0.131,0.012-0.162c0-0.02-0.006-0.043-0.002-0.062
c0.004-0.018,0.02-0.019,0.038-0.015c0.025,0.005,0.065,0.033,0.231,0.08c0.036,0.012,0.194,0.054,0.236,0.07
c0.019,0.008,0.038,0.02,0.032,0.045c-0.005,0.024-0.024,0.028-0.058,0.021C4.787,19.16,4.724,19.15,4.676,19.16
c-0.07,0.011-0.122,0.039-0.185,0.216c-0.021,0.062-0.11,0.443-0.126,0.518c-0.004,0.019,0.005,0.027,0.031,0.032l0.925,0.199
c0.025,0.005,0.041,0.005,0.045-0.017c0.018-0.081,0.108-0.501,0.119-0.587c0.011-0.089,0.012-0.146-0.018-0.188
c-0.023-0.032-0.038-0.051-0.034-0.068c0.003-0.016,0.013-0.024,0.034-0.021c0.022,0.005,0.078,0.032,0.262,0.087
c0.072,0.02,0.216,0.062,0.242,0.067C6,19.405,6.04,19.414,6.033,19.447c-0.005,0.026-0.021,0.03-0.04,0.026
c-0.037-0.005-0.084-0.015-0.135-0.014c-0.077,0.002-0.144,0.042-0.188,0.174c-0.021,0.068-0.104,0.43-0.123,0.518
c-0.004,0.018,0.011,0.024,0.032,0.03l0.289,0.062c0.125,0.027,0.46,0.104,0.567,0.122c0.254,0.047,0.32,0,0.401-0.373
c0.021-0.095,0.054-0.249,0.03-0.353c-0.022-0.104-0.091-0.165-0.235-0.224C6.594,19.4,6.581,19.39,6.587,19.364
c0.008-0.029,0.036-0.023,0.072-0.015c0.084,0.018,0.327,0.101,0.396,0.135c0.089,0.046,0.083,0.079,0.052,0.218
c-0.06,0.274-0.109,0.474-0.146,0.63c-0.041,0.156-0.068,0.27-0.093,0.378c-0.009,0.04-0.026,0.121-0.041,0.209
C6.808,21.003,6.796,21.1,6.78,21.173c-0.01,0.049-0.026,0.071-0.052,0.065c-0.019-0.004-0.026-0.021-0.018-0.065
c0.013-0.055,0.015-0.1,0.015-0.135c0.001-0.076-0.075-0.112-0.179-0.148c-0.149-0.052-0.438-0.113-0.774-0.187L5.128,20.567z
M5.822,17.875c-0.53-0.205-0.628-0.243-0.743-0.278c-0.121-0.039-0.188-0.032-0.257,0.072c-0.018,0.025-0.044,0.083-0.063,0.132
c-0.015,0.038-0.029,0.058-0.054,0.048c-0.023-0.01-0.021-0.037-0.002-0.09c0.072-0.185,0.167-0.399,0.195-0.474
c0.046-0.118,0.138-0.388,0.18-0.496c0.085-0.22,0.196-0.446,0.398-0.584c0.104-0.072,0.336-0.143,0.569-0.052
c0.259,0.1,0.454,0.299,0.604,0.763c0.511-0.16,0.915-0.28,1.21-0.401c0.278-0.117,0.357-0.251,0.389-0.3
c0.021-0.035,0.038-0.065,0.048-0.094c0.011-0.028,0.026-0.038,0.044-0.031c0.028,0.012,0.025,0.038,0.01,0.08l-0.128,0.332
c-0.075,0.195-0.126,0.276-0.21,0.349c-0.139,0.118-0.354,0.188-0.698,0.279c-0.246,0.065-0.545,0.135-0.615,0.159
c-0.027,0.009-0.039,0.029-0.048,0.053l-0.125,0.302c-0.007,0.018-0.004,0.03,0.017,0.039l0.049,0.019
c0.325,0.125,0.601,0.232,0.754,0.271C7.45,18,7.535,18.009,7.59,17.91c0.027-0.05,0.064-0.124,0.08-0.166
c0.012-0.028,0.027-0.038,0.044-0.031c0.024,0.011,0.025,0.038,0.009,0.083c-0.079,0.202-0.188,0.457-0.209,0.51
c-0.025,0.065-0.101,0.289-0.154,0.43C7.341,18.779,7.321,18.8,7.297,18.79c-0.018-0.007-0.021-0.023-0.006-0.065
c0.021-0.054,0.029-0.098,0.035-0.132c0.013-0.074-0.057-0.123-0.153-0.176c-0.14-0.074-0.415-0.181-0.735-0.305L5.822,17.875z
M6.257,17.57c0.038,0.015,0.056,0.013,0.075-0.007c0.053-0.063,0.104-0.164,0.138-0.251c0.054-0.141,0.058-0.19,0.036-0.271
c-0.035-0.134-0.157-0.298-0.443-0.408c-0.495-0.191-0.766,0.081-0.846,0.287c-0.033,0.087-0.055,0.151-0.058,0.19
c-0.002,0.027,0.009,0.04,0.037,0.05L6.257,17.57z M8.459,16.014c-0.053,0.038-0.074,0.04-0.146-0.001
c-0.18-0.103-0.367-0.227-0.417-0.259c-0.047-0.03-0.077-0.061-0.062-0.087c0.018-0.028,0.048-0.016,0.074-0.001
c0.042,0.024,0.118,0.05,0.183,0.064c0.281,0.064,0.479-0.077,0.594-0.278c0.166-0.293,0.049-0.549-0.123-0.646
C8.4,14.716,8.223,14.68,7.858,14.83l-0.202,0.083c-0.483,0.199-0.781,0.193-1.044,0.044c-0.357-0.204-0.445-0.647-0.188-1.101
c0.12-0.212,0.23-0.33,0.302-0.401c0.022-0.024,0.042-0.036,0.064-0.022c0.042,0.023,0.129,0.091,0.383,0.233
c0.072,0.041,0.093,0.065,0.078,0.091c-0.013,0.023-0.038,0.021-0.077,0c-0.028-0.017-0.139-0.058-0.263-0.038
c-0.089,0.015-0.241,0.054-0.361,0.265C6.413,14.224,6.47,14.45,6.665,14.56c0.15,0.085,0.307,0.074,0.664-0.079l0.12-0.052
c0.521-0.227,0.823-0.238,1.133-0.062c0.188,0.107,0.372,0.306,0.393,0.627c0.012,0.222-0.062,0.421-0.16,0.593
C8.706,15.776,8.6,15.909,8.459,16.014z M8.373,12.925c-0.459-0.335-0.544-0.396-0.645-0.462c-0.107-0.067-0.182-0.075-0.244-0.021
c-0.034,0.026-0.065,0.063-0.091,0.1c-0.022,0.03-0.042,0.044-0.066,0.026c-0.018-0.013-0.006-0.042,0.029-0.09
c0.084-0.115,0.231-0.299,0.296-0.387c0.055-0.075,0.176-0.26,0.26-0.375c0.029-0.039,0.052-0.059,0.07-0.046
c0.023,0.019,0.017,0.041-0.005,0.071c-0.022,0.03-0.037,0.057-0.058,0.097c-0.044,0.094-0.011,0.154,0.092,0.237
c0.092,0.077,0.177,0.139,0.636,0.474l0.532,0.389c0.293,0.214,0.531,0.388,0.671,0.471c0.088,0.05,0.157,0.069,0.235-0.013
c0.037-0.038,0.093-0.101,0.125-0.146c0.024-0.033,0.044-0.042,0.061-0.031c0.021,0.017,0.012,0.047-0.013,0.08
c-0.146,0.199-0.294,0.384-0.354,0.466c-0.051,0.068-0.18,0.267-0.269,0.387c-0.029,0.04-0.054,0.054-0.074,0.039
c-0.016-0.011-0.017-0.03,0.011-0.066c0.033-0.045,0.053-0.086,0.067-0.118c0.031-0.068-0.007-0.125-0.087-0.197
c-0.116-0.108-0.354-0.282-0.648-0.496L8.373,12.925z M9.295,10.406l-0.372,0.415c-0.144,0.163-0.19,0.241-0.17,0.338
c0.016,0.065,0.035,0.11,0.052,0.136c0.017,0.026,0.02,0.044,0.004,0.062c-0.019,0.019-0.037,0.012-0.066-0.018
c-0.044-0.041-0.242-0.326-0.259-0.353c-0.028-0.042-0.036-0.065-0.021-0.081c0.021-0.022,0.073-0.023,0.144-0.086
c0.083-0.071,0.186-0.169,0.271-0.259l1.018-1.076c0.083-0.087,0.131-0.154,0.164-0.2c0.029-0.049,0.045-0.076,0.056-0.086
c0.018-0.019,0.037-0.006,0.078,0.032c0.057,0.054,0.237,0.238,0.308,0.306c0.024,0.028,0.036,0.049,0.021,0.065
c-0.021,0.021-0.039,0.014-0.083-0.021l-0.032-0.024c-0.076-0.062-0.221-0.061-0.456,0.177l-0.332,0.335l1.115,1.053
c0.25,0.236,0.465,0.439,0.595,0.536c0.083,0.062,0.159,0.104,0.247,0.032c0.042-0.033,0.104-0.087,0.142-0.128
c0.028-0.03,0.05-0.036,0.062-0.023c0.02,0.019,0.007,0.047-0.021,0.077c-0.169,0.18-0.339,0.343-0.41,0.419
c-0.06,0.062-0.214,0.242-0.315,0.351c-0.033,0.035-0.06,0.047-0.079,0.028c-0.014-0.013-0.012-0.031,0.02-0.063
c0.038-0.041,0.063-0.078,0.081-0.108c0.04-0.065-0.004-0.138-0.074-0.22c-0.103-0.122-0.316-0.324-0.566-0.562L9.295,10.406z
M12.134,9.947c-0.015,0.012-0.016,0.021-0.008,0.044l0.176,0.544c0.029,0.096,0.07,0.178,0.099,0.213
c0.042,0.053,0.099,0.069,0.188-0.003l0.044-0.035c0.035-0.027,0.049-0.028,0.062-0.012c0.019,0.023,0.006,0.043-0.026,0.069
c-0.094,0.074-0.227,0.166-0.316,0.237c-0.032,0.026-0.187,0.163-0.338,0.284c-0.038,0.03-0.061,0.039-0.079,0.016
c-0.015-0.019-0.007-0.033,0.017-0.052c0.026-0.021,0.065-0.057,0.086-0.078c0.121-0.125,0.097-0.269,0.039-0.461l-0.73-2.421
c-0.032-0.112-0.041-0.159-0.012-0.182c0.026-0.021,0.065-0.009,0.147,0.035c0.199,0.104,1.618,0.924,2.159,1.22
c0.32,0.174,0.438,0.148,0.512,0.113c0.051-0.026,0.097-0.059,0.132-0.086c0.023-0.02,0.041-0.027,0.057-0.008
c0.02,0.023-0.003,0.051-0.11,0.138c-0.105,0.084-0.319,0.254-0.558,0.435c-0.055,0.039-0.09,0.067-0.105,0.047
c-0.014-0.018-0.007-0.032,0.02-0.059c0.017-0.022,0.016-0.065-0.026-0.088l-0.729-0.434c-0.017-0.01-0.031-0.009-0.045,0.003
L12.134,9.947z M12.614,9.325c0.014-0.012,0.01-0.022,0-0.029l-0.839-0.513c-0.012-0.009-0.027-0.021-0.036-0.015
c-0.009,0.007-0.003,0.025,0.003,0.041l0.305,0.934c0.008,0.014,0.018,0.021,0.028,0.011L12.614,9.325z M14.557,9.301
c-0.064,0.009-0.084-0.002-0.126-0.072c-0.104-0.178-0.206-0.378-0.233-0.432c-0.025-0.05-0.037-0.091-0.011-0.105
c0.028-0.018,0.049,0.01,0.063,0.036c0.024,0.042,0.078,0.102,0.126,0.146c0.212,0.196,0.453,0.17,0.652,0.052
c0.29-0.172,0.315-0.451,0.214-0.623c-0.093-0.156-0.229-0.276-0.62-0.327l-0.217-0.028c-0.518-0.067-0.773-0.22-0.928-0.48
c-0.21-0.354-0.064-0.782,0.383-1.048c0.209-0.124,0.363-0.172,0.46-0.199c0.033-0.011,0.055-0.011,0.068,0.013
c0.024,0.042,0.066,0.143,0.216,0.394c0.042,0.071,0.047,0.104,0.021,0.118c-0.021,0.013-0.043,0-0.065-0.039
c-0.018-0.029-0.093-0.119-0.209-0.163c-0.084-0.032-0.235-0.074-0.444,0.05c-0.238,0.142-0.301,0.365-0.187,0.559
c0.088,0.147,0.229,0.217,0.614,0.261l0.13,0.014c0.562,0.062,0.833,0.202,1.015,0.508c0.109,0.187,0.171,0.45,0.028,0.738
c-0.1,0.198-0.263,0.333-0.433,0.436C14.89,9.217,14.731,9.28,14.557,9.301z M34.089,6.584L33.6,6.314
c-0.191-0.104-0.278-0.133-0.368-0.09c-0.062,0.028-0.101,0.059-0.121,0.081c-0.022,0.021-0.039,0.028-0.06,0.018
c-0.021-0.013-0.02-0.033,0.002-0.069c0.029-0.051,0.264-0.309,0.285-0.332c0.034-0.036,0.056-0.05,0.075-0.038
c0.026,0.016,0.039,0.066,0.116,0.12c0.088,0.064,0.207,0.144,0.312,0.205l1.278,0.75c0.104,0.062,0.18,0.093,0.232,0.115
c0.054,0.019,0.084,0.027,0.097,0.035c0.022,0.014,0.015,0.035-0.014,0.083c-0.04,0.067-0.181,0.285-0.229,0.369
c-0.021,0.03-0.04,0.046-0.06,0.035c-0.025-0.016-0.022-0.035,0.003-0.086l0.018-0.036c0.042-0.089,0.008-0.229-0.274-0.404
l-0.401-0.25l-0.776,1.323c-0.174,0.297-0.323,0.552-0.39,0.699c-0.043,0.097-0.064,0.179,0.024,0.248
c0.042,0.033,0.108,0.081,0.156,0.109c0.036,0.021,0.047,0.04,0.037,0.056c-0.013,0.022-0.044,0.018-0.08-0.004
C33.25,9.129,33.053,9,32.962,8.947c-0.073-0.043-0.282-0.153-0.412-0.229c-0.042-0.023-0.059-0.048-0.046-0.069
c0.01-0.017,0.028-0.019,0.067,0.004c0.048,0.029,0.091,0.045,0.124,0.055c0.072,0.025,0.133-0.034,0.197-0.122
c0.096-0.126,0.245-0.381,0.42-0.678L34.089,6.584z M35.774,8.711c0.371-0.431,0.438-0.511,0.511-0.605
c0.077-0.102,0.093-0.166,0.017-0.267c-0.018-0.024-0.062-0.069-0.104-0.104c-0.031-0.027-0.043-0.047-0.026-0.067
c0.018-0.02,0.042-0.009,0.084,0.028c0.15,0.129,0.322,0.291,0.407,0.364c0.068,0.058,0.254,0.204,0.356,0.291
c0.042,0.037,0.058,0.059,0.04,0.079c-0.018,0.02-0.039,0.011-0.067-0.014c-0.031-0.026-0.051-0.039-0.09-0.062
c-0.089-0.053-0.153-0.022-0.244,0.071c-0.084,0.087-0.152,0.166-0.521,0.598l-0.34,0.396c-0.353,0.412-0.419,0.655-0.378,0.889
c0.038,0.214,0.158,0.338,0.255,0.421c0.125,0.106,0.312,0.198,0.523,0.183c0.29-0.021,0.518-0.271,0.769-0.562l0.306-0.354
c0.37-0.432,0.438-0.511,0.511-0.605c0.077-0.103,0.093-0.167,0.017-0.268c-0.018-0.024-0.062-0.068-0.095-0.097
C37.673,9,37.661,8.979,37.678,8.96c0.017-0.02,0.042-0.009,0.08,0.025c0.146,0.124,0.316,0.286,0.319,0.289
c0.034,0.028,0.22,0.174,0.331,0.269c0.04,0.034,0.055,0.058,0.038,0.077c-0.017,0.021-0.039,0.011-0.073-0.018
c-0.031-0.027-0.05-0.039-0.089-0.062c-0.089-0.052-0.153-0.022-0.244,0.071c-0.083,0.086-0.151,0.166-0.521,0.597l-0.262,0.303
c-0.27,0.315-0.589,0.623-1.002,0.623c-0.35,0-0.581-0.144-0.766-0.302c-0.15-0.129-0.416-0.372-0.462-0.737
c-0.033-0.254,0.04-0.557,0.393-0.969L35.774,8.711z M38.61,11.381c0.443-0.355,0.523-0.422,0.613-0.503
c0.094-0.085,0.122-0.146,0.064-0.259c-0.013-0.028-0.05-0.08-0.083-0.121c-0.025-0.032-0.034-0.054-0.014-0.071
c0.021-0.016,0.043,0,0.078,0.043c0.125,0.154,0.263,0.346,0.312,0.406c0.079,0.1,0.268,0.313,0.339,0.404
c0.148,0.184,0.288,0.393,0.308,0.637c0.01,0.126-0.045,0.362-0.24,0.521c-0.216,0.173-0.486,0.242-0.962,0.141
c-0.117,0.522-0.215,0.932-0.257,1.249c-0.038,0.299,0.038,0.436,0.063,0.485c0.021,0.037,0.038,0.064,0.058,0.089
c0.019,0.022,0.02,0.042,0.005,0.054c-0.023,0.02-0.045,0.003-0.074-0.032l-0.223-0.275c-0.132-0.163-0.176-0.248-0.195-0.356
c-0.034-0.179,0.014-0.399,0.106-0.744c0.066-0.246,0.156-0.539,0.171-0.611c0.006-0.029-0.007-0.05-0.022-0.069l-0.198-0.258
c-0.012-0.016-0.023-0.02-0.042-0.005l-0.041,0.032c-0.271,0.219-0.501,0.403-0.611,0.517c-0.077,0.077-0.127,0.146-0.068,0.243
c0.029,0.048,0.075,0.117,0.103,0.151c0.02,0.023,0.021,0.042,0.006,0.055c-0.021,0.016-0.046,0.003-0.076-0.035
c-0.137-0.169-0.301-0.393-0.336-0.437c-0.044-0.055-0.201-0.23-0.294-0.348c-0.031-0.038-0.038-0.065-0.019-0.082
c0.016-0.012,0.033-0.007,0.062,0.028c0.034,0.043,0.068,0.073,0.096,0.096c0.059,0.049,0.135,0.012,0.229-0.044
c0.135-0.084,0.364-0.271,0.633-0.485L38.61,11.381z M38.656,11.91c-0.032,0.026-0.039,0.041-0.031,0.069
c0.029,0.078,0.09,0.172,0.149,0.245c0.094,0.116,0.136,0.145,0.216,0.167c0.134,0.035,0.336,0.013,0.575-0.181
c0.414-0.333,0.312-0.703,0.174-0.875c-0.059-0.072-0.104-0.123-0.136-0.145c-0.022-0.017-0.038-0.012-0.062,0.006L38.656,11.91z
M40.591,14.113c0.49-0.289,0.58-0.342,0.68-0.409c0.104-0.071,0.142-0.136,0.116-0.216c-0.012-0.041-0.033-0.084-0.056-0.123
c-0.02-0.032-0.023-0.056,0.001-0.071c0.021-0.011,0.041,0.012,0.072,0.062c0.072,0.123,0.182,0.331,0.237,0.424
c0.047,0.081,0.169,0.265,0.241,0.387c0.024,0.042,0.033,0.072,0.015,0.083c-0.025,0.016-0.043,0-0.062-0.033
c-0.019-0.032-0.038-0.056-0.065-0.091c-0.067-0.078-0.139-0.07-0.254-0.011c-0.107,0.054-0.197,0.108-0.688,0.396l-0.566,0.334
c-0.312,0.185-0.567,0.335-0.698,0.43c-0.081,0.061-0.126,0.118-0.082,0.222c0.021,0.049,0.056,0.123,0.084,0.172
c0.021,0.035,0.021,0.058,0.004,0.066c-0.022,0.014-0.047-0.007-0.068-0.042c-0.125-0.212-0.234-0.421-0.287-0.508
c-0.043-0.074-0.173-0.271-0.249-0.4c-0.024-0.042-0.027-0.07-0.006-0.083c0.017-0.009,0.033-0.002,0.057,0.036
c0.028,0.049,0.058,0.083,0.082,0.109c0.051,0.057,0.116,0.043,0.216-0.002c0.145-0.063,0.398-0.214,0.712-0.397L40.591,14.113z
M40.314,16.493c0.157-0.461,0.507-0.711,0.842-0.85c0.235-0.098,0.672-0.193,1.114,0.013c0.331,0.154,0.604,0.425,0.817,0.94
c0.089,0.214,0.128,0.348,0.174,0.515c0.036,0.139,0.052,0.262,0.084,0.37c0.012,0.039,0.001,0.06-0.021,0.068
c-0.028,0.012-0.073,0.019-0.197,0.062c-0.116,0.041-0.306,0.123-0.378,0.144c-0.053,0.019-0.084,0.022-0.097-0.008
c-0.011-0.028,0.014-0.046,0.062-0.065c0.105-0.048,0.208-0.144,0.263-0.267c0.073-0.164,0.056-0.472-0.076-0.79
c-0.125-0.3-0.282-0.466-0.483-0.561c-0.335-0.156-0.69-0.086-1.025,0.053c-0.822,0.34-0.972,1.134-0.78,1.597
c0.127,0.309,0.239,0.48,0.455,0.554c0.09,0.03,0.209,0.034,0.275,0.022c0.061-0.012,0.076-0.011,0.089,0.017
c0.01,0.023-0.012,0.042-0.039,0.053c-0.041,0.017-0.359,0.1-0.491,0.114c-0.066,0.007-0.089,0-0.137-0.05
c-0.113-0.114-0.246-0.367-0.338-0.589C40.232,17.361,40.171,16.92,40.314,16.493z M42.775,19.295
c0.555-0.125,0.657-0.146,0.771-0.181c0.123-0.035,0.175-0.077,0.174-0.204c0-0.031-0.01-0.094-0.021-0.145
c-0.009-0.04-0.007-0.064,0.019-0.069c0.026-0.006,0.039,0.018,0.052,0.073c0.021,0.099,0.042,0.205,0.062,0.296
c0.019,0.096,0.033,0.181,0.044,0.23c0.026,0.117,0.189,0.844,0.208,0.912c0.023,0.068,0.043,0.125,0.058,0.152
c0.008,0.018,0.023,0.037,0.027,0.056s-0.01,0.025-0.027,0.029c-0.025,0.005-0.073-0.003-0.244,0.023
c-0.038,0.005-0.2,0.033-0.245,0.036c-0.02,0-0.042-0.002-0.048-0.026c-0.006-0.025,0.011-0.037,0.043-0.044
c0.025-0.005,0.087-0.023,0.126-0.052c0.06-0.041,0.096-0.087,0.077-0.273c-0.006-0.063-0.085-0.448-0.103-0.521
c-0.004-0.018-0.017-0.022-0.042-0.017l-0.923,0.208c-0.026,0.006-0.04,0.013-0.035,0.034c0.019,0.081,0.112,0.5,0.14,0.583
c0.026,0.086,0.05,0.138,0.094,0.163c0.035,0.019,0.056,0.028,0.061,0.047c0.003,0.015-0.001,0.026-0.022,0.032
c-0.022,0.005-0.083,0.003-0.273,0.03c-0.074,0.014-0.223,0.035-0.248,0.041c-0.028,0.006-0.068,0.017-0.077-0.018
c-0.006-0.025,0.008-0.036,0.025-0.04c0.036-0.012,0.083-0.022,0.128-0.044c0.069-0.035,0.112-0.099,0.096-0.236
c-0.008-0.071-0.086-0.434-0.104-0.521c-0.004-0.019-0.021-0.02-0.042-0.015l-0.288,0.065c-0.124,0.028-0.461,0.1-0.566,0.127
c-0.25,0.062-0.292,0.134-0.208,0.507c0.021,0.095,0.057,0.248,0.121,0.333c0.065,0.085,0.152,0.112,0.308,0.104
c0.042-0.001,0.058,0.002,0.063,0.027c0.006,0.029-0.022,0.036-0.06,0.044c-0.084,0.02-0.338,0.045-0.416,0.043
c-0.102-0.004-0.108-0.037-0.14-0.176c-0.062-0.272-0.099-0.476-0.131-0.634c-0.027-0.158-0.05-0.272-0.074-0.382
c-0.009-0.04-0.026-0.121-0.05-0.207c-0.019-0.084-0.047-0.177-0.063-0.25c-0.012-0.047-0.006-0.075,0.02-0.081
c0.019-0.004,0.033,0.008,0.042,0.052c0.012,0.055,0.029,0.097,0.044,0.128c0.031,0.069,0.115,0.07,0.226,0.062
c0.156-0.017,0.445-0.081,0.78-0.156L42.775,19.295z M42.087,22.487c-0.292,0.018-0.387,0.07-0.41,0.146
c-0.021,0.064-0.021,0.137-0.02,0.195c0.001,0.041-0.005,0.064-0.027,0.064c-0.029,0.001-0.039-0.032-0.041-0.084
c-0.009-0.243-0.003-0.394-0.005-0.465c-0.001-0.033-0.019-0.209-0.024-0.402c-0.002-0.048,0.001-0.083,0.033-0.083
c0.022-0.001,0.031,0.021,0.032,0.059c0.002,0.049,0.008,0.116,0.024,0.167c0.034,0.097,0.143,0.107,0.468,0.101l2.214-0.044
c0.075-0.003,0.128,0.007,0.129,0.044c0.001,0.041-0.046,0.075-0.11,0.143c-0.048,0.046-0.637,0.636-1.159,1.201
c-0.245,0.263-0.767,0.787-0.823,0.852v0.02l1.677-0.098c0.229-0.012,0.298-0.048,0.332-0.139c0.021-0.057,0.018-0.146,0.016-0.199
c-0.001-0.045,0.009-0.061,0.031-0.061c0.03-0.001,0.035,0.04,0.037,0.095c0.007,0.194,0.002,0.375,0.005,0.453
c0.001,0.041,0.019,0.183,0.024,0.366c0.001,0.049,0,0.086-0.031,0.087c-0.021,0.001-0.034-0.021-0.036-0.066
c-0.001-0.038-0.002-0.066-0.015-0.111c-0.034-0.104-0.113-0.132-0.323-0.127l-2.359,0.042c-0.082,0.003-0.116-0.011-0.118-0.044
c-0.001-0.041,0.038-0.088,0.078-0.13c0.216-0.243,0.688-0.738,1.06-1.142c0.39-0.421,0.842-0.853,0.911-0.923v-0.011L42.087,22.487
z M41.585,25.938c0.035-0.057,0.056-0.064,0.137-0.054c0.203,0.03,0.424,0.076,0.481,0.09c0.055,0.012,0.094,0.027,0.09,0.058
c-0.004,0.034-0.038,0.032-0.067,0.028c-0.049-0.008-0.128-0.003-0.194,0.006c-0.286,0.042-0.417,0.246-0.45,0.476
c-0.048,0.333,0.154,0.528,0.352,0.558c0.182,0.025,0.359-0.006,0.645-0.277l0.158-0.151c0.377-0.36,0.656-0.465,0.956-0.422
c0.407,0.06,0.649,0.441,0.577,0.956c-0.035,0.24-0.095,0.392-0.135,0.483c-0.012,0.031-0.025,0.049-0.052,0.045
c-0.048-0.007-0.152-0.037-0.441-0.078c-0.082-0.012-0.109-0.027-0.105-0.057c0.004-0.026,0.027-0.034,0.072-0.027
c0.033,0.004,0.149,0.002,0.258-0.062c0.078-0.046,0.205-0.137,0.239-0.378c0.04-0.272-0.097-0.463-0.318-0.495
c-0.17-0.023-0.312,0.042-0.589,0.316l-0.093,0.093c-0.401,0.398-0.68,0.521-1.031,0.472c-0.215-0.03-0.458-0.148-0.594-0.44
c-0.092-0.202-0.097-0.414-0.067-0.61C41.442,26.249,41.492,26.087,41.585,25.938z M42.705,29.112
c0.547,0.154,0.648,0.183,0.766,0.208c0.125,0.027,0.195,0.009,0.235-0.065c0.022-0.036,0.039-0.082,0.051-0.125
c0.011-0.036,0.023-0.056,0.053-0.048c0.021,0.007,0.021,0.037,0.004,0.095c-0.039,0.138-0.113,0.36-0.144,0.466
c-0.024,0.09-0.073,0.305-0.112,0.44c-0.014,0.047-0.028,0.074-0.051,0.068c-0.028-0.009-0.029-0.032-0.02-0.068
s0.015-0.064,0.02-0.11c0.01-0.103-0.044-0.147-0.168-0.19c-0.112-0.04-0.213-0.068-0.76-0.223l-0.633-0.181
c-0.349-0.099-0.633-0.179-0.792-0.208c-0.1-0.017-0.172-0.01-0.217,0.095c-0.021,0.049-0.051,0.126-0.066,0.18
c-0.011,0.04-0.026,0.055-0.045,0.05c-0.025-0.007-0.028-0.039-0.017-0.079c0.066-0.236,0.142-0.461,0.169-0.559
c0.022-0.082,0.077-0.312,0.118-0.456c0.013-0.047,0.03-0.068,0.056-0.062c0.018,0.005,0.025,0.022,0.013,0.066
c-0.016,0.053-0.021,0.099-0.021,0.133c-0.006,0.076,0.049,0.115,0.149,0.155c0.146,0.061,0.431,0.141,0.779,0.239L42.705,29.112z
M40.502,30.431c0.048-0.046,0.069-0.048,0.146-0.016c0.188,0.081,0.389,0.186,0.441,0.211c0.051,0.026,0.083,0.054,0.072,0.08
c-0.013,0.031-0.046,0.021-0.073,0.009c-0.044-0.019-0.123-0.035-0.188-0.044c-0.287-0.033-0.467,0.128-0.56,0.342
c-0.134,0.309,0.011,0.55,0.192,0.629c0.168,0.071,0.348,0.09,0.694-0.101l0.191-0.104c0.458-0.25,0.755-0.277,1.033-0.156
c0.377,0.163,0.512,0.596,0.306,1.072c-0.097,0.224-0.192,0.353-0.256,0.432c-0.02,0.028-0.038,0.041-0.062,0.03
c-0.044-0.021-0.139-0.076-0.406-0.192c-0.075-0.032-0.099-0.055-0.086-0.082c0.01-0.022,0.035-0.024,0.077-0.007
c0.031,0.013,0.145,0.042,0.265,0.009c0.088-0.022,0.233-0.079,0.33-0.302c0.11-0.254,0.029-0.473-0.177-0.562
c-0.158-0.068-0.312-0.041-0.651,0.149l-0.113,0.064c-0.492,0.28-0.792,0.325-1.119,0.185c-0.199-0.086-0.403-0.264-0.458-0.58
c-0.036-0.22,0.017-0.425,0.095-0.606C40.282,30.694,40.373,30.55,40.502,30.431z M13.791,41.773c-0.024,0-0.056-0.002-0.114-0.02
c-0.053-0.015-0.07-0.058-0.09-0.186l-0.189-1.25c-0.009-0.048-0.034-0.077-0.064-0.079c-0.029,0.001-0.052,0.024-0.069,0.06
l-0.548,1.112l-0.546-1.098c-0.026-0.057-0.05-0.074-0.08-0.074c-0.029,0.001-0.052,0.029-0.057,0.066l-0.207,1.321
c-0.009,0.067-0.032,0.141-0.071,0.142c-0.032,0.004-0.046,0.004-0.062,0.004c-0.025,0-0.049,0.012-0.05,0.037
c0,0.032,0.033,0.04,0.055,0.04c0.068,0,0.168-0.008,0.208-0.008c0.037,0,0.132,0.008,0.221,0.008c0.029,0,0.065-0.005,0.067-0.04
c-0.003-0.027-0.026-0.036-0.05-0.037c-0.018,0-0.035-0.002-0.07-0.009c-0.035-0.009-0.05-0.017-0.051-0.044
c0-0.031,0.002-0.058,0.007-0.093l0.094-0.746c0.07,0.145,0.175,0.359,0.19,0.395c0.024,0.06,0.19,0.367,0.241,0.461
c0.034,0.061,0.054,0.105,0.099,0.111c0.04-0.004,0.051-0.031,0.099-0.124l0.432-0.866l0.109,0.832
c0.002,0.018,0.003,0.032,0.003,0.043c0,0.023-0.003,0.022-0.002,0.024c-0.015,0.006-0.033,0.015-0.035,0.037
c0.003,0.029,0.029,0.039,0.078,0.041c0.084,0.005,0.382,0.015,0.436,0.015c0.031-0.001,0.066-0.003,0.071-0.04
C13.839,41.781,13.812,41.773,13.791,41.773z M15.5,40.521c-0.234-0.237-0.593-0.235-0.854-0.235c-0.126,0-0.277,0.004-0.341,0.004
c-0.06,0-0.193-0.004-0.303-0.004c-0.028,0-0.062,0.002-0.064,0.035c0.001,0.029,0.029,0.039,0.053,0.039
c0.03,0,0.066,0.002,0.079,0.006c0.063,0.021,0.071,0.028,0.078,0.095c0.003,0.063,0.003,0.12,0.003,0.429v0.355
c0,0.188,0,0.346-0.01,0.43c-0.009,0.061-0.019,0.086-0.05,0.094c-0.018,0.004-0.04,0.006-0.069,0.006
c-0.031,0-0.05,0.02-0.05,0.039c0.001,0.028,0.029,0.038,0.058,0.038c0.043,0,0.096-0.002,0.146-0.005
c0.051,0,0.097-0.003,0.119-0.003c0.052,0,0.127,0.006,0.21,0.013c0.083,0.005,0.175,0.012,0.251,0.012
c0.391,0,0.616-0.145,0.715-0.243c0.121-0.119,0.233-0.315,0.233-0.576C15.703,40.802,15.607,40.629,15.5,40.521z M15.368,41.105
c0,0.207-0.042,0.382-0.172,0.488c-0.123,0.103-0.258,0.135-0.446,0.135c-0.162,0.001-0.24-0.044-0.26-0.073
c-0.011-0.012-0.02-0.086-0.021-0.134c-0.003-0.04-0.006-0.195-0.006-0.409v-0.255c0-0.159,0-0.326,0.003-0.396
c0.003-0.021,0-0.018,0.016-0.024c0.009-0.007,0.082-0.015,0.123-0.014c0.163,0,0.386,0.021,0.574,0.188
C15.268,40.69,15.368,40.853,15.368,41.105z M17.351,41.434c-0.028-0.001-0.041,0.025-0.042,0.055
c-0.006,0.033-0.034,0.09-0.068,0.127c-0.078,0.087-0.173,0.102-0.358,0.102c-0.272-0.001-0.635-0.222-0.636-0.691
c0-0.196,0.038-0.386,0.188-0.514c0.091-0.077,0.203-0.11,0.384-0.11c0.189,0,0.327,0.05,0.393,0.114
c0.049,0.049,0.074,0.116,0.076,0.177c0,0.023,0.004,0.058,0.039,0.06c0.036-0.003,0.043-0.038,0.045-0.065
c0.002-0.041,0.002-0.153,0.007-0.22c0.004-0.07,0.01-0.094,0.01-0.112c0.002-0.021-0.02-0.04-0.047-0.04
c-0.061-0.008-0.129-0.022-0.208-0.034c-0.096-0.012-0.176-0.021-0.308-0.021c-0.313,0-0.52,0.083-0.675,0.22
c-0.205,0.184-0.251,0.426-0.251,0.566c0,0.198,0.056,0.435,0.268,0.61c0.195,0.164,0.442,0.224,0.73,0.224
c0.137,0,0.296-0.012,0.385-0.047c0.037-0.013,0.057-0.033,0.065-0.069c0.021-0.072,0.045-0.245,0.045-0.273
C17.391,41.465,17.382,41.437,17.351,41.434z M19.029,41.434c-0.028-0.001-0.041,0.025-0.043,0.055
c-0.005,0.033-0.034,0.09-0.067,0.127c-0.079,0.087-0.173,0.102-0.358,0.102c-0.273-0.001-0.636-0.222-0.637-0.691
c0-0.196,0.038-0.386,0.188-0.514c0.092-0.077,0.203-0.11,0.384-0.11c0.19,0,0.327,0.05,0.394,0.114
c0.049,0.049,0.072,0.116,0.075,0.177c0,0.023,0.006,0.058,0.039,0.06c0.037-0.003,0.043-0.038,0.045-0.065
c0.002-0.041,0.002-0.153,0.008-0.22c0.004-0.07,0.009-0.094,0.01-0.112c0.001-0.021-0.021-0.04-0.047-0.04
c-0.062-0.008-0.129-0.022-0.208-0.034c-0.097-0.012-0.177-0.021-0.308-0.021c-0.314,0-0.521,0.083-0.676,0.22
c-0.205,0.184-0.251,0.426-0.251,0.566c0,0.198,0.056,0.435,0.269,0.61c0.194,0.164,0.441,0.224,0.729,0.224
c0.137,0,0.297-0.012,0.385-0.047c0.037-0.013,0.058-0.033,0.064-0.069c0.021-0.072,0.045-0.245,0.046-0.273
C19.069,41.465,19.06,41.437,19.029,41.434z M20.707,41.434c-0.027-0.001-0.041,0.025-0.043,0.055
c-0.006,0.033-0.034,0.09-0.066,0.127c-0.079,0.087-0.174,0.102-0.359,0.102c-0.272-0.001-0.635-0.222-0.635-0.691
c0-0.196,0.037-0.386,0.187-0.514c0.092-0.077,0.203-0.11,0.385-0.11c0.189,0,0.327,0.05,0.394,0.114
c0.049,0.049,0.074,0.116,0.076,0.177c0,0.023,0.005,0.058,0.039,0.06c0.037-0.003,0.043-0.038,0.045-0.065
c0.002-0.041,0.002-0.153,0.007-0.22c0.005-0.07,0.009-0.094,0.009-0.112c0.002-0.021-0.019-0.04-0.046-0.04
c-0.061-0.008-0.129-0.022-0.209-0.034c-0.095-0.012-0.175-0.021-0.307-0.021c-0.314,0-0.521,0.083-0.677,0.22
c-0.205,0.184-0.251,0.426-0.251,0.566c0,0.198,0.057,0.435,0.269,0.61c0.194,0.164,0.441,0.224,0.729,0.224
c0.138,0,0.297-0.012,0.386-0.047c0.036-0.013,0.057-0.033,0.064-0.069c0.022-0.072,0.045-0.245,0.046-0.273
C20.747,41.465,20.738,41.437,20.707,41.434z M28.803,41.773c-0.019,0-0.051-0.002-0.076-0.011
c-0.043-0.015-0.072-0.032-0.106-0.075c-0.049-0.061-0.379-0.581-0.459-0.703l0.34-0.465c0.064-0.089,0.104-0.141,0.142-0.148
c0.027-0.007,0.053-0.011,0.071-0.011c0.025,0,0.05-0.014,0.051-0.039c-0.002-0.031-0.032-0.036-0.056-0.036
c-0.08,0-0.166,0.005-0.205,0.005c-0.04,0-0.138-0.005-0.22-0.005c-0.029,0-0.06,0.005-0.062,0.036
c0.001,0.028,0.025,0.039,0.043,0.039c0.017,0,0.044,0,0.062,0.006c0.019,0.004,0.024,0.015,0.024,0.018
c0,0.013-0.006,0.039-0.022,0.064c-0.032,0.053-0.195,0.293-0.268,0.399c-0.079-0.132-0.161-0.266-0.248-0.418
c-0.011-0.02-0.021-0.047-0.021-0.052c0-0.002,0.002-0.009,0.017-0.013s0.036-0.006,0.046-0.006c0.021,0,0.049-0.01,0.05-0.039
c-0.002-0.031-0.033-0.036-0.06-0.036c-0.079,0-0.208,0.005-0.236,0.005c-0.101,0-0.269-0.005-0.319-0.005
c-0.024,0.001-0.055,0.001-0.06,0.034c0,0.021,0.016,0.041,0.038,0.041c0.019,0,0.051,0.004,0.083,0.014
c0.068,0.022,0.106,0.062,0.16,0.142l0.361,0.564l-0.401,0.544c-0.072,0.099-0.102,0.125-0.158,0.142
c-0.028,0.007-0.06,0.009-0.072,0.009c-0.024,0.001-0.045,0.018-0.045,0.041s0.024,0.036,0.048,0.036h0.036
c0.034,0,0.137-0.008,0.176-0.008c0.052,0,0.189,0.008,0.203,0.008h0.038c0.027,0,0.058-0.004,0.06-0.036
c-0.002-0.025-0.021-0.039-0.043-0.041c-0.015,0-0.03-0.002-0.045-0.002c-0.013,0-0.022-0.01-0.022-0.018c0-0.001,0-0.003,0-0.003
c0-0.015,0.01-0.042,0.028-0.07l0.293-0.453c0.093,0.149,0.201,0.332,0.316,0.52c0.004,0.007,0.005,0.012,0.005,0.014
c0,0.005-0.002,0.005-0.002,0.005c-0.021,0.005-0.041,0.021-0.041,0.043c0.006,0.036,0.038,0.034,0.09,0.038
c0.172,0.005,0.339,0.005,0.39,0.005h0.062c0.024,0,0.055-0.008,0.059-0.038C28.846,41.788,28.824,41.773,28.803,41.773z
M30.581,41.773c-0.018,0-0.05-0.002-0.076-0.011c-0.043-0.015-0.071-0.032-0.105-0.075c-0.049-0.061-0.379-0.581-0.46-0.703
l0.341-0.465c0.064-0.089,0.104-0.141,0.141-0.148c0.028-0.007,0.054-0.011,0.072-0.011c0.025,0,0.05-0.014,0.05-0.039
c-0.001-0.031-0.031-0.036-0.055-0.036c-0.079,0-0.165,0.005-0.205,0.005s-0.138-0.005-0.219-0.005
c-0.029,0-0.062,0.005-0.062,0.036c0.001,0.028,0.026,0.039,0.043,0.039c0.018,0,0.044,0,0.062,0.006
c0.019,0.004,0.024,0.015,0.023,0.018c0,0.013-0.006,0.039-0.021,0.064c-0.032,0.053-0.195,0.293-0.269,0.399
c-0.078-0.133-0.16-0.266-0.247-0.418c-0.012-0.02-0.021-0.047-0.02-0.052c0-0.002,0.001-0.008,0.016-0.013
c0.016-0.004,0.036-0.006,0.047-0.006c0.021,0,0.049-0.01,0.05-0.039c-0.002-0.031-0.033-0.036-0.061-0.036
c-0.079,0-0.208,0.005-0.234,0.005c-0.101,0-0.27-0.005-0.319-0.005c-0.025,0.001-0.055,0.001-0.06,0.034
c0,0.021,0.017,0.041,0.037,0.041s0.053,0.004,0.083,0.014c0.069,0.022,0.107,0.062,0.16,0.142l0.362,0.564l-0.402,0.544
c-0.071,0.099-0.101,0.125-0.157,0.142c-0.029,0.007-0.06,0.009-0.071,0.009c-0.025,0.001-0.045,0.018-0.046,0.041
c0,0.023,0.023,0.036,0.048,0.036h0.036c0.035,0,0.137-0.008,0.176-0.008c0.052,0,0.189,0.008,0.203,0.008h0.038
c0.026,0,0.058-0.004,0.06-0.036c-0.002-0.025-0.021-0.039-0.043-0.041c-0.015,0-0.031-0.002-0.045-0.002
c-0.013,0-0.022-0.008-0.023-0.018c0-0.001,0-0.003,0-0.003c0-0.015,0.01-0.042,0.028-0.07l0.292-0.453
c0.092,0.149,0.201,0.332,0.317,0.52c0.004,0.007,0.005,0.012,0.005,0.014c0,0.004-0.002,0.005-0.002,0.005
c-0.021,0.005-0.041,0.021-0.041,0.043c0.006,0.036,0.038,0.034,0.09,0.038c0.171,0.005,0.338,0.005,0.389,0.005h0.062
c0.024,0,0.055-0.008,0.058-0.038C30.624,41.788,30.603,41.773,30.581,41.773z M32.359,41.773c-0.019,0-0.05-0.002-0.076-0.011
c-0.043-0.015-0.072-0.032-0.106-0.075c-0.049-0.061-0.379-0.581-0.459-0.703l0.341-0.465c0.063-0.089,0.104-0.141,0.141-0.148
c0.028-0.007,0.053-0.011,0.072-0.011c0.024,0,0.049-0.014,0.05-0.039c-0.002-0.031-0.032-0.036-0.055-0.036
c-0.079,0-0.166,0.005-0.205,0.005c-0.04,0-0.139-0.005-0.22-0.005c-0.028,0-0.061,0.005-0.062,0.036
c0.001,0.028,0.025,0.039,0.043,0.039c0.016,0,0.044,0,0.061,0.006c0.02,0.004,0.025,0.015,0.024,0.018
c0,0.013-0.006,0.039-0.021,0.064c-0.032,0.053-0.195,0.293-0.269,0.399c-0.079-0.132-0.161-0.266-0.248-0.418
c-0.011-0.02-0.02-0.047-0.02-0.052c0-0.002,0.002-0.009,0.016-0.013c0.015-0.004,0.036-0.006,0.046-0.006
c0.021,0,0.049-0.01,0.05-0.039c-0.002-0.031-0.032-0.036-0.06-0.036c-0.079,0-0.208,0.005-0.236,0.005
c-0.099,0-0.268-0.005-0.317-0.005c-0.026,0.001-0.057,0.001-0.061,0.034c0,0.021,0.018,0.041,0.038,0.041
c0.02,0,0.052,0.004,0.083,0.014c0.069,0.022,0.107,0.062,0.159,0.142l0.363,0.564l-0.402,0.544
c-0.072,0.099-0.101,0.125-0.157,0.142c-0.029,0.007-0.059,0.009-0.072,0.009c-0.023,0.001-0.045,0.018-0.045,0.041
s0.024,0.036,0.047,0.036h0.036c0.035,0,0.138-0.008,0.177-0.008c0.052,0,0.188,0.008,0.201,0.008h0.038
c0.027,0,0.059-0.004,0.061-0.036c-0.002-0.025-0.021-0.039-0.043-0.041c-0.017,0-0.032-0.002-0.045-0.002
c-0.014,0-0.023-0.008-0.024-0.018v-0.003c0-0.015,0.01-0.042,0.028-0.07l0.293-0.453c0.092,0.149,0.201,0.332,0.315,0.52
c0.004,0.007,0.006,0.012,0.006,0.014c0,0.004-0.003,0.005-0.003,0.005c-0.021,0.005-0.041,0.021-0.041,0.043
c0.007,0.036,0.038,0.033,0.091,0.038c0.172,0.005,0.339,0.005,0.389,0.005h0.062c0.025,0,0.056-0.008,0.059-0.038
C32.401,41.788,32.38,41.773,32.359,41.773z M33.453,41.773c-0.03,0-0.078-0.002-0.104-0.007c-0.057-0.012-0.061-0.026-0.069-0.08
c-0.009-0.084-0.009-0.246-0.009-0.44v-0.356c0-0.309,0-0.364,0.004-0.429c0.007-0.069,0.015-0.083,0.063-0.094
c0.024-0.005,0.039-0.007,0.06-0.007s0.051-0.012,0.051-0.041c-0.005-0.033-0.035-0.033-0.061-0.034
c-0.081,0-0.218,0.005-0.27,0.005c-0.059,0-0.202-0.005-0.281-0.005c-0.031,0.001-0.062,0-0.066,0.034
c0,0.029,0.028,0.041,0.051,0.041c0.025,0,0.05,0.002,0.071,0.009c0.04,0.015,0.053,0.025,0.06,0.092
c0.002,0.063,0.002,0.12,0.002,0.429v0.356c0,0.194,0,0.356-0.01,0.438c-0.009,0.06-0.015,0.073-0.049,0.083
c-0.018,0.004-0.04,0.006-0.07,0.006c-0.031,0-0.051,0.02-0.051,0.039c0.001,0.029,0.031,0.038,0.058,0.038
c0.084,0,0.228-0.008,0.273-0.008c0.056,0,0.2,0.008,0.338,0.008c0.025,0,0.055-0.008,0.058-0.038
C33.504,41.791,33.482,41.773,33.453,41.773z M34.724,41.773c-0.03,0-0.078-0.002-0.104-0.007c-0.056-0.012-0.06-0.026-0.069-0.08
c-0.01-0.084-0.01-0.246-0.01-0.44v-0.356c0-0.309,0-0.364,0.005-0.429c0.008-0.069,0.015-0.083,0.064-0.094
c0.023-0.005,0.039-0.007,0.059-0.007c0.021,0,0.051-0.012,0.051-0.041c-0.006-0.033-0.034-0.033-0.061-0.034
c-0.082,0-0.218,0.005-0.27,0.005c-0.06,0-0.202-0.005-0.281-0.005c-0.032,0.001-0.062,0-0.067,0.034
c0,0.029,0.029,0.041,0.05,0.041c0.025,0,0.051,0.002,0.071,0.009c0.04,0.015,0.052,0.025,0.06,0.092
c0.003,0.063,0.003,0.12,0.003,0.429v0.356c0,0.194,0,0.356-0.011,0.438c-0.009,0.06-0.015,0.073-0.049,0.083
c-0.018,0.004-0.04,0.006-0.069,0.006c-0.031,0-0.051,0.02-0.051,0.039c0.001,0.029,0.03,0.038,0.058,0.038
c0.084,0,0.227-0.008,0.273-0.008c0.057,0,0.2,0.008,0.338,0.008c0.025,0,0.056-0.008,0.059-0.038
C34.774,41.791,34.753,41.773,34.724,41.773z M35.995,41.773c-0.03,0-0.077-0.002-0.104-0.007c-0.057-0.012-0.061-0.026-0.069-0.08
c-0.01-0.084-0.01-0.246-0.01-0.44v-0.356c0-0.309,0-0.364,0.005-0.429c0.007-0.069,0.014-0.083,0.063-0.094
c0.024-0.005,0.039-0.007,0.06-0.007s0.05-0.012,0.05-0.041c-0.005-0.033-0.035-0.033-0.06-0.034c-0.081,0-0.218,0.005-0.27,0.005
c-0.059,0-0.202-0.005-0.281-0.005c-0.031,0.001-0.062,0-0.066,0.034c0,0.029,0.028,0.041,0.05,0.041
c0.025,0,0.05,0.002,0.071,0.009c0.041,0.015,0.053,0.025,0.06,0.092c0.002,0.063,0.002,0.12,0.002,0.429v0.356
c0,0.194,0,0.356-0.009,0.438c-0.009,0.06-0.015,0.073-0.049,0.083c-0.019,0.004-0.04,0.006-0.07,0.006
c-0.031,0-0.05,0.02-0.05,0.039c0.001,0.029,0.031,0.038,0.058,0.038c0.084,0,0.228-0.008,0.274-0.008
c0.056,0,0.2,0.008,0.338,0.008c0.024,0,0.055-0.008,0.057-0.038C36.045,41.79,36.024,41.773,35.995,41.773z M23.406,31.974v-1.188
c0-0.222-0.18-0.402-0.402-0.402c-0.222,0-0.402,0.182-0.402,0.402v1.188H23.406z M25.583,31.974v-1.188
c0-0.222-0.18-0.402-0.401-0.402c-0.224,0-0.403,0.182-0.403,0.402v1.188H25.583z M24.092,8.076c-0.232,0-0.422,0.189-0.422,0.422
c0,0.233,0.189,0.422,0.422,0.422c0.233,0,0.423-0.188,0.423-0.422C24.514,8.265,24.325,8.076,24.092,8.076z M23.879,19.853h0.426
v-0.915h-0.426V19.853L23.879,19.853z M19.84,42.941c-0.232,0-0.422,0.188-0.422,0.422s0.189,0.422,0.422,0.422
c0.233,0,0.423-0.188,0.423-0.422S20.073,42.941,19.84,42.941z M19.584,19.869v-1.345c-0.003-0.315-0.158-0.533-0.39-0.635
c-0.231,0.102-0.386,0.319-0.389,0.636v1.344H19.584z M19.584,15.548V14.68c-0.003-0.315-0.158-0.534-0.39-0.637
c-0.231,0.104-0.386,0.321-0.389,0.637v0.867H19.584z M19.402,24.539v-1.127c0-0.116-0.093-0.209-0.209-0.209
s-0.209,0.093-0.209,0.209v1.127H19.402z M19.402,27.931v-1.126c0-0.116-0.093-0.211-0.209-0.211s-0.209,0.095-0.209,0.211v1.126
H19.402z M19.525,31.721v-1c0-0.188-0.151-0.339-0.338-0.339c-0.188,0-0.338,0.151-0.338,0.339v1H19.525z M19.194,3.409
c-0.233,0-0.422,0.189-0.422,0.422c0,0.233,0.188,0.422,0.422,0.422c0.232,0,0.422-0.188,0.422-0.422
C19.616,3.598,19.427,3.409,19.194,3.409z M29.375,19.869v-1.345c-0.002-0.315-0.158-0.533-0.389-0.635
c-0.231,0.102-0.387,0.319-0.389,0.636v1.344H29.375z M29.375,15.548V14.68c-0.002-0.315-0.158-0.534-0.389-0.637
c-0.231,0.104-0.387,0.321-0.389,0.637v0.867H29.375z M29.194,24.539v-1.127c0-0.116-0.094-0.209-0.209-0.209
s-0.209,0.093-0.209,0.209v1.127H29.194z M29.194,27.931v-1.126c0-0.116-0.094-0.211-0.209-0.211s-0.209,0.095-0.209,0.211v1.126
H29.194z M29.317,31.721v-1c0-0.188-0.151-0.339-0.338-0.339c-0.188,0-0.338,0.151-0.338,0.339v1H29.317z M28.985,3.409
c-0.233,0-0.422,0.189-0.422,0.422c0,0.233,0.188,0.422,0.422,0.422c0.232,0,0.422-0.188,0.422-0.422
C29.408,3.598,29.219,3.409,28.985,3.409z M60.109,21.36c-3.838,0-4.703-2.09-4.703-4.415V8.998h2.342v7.803
c0,1.532,0.505,2.613,2.523,2.613c1.802,0,2.559-0.757,2.559-2.829V8.998h2.325v7.442C65.155,19.774,63.316,21.36,60.109,21.36z
M73.155,21.162v-5.729c0-0.938-0.253-1.496-1.1-1.496c-1.172,0-2.091,1.334-2.091,2.901v4.325h-2.307v-8.956h2.181
c0,0.415-0.035,1.117-0.126,1.586l0.019,0.019c0.541-1.063,1.586-1.803,3.046-1.803c2.018,0,2.666,1.298,2.666,2.865v6.289
L73.155,21.162L73.155,21.162z M79.102,11.052c-0.793,0-1.424-0.631-1.424-1.405c0-0.757,0.631-1.389,1.424-1.389
s1.44,0.613,1.44,1.389C80.543,10.422,79.895,11.052,79.102,11.052z M77.948,21.162v-8.956h2.307v8.956H77.948z M87.211,21.162
h-2.343l-3.314-8.956h2.521l1.424,4.036c0.216,0.613,0.434,1.334,0.596,1.982h0.035c0.145-0.612,0.324-1.298,0.54-1.91l1.441-4.108
h2.451L87.211,21.162z M98.797,17.054h-5.551c-0.018,1.676,0.812,2.485,2.469,2.485c0.884,0,1.839-0.198,2.613-0.559l0.216,1.784
c-0.955,0.378-2.09,0.576-3.207,0.576c-2.848,0-4.433-1.423-4.433-4.576c0-2.739,1.514-4.758,4.198-4.758
c2.611,0,3.767,1.784,3.767,4C98.869,16.314,98.851,16.675,98.797,17.054z M95.03,13.702c-0.955,0-1.622,0.703-1.748,1.784h3.298
C96.616,14.368,96.004,13.702,95.03,13.702z M105.59,14.278c-1.657-0.343-2.485,0.739-2.485,3.226v3.658h-2.308v-8.956h2.181
c0,0.45-0.055,1.171-0.162,1.802h0.036c0.433-1.135,1.298-2.126,2.848-2L105.59,14.278z M108.743,21.342
c-0.647,0-1.298-0.071-1.874-0.161l0.055-1.893c0.559,0.145,1.242,0.271,1.929,0.271c0.883,0,1.459-0.361,1.459-0.956
c0-1.585-3.621-0.686-3.621-3.73c0-1.567,1.278-2.865,3.802-2.865c0.522,0,1.1,0.072,1.64,0.162l-0.07,1.82
c-0.505-0.146-1.101-0.234-1.658-0.234c-0.901,0-1.333,0.36-1.333,0.919c0,1.46,3.676,0.812,3.676,3.713
C112.744,20.153,111.194,21.342,108.743,21.342z M115.861,11.052c-0.793,0-1.424-0.631-1.424-1.405c0-0.757,0.631-1.389,1.424-1.389
s1.44,0.613,1.44,1.389C117.302,10.422,116.654,11.052,115.861,11.052z M114.708,21.162v-8.956h2.308v8.956H114.708z
M122.851,21.342c-1.982,0-2.613-0.721-2.613-2.811V13.99h-1.531v-1.784h1.531V9.449l2.307-0.613v3.37h2.182v1.784h-2.182v3.928
c0,1.153,0.271,1.479,1.063,1.479c0.378,0,0.793-0.055,1.117-0.145v1.856C124.147,21.252,123.481,21.342,122.851,21.342z
M131.731,21.162c0-0.521,0.019-1.045,0.091-1.514l-0.019-0.019c-0.434,1.01-1.531,1.712-2.865,1.712c-1.621,0-2.56-0.919-2.56-2.36
c0-2.145,2.126-3.279,5.172-3.279v-0.487c0-0.937-0.45-1.423-1.747-1.423c-0.812,0-1.894,0.271-2.649,0.703l-0.198-1.928
c0.901-0.324,2.056-0.56,3.208-0.56c2.884,0,3.694,1.172,3.694,3.118v3.73c0,0.721,0.018,1.567,0.054,2.307L131.731,21.162
L131.731,21.162z M128.561,10.729c-0.687,0-1.243-0.56-1.243-1.244c0-0.703,0.558-1.244,1.243-1.244
c0.685,0,1.243,0.541,1.243,1.244C129.804,10.169,129.245,10.729,128.561,10.729z M131.552,17.198c-2.433,0-2.974,0.703-2.974,1.423
c0,0.577,0.396,0.955,1.063,0.955c1.135,0,1.909-1.081,1.909-2.162L131.552,17.198L131.552,17.198z M132.2,10.729
c-0.685,0-1.243-0.56-1.243-1.244c0-0.703,0.56-1.244,1.243-1.244s1.244,0.541,1.244,1.244
C133.444,10.169,132.885,10.729,132.2,10.729z M139.55,21.342c-1.981,0-2.612-0.721-2.612-2.811V13.99h-1.531v-1.784h1.531V9.449
l2.307-0.613v3.37h2.181v1.784h-2.181v3.928c0,1.153,0.271,1.479,1.063,1.479c0.379,0,0.793-0.055,1.116-0.145v1.856
C140.847,21.252,140.181,21.342,139.55,21.342z M54.955,40.202V38.4l4.379-7.137c0.252-0.414,0.504-0.774,0.793-1.153
c-0.433,0.019-1.009,0.036-2.217,0.036h-2.811v-2.108h7.856v1.874l-4.631,7.316c-0.18,0.288-0.342,0.559-0.558,0.848
c0.306-0.036,1.135-0.036,2.631-0.036h2.631v2.162H54.955z M70.484,40.202c0-0.414,0.018-1.117,0.107-1.586l-0.018-0.018
c-0.541,1.062-1.567,1.802-3.045,1.802c-2.02,0-2.667-1.298-2.667-2.865v-6.289h2.289v5.73c0,0.937,0.252,1.496,1.117,1.496
c1.171,0,2.071-1.334,2.071-2.901v-4.325h2.308v8.956H70.484z M66.826,29.769c-0.685,0-1.243-0.56-1.243-1.244
c0-0.702,0.56-1.243,1.243-1.243c0.685,0,1.244,0.541,1.244,1.243C68.069,29.21,67.511,29.769,66.826,29.769z M70.466,29.769
c-0.685,0-1.243-0.56-1.243-1.244c0-0.702,0.559-1.243,1.243-1.243c0.686,0,1.243,0.541,1.243,1.243
C71.709,29.21,71.151,29.769,70.466,29.769z M79.926,33.318c-1.656-0.343-2.485,0.739-2.485,3.226v3.658h-2.308v-8.956h2.182
c0,0.45-0.055,1.171-0.162,1.802h0.036c0.432-1.135,1.297-2.126,2.847-2L79.926,33.318z M82.854,30.093
c-0.793,0-1.424-0.631-1.424-1.405c0-0.756,0.631-1.388,1.424-1.388s1.44,0.612,1.44,1.388
C84.295,29.462,83.646,30.093,82.854,30.093z M81.7,40.202v-8.956h2.307v8.956H81.7z M89.729,40.364
c-2.486,0-4.036-1.297-4.036-4.343c0-2.793,1.46-4.938,4.632-4.938c0.612,0,1.261,0.09,1.838,0.252l-0.233,2.001
c-0.486-0.181-1.046-0.325-1.622-0.325c-1.46,0-2.198,1.081-2.198,2.775c0,1.531,0.595,2.577,2.126,2.577
c0.612,0,1.279-0.127,1.767-0.379l0.181,1.965C91.567,40.185,90.684,40.364,89.729,40.364z M99.478,40.202v-5.729
c0-0.938-0.252-1.496-1.1-1.496c-1.172,0-2.091,1.334-2.091,2.9v4.325h-2.307V27.047h2.307v3.839c0,0.541-0.036,1.298-0.162,1.82
l0.036,0.019c0.522-1.01,1.55-1.677,2.938-1.677c2.018,0,2.667,1.299,2.667,2.865v6.289H99.478z M105.334,31.716
c-1.461,0-1.813-0.788-1.813-1.706v-2.972h1.061v2.91c0,0.537,0.17,0.877,0.822,0.877c0.599,0,0.83-0.252,0.83-0.938v-2.85h1.047
v2.788C107.279,31.118,106.552,31.716,105.334,31.716z M108.163,31.628v-0.83l1.477-2.468c0.073-0.122,0.155-0.238,0.243-0.354
c-0.12,0.007-0.277,0.014-0.721,0.014h-0.951v-0.952h2.972v0.863l-1.558,2.502c-0.055,0.089-0.107,0.164-0.177,0.259
c0.089-0.014,0.347-0.014,0.891-0.014h0.87v0.979L108.163,31.628L108.163,31.628z M114.922,31.628v-1.911h-1.68v1.911h-1.061v-4.59
h1.061V28.8h1.68v-1.762h1.062v4.59H114.922z M0,24.091C0,10.786,10.786,0,24.091,0l0,0c13.306,0,24.092,10.786,24.092,24.091l0,0
c0,13.306-10.786,24.092-24.092,24.092l0,0C10.786,48.182,0,37.396,0,24.091L0,24.091z M7.477,7.477
C3.225,11.73,0.595,17.602,0.595,24.091l0,0c0,6.489,2.629,12.361,6.882,16.614l0,0c4.253,4.252,10.125,6.881,16.613,6.881l0,0
c6.489,0,12.361-2.629,16.614-6.881l0,0c4.252-4.253,6.882-10.125,6.882-16.614l0,0c0-6.488-2.629-12.36-6.882-16.613l0,0
C36.452,3.224,30.58,0.594,24.091,0.594l0,0C17.602,0.594,11.73,3.224,7.477,7.477L7.477,7.477z M24.456,39.232h0.194v-0.34h-0.462
v0.753h0.269L24.456,39.232z M24.091,1.568c-12.436,0-22.516,10.081-22.516,22.516c0,12.437,10.081,22.518,22.516,22.518
c12.436,0,22.516-10.081,22.517-22.518C46.606,11.649,36.526,1.568,24.091,1.568z M21.598,43.19h-1.021v0.34h1.021v2.53
c-5.112-0.573-9.696-2.889-13.146-6.338c-0.495-0.495-0.967-1.015-1.414-1.555c0.091-0.549,0.565-0.967,1.141-0.967
c0.639,0,1.155,0.518,1.155,1.156v1.11h1.468v-0.007l0,0v-1.104c0-0.64,0.519-1.156,1.156-1.156c0.639,0,1.157,0.518,1.157,1.156
v1.11h1.468v-0.007l0,0v-1.104c0-0.64,0.519-1.156,1.156-1.156s1.157,0.518,1.157,1.156v1.11h1.468v-0.007l0,0v-1.104
c0-0.64,0.518-1.156,1.157-1.156c0.639,0,1.155,0.518,1.155,1.156v1.11h0.922V43.19z M25.794,46.135
c-0.562,0.044-1.13,0.065-1.703,0.065c-0.571,0-1.14-0.021-1.7-0.063l-0.418-2.606h0.481c-0.12-0.286-0.329-0.786-0.409-0.989
c-0.13-0.326-0.032-0.542,0.185-0.626c0.211-0.082,0.449,0.034,0.523,0.312l0.211,0.792h0.309v0.511l0,0v0.102h-0.729v0.341h1.066
l0.001-0.441h1.293l-0.001-0.511h0.31l0.213-0.792c0.075-0.278,0.315-0.396,0.528-0.312c0.218,0.084,0.315,0.3,0.186,0.626
c-0.035,0.089-0.093,0.235-0.154,0.396l-0.288,0.695H24.92v0.341h1.005l0.184-0.441h0.104L25.794,46.135z M23.391,38.684h1.4
c0.055,0.129,0.188,0.454,0.271,0.643c0.057,0.131,0.13,0.271,0.041,0.416c-0.106,0.171-0.245,0.146-0.342,0.139
c-0.006-0.001-0.012,0-0.018-0.001c-0.098,0.269-0.352,0.459-0.653,0.459c-0.301,0-0.555-0.19-0.652-0.459
c-0.005,0.001-0.013,0-0.018,0.001c-0.098,0.009-0.235,0.032-0.342-0.139c-0.089-0.145-0.016-0.285,0.041-0.416
C23.204,39.138,23.337,38.812,23.391,38.684z M23.18,37.23l0.447,0.641l0.458-0.869l0.458,0.869l0.448-0.641l-0.178,1.111h-1.456
L23.18,37.23z M23.715,40.376c0.105,0.293,0.385,0.504,0.715,0.504c0.356,0,0.654-0.247,0.736-0.58
c0.222,0.012,0.442,0.075,0.661,0.261c0.211,0.177,0.293,0.48,0.323,0.632h-4.116c0.029-0.15,0.112-0.455,0.323-0.632
C22.808,40.181,23.268,40.305,23.715,40.376z M25.414,41.532c-0.155,0.586-0.688,1.019-1.322,1.019
c-0.634,0-1.167-0.433-1.321-1.019H25.414z M39.729,39.723c-3.45,3.449-8.033,5.764-13.145,6.337v-2.53h0.935
c1.731,0,2.983-0.146,2.983-0.146s-1.289-0.194-2.983-0.194h-0.935v-3.721h1.117v-0.007l0,0v-1.104c0-0.64,0.519-1.156,1.157-1.156
s1.157,0.519,1.157,1.156v1.11h1.467v-0.007l0,0v-1.104c0-0.64,0.518-1.156,1.156-1.156c0.638,0,1.155,0.519,1.155,1.156v1.11h1.469
v-0.007l0,0v-1.104c0-0.64,0.518-1.156,1.156-1.156c0.64,0,1.156,0.519,1.156,1.156v1.11h1.468v-0.007l0,0v-1.104
c0-0.64,0.519-1.156,1.157-1.156c0.506,0,0.937,0.326,1.092,0.78C40.803,38.589,40.281,39.171,39.729,39.723z M41.541,37.673
c-0.257-0.541-0.808-0.916-1.445-0.916c-0.885,0-1.602,0.718-1.602,1.603v0.517h-0.579v-0.517c0-0.885-0.717-1.603-1.601-1.603
s-1.602,0.718-1.602,1.603v0.517h-0.58v-0.517c0-0.885-0.717-1.603-1.601-1.603s-1.601,0.718-1.601,1.603v0.517h-0.58v-0.517
c0-0.885-0.718-1.603-1.602-1.603s-1.601,0.718-1.601,1.603v0.517h-0.598c-0.182-1.198-1.213-2.118-2.462-2.118
s-2.279,0.92-2.462,2.118h-0.614v-0.517c0-0.885-0.718-1.603-1.602-1.603s-1.601,0.718-1.601,1.603v0.517h-0.58v-0.517
c0-0.885-0.716-1.603-1.602-1.603c-0.884,0-1.601,0.718-1.601,1.603v0.517h-0.58v-0.517c0-0.885-0.717-1.603-1.601-1.603
c-0.885,0-1.602,0.718-1.602,1.603v0.517H9.672v-0.517c0-0.885-0.717-1.603-1.601-1.603c-0.634,0-1.181,0.369-1.44,0.903
c-0.39-0.501-0.758-1.02-1.104-1.554h37.129C42.306,36.645,41.935,37.168,41.541,37.673z M43.027,35.515H5.155
c-0.11-0.183-0.217-0.366-0.322-0.553h38.517C43.243,35.148,43.137,35.333,43.027,35.515z M20.275,7.089v0.187
C20.27,7.313,20.26,7.368,20.241,7.42c-0.032,0.085-0.111,0.221-0.217,0.386c-0.04,0.064-0.11,0.174-0.188,0.33
C19.735,7.982,19.65,7.88,19.59,7.817c-0.066-0.07-0.154-0.16-0.228-0.246c-0.157-0.187-0.166-0.298-0.166-0.298l0,0
c0,0-0.005,0.11-0.163,0.297c-0.073,0.085-0.161,0.175-0.228,0.246c-0.062,0.063-0.147,0.167-0.25,0.325
c-0.08-0.161-0.151-0.272-0.192-0.341c-0.105-0.165-0.185-0.3-0.217-0.385c-0.03-0.081-0.041-0.174-0.042-0.184V7.089H20.275z
M18.109,6.749c0.017-0.277,0.092-0.805,0.362-1.164c0.204-0.278,0.407-0.443,0.557-0.54C19.096,5,19.151,4.973,19.19,4.954
c0.04,0.02,0.095,0.046,0.164,0.091c0.147,0.097,0.352,0.262,0.557,0.54c0.271,0.359,0.345,0.887,0.362,1.164H18.109z M20.354,7.988
c0.062,0.085,0.125,0.209,0.194,0.359c0.096,0.233,0.121,0.462,0.124,0.63v2.492h-0.642c0-0.548,0-1.756,0-2.491
c0.002-0.171,0.033-0.409,0.124-0.632C20.234,8.17,20.296,8.07,20.354,7.988z M19.682,8.994l-0.001,2.476h-0.974
c0-0.689-0.001-2.477-0.001-2.478c0.008-0.162,0.021-0.386,0.138-0.617c0.122-0.242,0.354-0.386,0.354-0.386h0.001
c0,0,0.219,0.144,0.342,0.386C19.664,8.618,19.675,8.828,19.682,8.994z M18.245,8.343c0.091,0.224,0.106,0.465,0.109,0.636
l0.003,2.491h-0.634V8.975c0.003-0.168,0.021-0.396,0.116-0.631c0.07-0.149,0.138-0.273,0.195-0.358
C18.106,8.091,18.175,8.193,18.245,8.343z M21.164,11.877v0.807h-3.941v-0.807H21.164z M21.164,13.022v3.141H20.21V14.75
c0.001-0.469-0.173-0.866-0.459-1.138c-0.153-0.147-0.338-0.258-0.542-0.328c-0.013-0.004-0.012-0.005-0.012-0.005
s-0.002,0-0.015,0.005c-0.204,0.07-0.39,0.181-0.543,0.328c-0.286,0.271-0.459,0.669-0.458,1.138v1.414h-0.958v-3.141L21.164,13.022
L21.164,13.022z M19.87,14.75v1.414h-1.35V14.75c0.005-0.546,0.272-0.925,0.675-1.103C19.596,13.824,19.865,14.203,19.87,14.75z
M21.164,16.503v3.995h-0.876v-1.995c0-0.468-0.176-0.875-0.47-1.155c-0.157-0.15-0.344-0.263-0.55-0.333
c-0.011-0.005-0.074-0.024-0.074-0.024s-0.062,0.02-0.067,0.022c-0.207,0.07-0.396,0.185-0.555,0.335
c-0.294,0.279-0.471,0.688-0.47,1.154v1.995h-0.88v-3.995L21.164,16.503L21.164,16.503z M19.948,18.506v1.992h-1.507v-1.995
c0-0.384,0.141-0.694,0.364-0.909c0.11-0.105,0.241-0.185,0.389-0.241h0.001c0.147,0.058,0.276,0.136,0.387,0.24
c0.226,0.215,0.364,0.525,0.365,0.91L19.948,18.506z M21.164,20.838v4.755h-3.941v-4.755H21.164z M21.164,25.933v3.103h-3.941
v-3.103H21.164z M21.164,29.375v5.247h-3.941v-5.247H21.164z M24.634,16.538v1.416l-0.542-0.542l-0.541,0.541v-1.415H24.634z
M23.566,16.13l0.396-1.694l0.163-2.835l0.098,2.833l0.396,1.696H23.566z M24.092,17.988l2.511,2.51h-5.021L24.092,17.988z
M26.603,20.838v1.921h-5.037v-1.921H26.603z M26.603,23.354v2.238h-1.575v-0.561c0-0.516-0.419-0.938-0.938-0.938
c-0.517,0-0.937,0.421-0.937,0.938v0.561h-1.589v-2.238H26.603z M23.155,25.933v1.998h1.873v-1.998h1.575v3.103h-5.037v-3.103
H23.155z M26.603,29.375v5.247h-5.037v-5.247H26.603z M30.067,7.089v0.187c-0.005,0.038-0.017,0.093-0.035,0.145
C30,7.505,29.921,7.64,29.815,7.805c-0.04,0.064-0.11,0.174-0.188,0.33c-0.101-0.153-0.186-0.256-0.246-0.318
c-0.065-0.07-0.154-0.16-0.228-0.246c-0.158-0.187-0.166-0.298-0.166-0.298l0,0c0,0-0.005,0.11-0.163,0.297
c-0.073,0.085-0.161,0.175-0.228,0.246c-0.062,0.063-0.147,0.167-0.25,0.325c-0.08-0.161-0.151-0.272-0.192-0.341
c-0.105-0.165-0.185-0.3-0.217-0.385c-0.031-0.082-0.041-0.177-0.042-0.185V7.089H30.067z M27.901,6.749
c0.017-0.277,0.092-0.805,0.362-1.164c0.204-0.278,0.407-0.443,0.557-0.54C28.888,5,28.942,4.973,28.982,4.954
c0.04,0.02,0.095,0.046,0.163,0.091c0.147,0.097,0.352,0.262,0.557,0.54c0.271,0.359,0.345,0.887,0.362,1.164H27.901z M30.146,7.988
c0.061,0.085,0.125,0.209,0.193,0.359c0.096,0.233,0.121,0.462,0.124,0.63v2.492h-0.642c0-0.548,0-1.756,0-2.491
c0.003-0.171,0.033-0.409,0.125-0.632C30.025,8.17,30.088,8.07,30.146,7.988z M29.473,8.994l-0.001,2.476H28.5
c0-0.689-0.001-2.477-0.001-2.478c0.008-0.162,0.021-0.386,0.138-0.617c0.122-0.242,0.354-0.386,0.354-0.386h0.001
c0,0,0.219,0.144,0.342,0.386C29.455,8.618,29.466,8.828,29.473,8.994z M28.036,8.343c0.091,0.224,0.106,0.465,0.109,0.636
l0.003,2.491h-0.635V8.975c0.004-0.168,0.021-0.396,0.116-0.631c0.07-0.149,0.138-0.273,0.194-0.358
C27.898,8.091,27.966,8.193,28.036,8.343z M30.956,11.877v0.807h-3.941v-0.807H30.956z M30.956,13.022v3.141h-0.954V14.75
c0-0.469-0.173-0.866-0.459-1.138c-0.153-0.147-0.339-0.258-0.543-0.328c-0.013-0.004-0.012-0.005-0.012-0.005s-0.002,0-0.015,0.005
c-0.204,0.07-0.39,0.181-0.543,0.328c-0.286,0.271-0.459,0.669-0.459,1.138v1.414h-0.958v-3.141L30.956,13.022L30.956,13.022z
M29.662,14.75v1.414h-1.351V14.75c0.005-0.546,0.272-0.925,0.675-1.103C29.388,13.824,29.657,14.203,29.662,14.75z M30.956,16.503
v3.995H30.08v-1.995c0-0.468-0.176-0.875-0.47-1.155c-0.157-0.15-0.344-0.263-0.55-0.333c-0.01-0.005-0.074-0.024-0.074-0.024
s-0.062,0.02-0.068,0.022c-0.207,0.07-0.396,0.185-0.555,0.335c-0.294,0.279-0.47,0.688-0.47,1.154v1.995h-0.88v-3.995
L30.956,16.503L30.956,16.503z M29.74,18.506v1.992h-1.506v-1.995c0-0.384,0.141-0.694,0.364-0.909
c0.11-0.105,0.241-0.185,0.388-0.241h0.001c0.147,0.058,0.277,0.136,0.388,0.24c0.225,0.215,0.364,0.525,0.365,0.91V18.506z
M30.956,20.838v4.755h-3.941v-4.755H30.956z M30.956,25.933v3.103h-3.941v-3.103H30.956z M30.956,29.375v5.247h-3.941v-5.247
H30.956z M31.562,34.622V11.469h-0.497V8.917c-0.005-0.317-0.051-0.519-0.191-0.844c-0.036-0.082-0.087-0.172-0.145-0.262V6.889
c0,0-0.001-0.725-0.3-1.27c-0.325-0.595-0.956-0.92-1.264-1.049c-0.07-0.033-0.117-0.048-0.126-0.05l-0.051-0.019l-0.063,0.019
c-0.03,0.009-0.529,0.167-1.004,0.812c-0.43,0.593-0.444,1.354-0.448,1.563c0,0.034,0.001,0.054,0.001,0.057v0.851
c-0.058,0.084-0.138,0.211-0.226,0.398c-0.132,0.324-0.141,0.624-0.146,0.804v2.465h-0.5v8.452l-1.368-1.367v-2.413l-0.464-1.558
l-0.68-5.341l0,0l-0.501,5.117l-0.466,1.782v2.234l-1.354,1.355v-8.265h-0.497V8.917c-0.005-0.317-0.051-0.519-0.192-0.844
c-0.035-0.082-0.087-0.172-0.144-0.262V6.889c0,0-0.001-0.725-0.3-1.27c-0.326-0.595-0.957-0.92-1.264-1.049
c-0.07-0.033-0.117-0.048-0.126-0.05l-0.051-0.019l-0.063,0.019c-0.031,0.009-0.529,0.167-1.005,0.812
c-0.43,0.593-0.444,1.354-0.448,1.563c0,0.034,0,0.054,0,0.057v0.851c-0.058,0.084-0.138,0.211-0.226,0.398
c-0.131,0.324-0.141,0.624-0.145,0.804v2.465h-0.5v23.151H4.646c-1.702-3.133-2.672-6.721-2.672-10.538
c0-6.106,2.475-11.635,6.477-15.639c4.003-4.002,9.531-6.479,15.639-6.479c6.108,0,11.637,2.476,15.64,6.479
c4.002,4.003,6.478,9.531,6.478,15.639c0,3.817-0.97,7.405-2.672,10.538L31.562,34.622L31.562,34.622z"/>
</svg>

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -646,8 +646,8 @@ function handleAssessmentResponse(depict_url, data) {
var reactivityCentersImgSrc = null;
if (data['assessment']['node'] !== undefined) {
functionalGroupsImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "'>";
reactivityCentersImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "'>"
functionalGroupsImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "&highlight=true'>";
reactivityCentersImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "&highlightReactivity=true'>"
} else {
functionalGroupsImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">";
reactivityCentersImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">"
@ -751,7 +751,7 @@ function handleAssessmentResponse(depict_url, data) {
var predProb = "<a class='list-group-item'>Predicted probability: " + transObj['probability'].toFixed(2) + "</a>";
var timesTriggered = "<a class='list-group-item'>This rule has triggered " + transObj['times_triggered'] + " times in the training set</a>";
var reliability = "<a class='list-group-item'>Reliability: " + transObj['reliability'].toFixed(2) + " (" + (transObj['reliability'] > data['ad_params']['reliability_threshold'] ? "&gt" : "&lt") + " Reliability Threshold of " + data['ad_params']['reliability_threshold'] + ") </a>";
var localCompatibility = "<a class='list-group-item'>Local Compatibility: " + transObj['local_compatibility'].toFixed(2) + " (" + (transObj['local_compatibility'] > data['ad_params']['local_compatibilty_threshold'] ? "&gt" : "&lt") + " Local Compatibility Threshold of " + data['ad_params']['local_compatibilty_threshold'] + ")</a>";
var localCompatibility = "<a class='list-group-item'>Local Compatibility: " + transObj['local_compatibility'].toFixed(2) + " (" + (transObj['local_compatibility'] > data['ad_params']['local_compatibility_threshold'] ? "&gt" : "&lt") + " Local Compatibility Threshold of " + data['ad_params']['local_compatibility_threshold'] + ")</a>";
var transImg = "<img width='100%' src='" + transObj['rule']['url'] + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "'>";
@ -784,4 +784,4 @@ function handleAssessmentResponse(depict_url, data) {
$("#appDomainAssessmentResultTable").append(res);
}
}

View File

@ -444,6 +444,13 @@ function serializeSVG(svgElement) {
line.setAttribute("fill", style.fill);
});
svgElement.querySelectorAll("line.link_no_arrow").forEach(line => {
const style = getComputedStyle(line);
line.setAttribute("stroke", style.stroke);
line.setAttribute("stroke-width", style.strokeWidth);
line.setAttribute("fill", style.fill);
});
const serializer = new XMLSerializer();
let svgString = serializer.serializeToString(svgElement);
@ -455,7 +462,26 @@ function serializeSVG(svgElement) {
return svgString;
}
function shrinkSVG(svgSelector) {
const svg = d3.select(svgSelector);
const node = svg.node();
// Compute bounding box of everything inside the SVG
const bbox = node.getBBox();
const padding = 10;
svg.attr("viewBox",
`${bbox.x - padding} ${bbox.y - padding} ${bbox.width + 2 * padding} ${bbox.height + 2 * padding}`
)
.attr("width", bbox.width + 2 * padding)
.attr("height", bbox.height + 2 * padding);
return bbox;
}
function downloadSVG(svgElement, filename = 'chart.svg') {
shrinkSVG("#" + svgElement.id);
const svgString = serializeSVG(svgElement);
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(blob);

View File

@ -3,6 +3,10 @@
<a role="button" data-toggle="modal" data-target="#edit_compound_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Compound</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#add_structure_modal">
<i class="glyphicon glyphicon-plus"></i> Add Structure</a>
@ -11,6 +15,10 @@
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#generic_set_external_reference_modal">
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a>
</li>
{% endif %}
<li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
@ -21,4 +29,4 @@
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
</li>
{% endif %}
{% endif %}

View File

@ -3,12 +3,20 @@
<a role="button" data-toggle="modal" data-target="#edit_compound_structure_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Compound Structure</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#generic_set_external_reference_modal">
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Compound Structure</a>
</li>
{% endif %}
{% endif %}

View File

@ -1,4 +1,8 @@
{% if meta.can_edit %}
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>

View File

@ -3,6 +3,10 @@
<a role="button" data-toggle="modal" data-target="#edit_node_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Node</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>

View File

@ -31,6 +31,10 @@
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal">#}
{# <i class="glyphicon glyphicon-plus"></i> Calculate Compound Properties</a>#}
@ -48,4 +52,4 @@
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Pathway</a>
</li>
{% endif %}
{% endif %}

View File

@ -3,10 +3,18 @@
<a role="button" data-toggle="modal" data-target="#edit_reaction_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Reaction</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#generic_set_external_reference_modal">
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a>
</li>
{% endif %}
<li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
@ -17,4 +25,4 @@
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
</li>
{% endif %}
{% endif %}

View File

@ -3,6 +3,10 @@
<a role="button" data-toggle="modal" data-target="#edit_rule_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Rule</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
@ -17,4 +21,4 @@
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Rule</a>
</li>
{% endif %}
{% endif %}

55
templates/compare.html Normal file
View File

@ -0,0 +1,55 @@
{% extends "framework.html" %}
{% block content %}
<div>
<form action="" method="post">
{% csrf_token %}
<input type="text" class="form-control" id="smiles" name="smiles" placeholder="SMILES"
value="{{ smiles }}"/>
<input type="text" class="form-control" id="smiles" name="smirks" placeholder="SMIRKS"
value="{{ smirks }}"/>
<button type="submit" class="btn btn-primary">Test</button>
</form>
</div>
{% if result %}
{{ smiles }}<p></p>
<img width='400' src='{% url 'depict' %}?smiles={{ smiles|urlencode }}'><br>
<p></p>
{% if rule %}
{{ smirks }}
<p></p>
{{ rule.reactants_smarts }}
<p></p>
{{ rule.products_smarts }}
<p></p>
<div>
{{ rule.as_svg|safe }}
</div>
{% endif %}
<h2>Diff</h2>
{% if diff %}
{% for d in diff %}
{{ d }}
{% endfor %}
{% else %}
{{ "No diff" }}
{% endif %}
<div>
<div class="col-md-6">
<h2>Ambit</h2>
{% for p in ambit_res %}
{{ p }}<br>
<img width='400' src='{% url 'depict' %}?smiles={{ p|urlencode }}'><br>
{% endfor %}
</div>
<div class="col-md-6">
<h2>RDKit</h2>
{% for p in rdkit_res %}
{{ p }}<br>
<img width='400' src='{% url 'depict' %}?smiles={{ p|urlencode }}'><br>
{% endfor %}
</div>
</div>
{% endif %}
{% endblock content %}

View File

@ -6,8 +6,7 @@
<h4 class="alert-heading">{{ error_message }}</h4>
<hr>
<p class="mb-0">
{{ error_detail }}<br>
The error was logged and will be investigated.
{{ error_detail }}
</p>
</div>

View File

@ -89,7 +89,7 @@
<div class="collapse navbar-collapse collapse-framework navbar-collapse-framework" id="navbarCollapse">
<ul class="nav navbar-nav navbar-nav-framework">
<li>
<a class="button" data-toggle="modal" data-target="#predict_modal">
<a href="#" data-toggle="modal" data-target="#predict_modal">
Predict Pathway
</a>
</li>
@ -134,7 +134,7 @@
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Info <b class="caret"></b></a>
<ul role="menu" class="dropdown-menu">
<!--<li><a href="{{ meta.server_url }}/funding" id="fundingLink">Funding</a></li>-->
<li><a href="https://envipath.com/license/" id="licenceLink">Licences</a></li>
<li><a href="https://community.envipath.org/t/envipath-license/109" id="licenceLink">Licences</a></li>
<li class="divider"></li>
<li><a target="_blank" href="https://wiki.envipath.org/" id="wikiLink">Documentation Wiki</a>
</li>
@ -227,21 +227,23 @@
<div class="row">
<div class="col-lg-12">
<ul class="nav nav-pills nav-justified">
<li><a href="http://eawag.ch" target="_blank">
<img id="image-ealogo"
height="60"
src='{% static "/images/ealogo.gif" %}'
alt="Eawag"/>
</a>
</li>
<li>
<a href="http://ml.auckland.ac.nz" target="_blank">
<img id="image-uoalogo"
height="60"
src='{% static "/images/uoa.png" %}'
<img id="image-uoalogo" height="60" src='{% static "/images/UoA-Logo-Primary-RGB-Small.png" %}'
alt="The Univserity of Auckland"/>
</a>
</li>
<li>
<a href="https://eawag.ch" target="_blank">
<img id="image-ealogo" height="60" src='{% static "/images/ealogo.gif" %}' alt="Eawag"/>
</a>
</li>
<li>
<a href="https://www.uzh.ch/" target="_blank">
<img id="image-ufzlogo" height="60" src='{% static "/images/uzh-logo.svg" %}'
alt="University of Zurich"/>
</a>
</li>
</ul>
</div>
</div>

View File

@ -103,6 +103,7 @@
var textSmiles = $('#index-form-text-input').val().trim();
if (textSmiles === '') {
$(this).prop("disabled", false);
return;
}

View File

@ -1,50 +1,45 @@
{% extends "framework.html" %}
{% block content %}
<div class="panel-group" id="migration-detail">
<div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
Migration Status for {{ bt_rule_name }}
</div>
<div class="panel-body">
<p>A package contains pathways, rules, etc. and can reflect specific experimental
conditions. <a target="_blank" href="https://wiki.envipath.org/index.php/packages" role="button">Learn
more &gt;&gt;</a></p>
</div>
{% for obj in results %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
{% if obj.status %}
<span class="glyphicon glyphicon-ok" aria-hidden="true"
style="float:right" data-toggle="tooltip"
data-placement="top" title="" data-original-title="Reviewed">
</span>
{% else %}
<span class="glyphicon glyphicon-remove" aria-hidden="true"
style="float:right" data-toggle="tooltip"
data-placement="top" title="" data-original-title="Reviewed">
</span>
{% endif %}
<h4 class="panel-title">
<a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail"
href="#{{ obj.id }}">{{ obj.name }}</a>
</h4>
</div>
<div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}">
<div class="panel-body list-group-item">
{% if obj.status %}
<p>Products generated by AMBIT: {{ obj.ambit_smiles }}</p>
<p>Products generated by RDKit: {{ obj.rdkit_smiles }}</p>
{% else %}
<pre>{{ obj.detail }}</pre>
{% endif %}
<div class="panel-group" id="migration-detail">
<div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
Migration Status for {{ bt_rule_name }}
</div>
<div class="panel-body">
<p>A package contains pathways, rules, etc. and can reflect specific experimental
conditions. <a target="_blank" href="https://wiki.envipath.org/index.php/packages" role="button">Learn
more &gt;&gt;</a></p>
</div>
{% for obj in results %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
{% if obj.status %}
<span class="glyphicon glyphicon-ok" aria-hidden="true"
style="float:right" data-toggle="tooltip"
data-placement="top" title="" data-original-title="Reviewed">
</span>
{% else %}
<span class="glyphicon glyphicon-remove" aria-hidden="true"
style="float:right" data-toggle="tooltip"
data-placement="top" title="" data-original-title="Reviewed">
</span>
{% endif %}
<h4 class="panel-title">
<a id="{{ obj.id }}-link" data-toggle="collapse" data-parent="#migration-detail"
href="#{{ obj.id }}">{{ obj.name }}</a>
</h4>
</div>
<div id="{{ obj.id }}" class="panel-collapse collapse {% if not obj.status %}in{% endif %}">
<div class="panel-body list-group-item">
<pre>{{ obj.detail }}</pre>
</div>
</div>
{% endfor %}
</div>
{% endfor %}
</div>
</div>
<script>
<script>
</script>
</script>
{% endblock content %}

View File

@ -15,12 +15,12 @@
enctype="multipart/form-data">
{% csrf_token %}
<p>
<label class="btn btn-primary" for="jsonFile">
<input id="jsonFile" name="file" type="file" style="display:none;"
onchange="$('#upload-file-info').html(this.files[0].name)">
<label class="btn btn-primary" for="legacyJsonFile">
<input id="legacyJsonFile" name="file" type="file" style="display:none;"
onchange="$('#upload-legacy-file-info').html(this.files[0].name)">
Choose JSON File
</label>
<span class="label label-info" id="upload-file-info"></span>
<span class="label label-info" id="upload-legacy-file-info"></span>
<input type="hidden" value="import-legacy-package-json" name="hidden" readonly="">
</p>
</form>

View File

@ -29,11 +29,11 @@
<tr>
<th>
<input type="number" id="dateYear" name="scenario-date-year" class="form-control"
placeholder="YYYY">
placeholder="YYYY" max="{% now "Y" %}">
</th>
<th>
<input type="number" id="dateMonth" name="scenario-date-month" min="1" max="12"
class="form-control" placeholder="MM" align="">
class="form-control" placeholder="MM" >
</th>
<th>
<input type="number" id="dateDay" name="scenario-date-day" min="1" max="31" class="form-control"
@ -88,8 +88,15 @@
$('#new_scenario_form').submit();
});
});
var dateYear = document.getElementById("dateYear");
dateYear.addEventListener("change", () => {
console.log("Final value after editing:", dateYear.value);
if (dateYear.value.length < 4) {
dateYear.value = {% now "Y" %};
}
});
});
</script>

View File

@ -15,12 +15,12 @@
{% csrf_token %}
<p>
<label for="compound-structure-name">Name</label>
<input id="compound-structure-name" class="form-control" name="compound-structure-name" value="{{ structure.name }}">
<input id="compound-structure-name" class="form-control" name="compound-structure-name" value="{{ compound_structure.name }}">
</p>
<p>
<label for="compound-structure-description">Description</label>
<input id="compound-structure-description" type="text" class="form-control"
value="{{ structure.description }}" name="compound-structure-description">
value="{{ compound_structure.description }}" name="compound-structure-description">
</p>
</form>
</div>

View File

@ -23,6 +23,8 @@
</select>
<input type="hidden" name="hidden" value="copy">
</form>
<div id="copy-object-error-message" class="alert alert-danger" role="alert" style="display: none">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
@ -38,6 +40,7 @@
$('#generic-copy-object-modal-form-submit').click(function (e) {
e.preventDefault();
$('#copy-object-error-message').hide()
const packageUrl = $('#target-package').find(":selected").val();
@ -49,12 +52,22 @@
object_to_copy: '{{ current_object.url }}',
}
$.post(packageUrl, formData, function (response) {
if (response.success) {
window.location.href = response.success;
$.ajax({
type: 'post',
data: formData,
url: packageUrl,
success: function (data, textStatus) {
window.location.href = data.success;
},
error: function (jqXHR, textStatus, errorThrown) {
if (jqXHR.responseJSON.error.indexOf('to the same package') > -1) {
$('#copy-object-error-message').append('<p>The target Package is the same as the source Package. Please select another target!</p>');
} else {
$('#copy-object-error-message').append('<p>' + jqXHR.responseJSON.error + '</p>');
}
$('#copy-object-error-message').show();
}
});
});
})

View File

@ -0,0 +1,169 @@
{% load static %}
<style>
.alias-container {
display: flex;
flex-wrap: wrap;
align-items: center;
border: 1px solid #ccc;
border-radius: 4px;
padding: 4px 6px;
cursor: text;
min-height: 38px;
background-color: #fff;
}
.alias {
display: inline-flex;
align-items: center;
background-color: #5bc0de;
color: white;
padding: 4px 8px;
margin: 3px 3px;
border-radius: 4px;
font-size: 13px;
line-height: 1.4;
}
.alias .remove {
margin-left: 6px;
cursor: pointer;
font-weight: bold;
line-height: 1;
}
.alias-input {
flex: 1;
min-width: 120px;
border: none;
outline: none;
margin: 3px 3px;
font-size: 14px;
}
.form-control.alias-container {
height: auto;
box-shadow: none;
}
</style>
<div class="modal fade bs-modal-lg" id="set_aliases_modal" tabindex="-1" aria-labelledby="set_aliases_modal"
aria-modal="true" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<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>
</div>
<div class="modal-body">
<form id="set_aliases_modal_form" accept-charset="UTF-8" action="{{ current_object.url }}"
data-remote="true" method="post">
{% csrf_token %}
<label for="alias-input">Aliases:</label>
<div class="form-control alias-container" id="alias-box">
{% for alias in current_object.aliases %}
<span class="alias">{{ alias|escape }}<span class="remove">&times;</span></span>
{% endfor %}
<input type="text" id="alias-input" class="alias-input" placeholder="Add Alias...">
</div>
</form>
<div id="add-alias-error-message" class="alert alert-danger" role="alert" style="display: none">
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary pull-left" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="set_aliases_modal_form_submit">Submit</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
function addAlias(aliasText) {
aliasText = aliasText.trim();
if (aliasText === '') return;
// Avoid duplicate aliass
var exists = false;
$('#alias-box .alias').each(function () {
if ($(this).text().replace('×', '').trim().toLowerCase() === aliasText.toLowerCase()) {
exists = true;
return false;
}
});
if (!exists) {
var aliasHtml = '<span class="alias">' + $('<div>').text(aliasText).html() +
'<span class="remove">&times;</span></span>';
$(aliasHtml).insertBefore('#alias-input');
}
$('#alias-input').val('');
}
// Add alias when Enter is pressed
$('#alias-input').on('keypress', function (e) {
if (e.which === 13) {
e.preventDefault();
addAlias($(this).val());
}
});
// Add alias when input loses focus
$('#alias-input').on('blur', function () {
var val = $(this).val();
if (val.trim() !== '') {
addAlias(val);
}
});
// Remove alias when clicking ×
$('#alias-box').on('click', '.remove', function () {
$(this).closest('.alias').remove();
});
// Focus input when clicking the container
$('#alias-box').on('click', function () {
$('#alias-input').focus();
});
$('#set_aliases_modal_form_submit').on('click', function (e) {
e.preventDefault();
let aliases = [];
$('#alias-box .alias').each(function () {
aliases.push($(this).text().replace('×', '').trim())
});
if (aliases.length === 0) {
// Set empty string for deletion of all aliases
// If empty list is sent, its gets removed entirely from post data
aliases = ['']
}
formData = {
'aliases': aliases
}
$.ajax({
type: 'post',
data: formData,
url: '{{ current_object.url }}',
traditional: true,
success: function (data, textStatus) {
window.location.href = data.success;
},
error: function (jqXHR, textStatus, errorThrown) {
$('#add-alias-error-message').append('<p>Setting aliases failed!</p>');
$('#add-alias-error-message').show(); }
});
});
});
</script>

View File

@ -0,0 +1,60 @@
{% load static %}
<!-- Delete Object -->
<div id="generic_set_external_reference_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Add External References</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="generic-set-external-reference-modal-form" accept-charset="UTF-8"
action="{{ current_object.url }}"
data-remote="true" method="post">
{% csrf_token %}
<label for="database-select">Select the Database you want to attach an External Reference
for</label>
<select id="database-select" name="selected-database" data-actions-box='true' class="form-control"
data-width='100%'>
<option disabled selected>Select Database</option>
{% for entity, databases in meta.external_databases.items %}
{% 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>`
{% endfor %}
{% endif %}
{% endfor %}
</select>
<p></p>
<div id="input-div" style="display: none">
<label for="identifier" >The reference</label>
<input type="text" id="identifier" name="identifier" class="form-control" placeholder="">
</div>
</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="generic-set-external-reference-modal-form-submit">Submit</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$("#database-select").on("change", function () {
let selected = $(this).val();
$("#identifier").attr("placeholder", $('#db-select-' + selected).data('input-placeholder'));
$("#input-div").show();
});
$('#generic-set-external-reference-modal-form-submit').click(function (e) {
e.preventDefault();
$('#generic-set-external-reference-modal-form').submit();
});
})
</script>

View File

@ -18,6 +18,7 @@
<select id="scenario-select" name="selected-scenarios" data-actions-box='true' class="form-control"
multiple data-width='100%'>
<option disabled>Select Scenarios</option>
<option value="" hidden></option>
</select>
</form>
</div>
@ -64,6 +65,9 @@
$('#set_scenario_modal_form_submit').on('click', function (e) {
e.preventDefault();
if ($('#scenario-select').val().length == 0) {
$('#scenario-select').val("")
}
$('#set_scenario_modal_form').submit();
});
});

View File

@ -2,12 +2,13 @@
{% block content %}
{% block action_modals %}
{% include "modals/objects/edit_rule_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
{% block action_modals %}
{% include "modals/objects/edit_rule_modal.html" %}
{% include "modals/objects/generic_set_aliases_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="rule-detail">
<div class="panel panel-default">
@ -28,10 +29,27 @@
</div>
<div class="panel-body">
<p>
{{ rule.description }}
{{ rule.description|safe }}
</p>
</div>
{% if rule.aliases %}
<!-- Aliases -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-aliases-link" data-toggle="collapse" data-parent="#rule-detail"
href="#rule-aliases">Aliases</a>
</h4>
</div>
<div id="rule-aliases" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for alias in rule.aliases %}
<a class="list-group-item">{{ alias }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Reaction Patterns -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
@ -69,19 +87,41 @@
</div>
{% endif %}
<!-- EC Numbers -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-ec-numbers-link" data-toggle="collapse" data-parent="#rule-detail"
href="#rule-ec-numbers">EC Numbers</a>
</h4>
</div>
<div id="rule-ec-numbers" class="panel-collapse collapse">
<div class="panel-body list-group-item">
{% if rule.enzymelinks %}
<!-- EC Numbers -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-ec-numbers-link" data-toggle="collapse" data-parent="#rule-detail"
href="#rule-ec-numbers">EC Numbers</a>
</h4>
</div>
</div>
<div id="rule-ec-numbers" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for k, v in rule.get_grouped_enzymelinks.items %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="{{ k|slugify }}_Link" data-toggle="collapse"
data-parent="#{{ k|slugify }}_Accordion"
href="#{{ k|slugify }}">
{{ k }}
</a>
</h4>
</div>
<div id="{{ k|slugify }}" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for enzyme in v %}
<a class="list-group-item" href="{{ enzyme.url }}">
{{ enzyme.ec_number }}
<div style="position:absolute;bottom:10px;left:100px;">{{ enzyme.name }}</div>
<div style="float:right;">{{ enzyme.linking_method }}</div>
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endblock content %}

View File

@ -4,8 +4,10 @@
{% block action_modals %}
{% include "modals/objects/edit_compound_modal.html" %}
{% include "modals/objects/generic_set_aliases_modal.html" %}
{% include "modals/objects/add_structure_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_set_external_reference_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
@ -36,6 +38,23 @@
</p>
</div>
{% if compound.aliases %}
<!-- Aliases -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound-aliases-link" data-toggle="collapse" data-parent="#compound-detail"
href="#compound-aliases">Aliases</a>
</h4>
</div>
<div id="compound-aliases" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for alias in compound.aliases %}
<a class="list-group-item">{{ alias }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Description -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">

View File

@ -2,11 +2,13 @@
{% block content %}
{% block action_modals %}
{% include "modals/objects/edit_compound_structure_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
{% block action_modals %}
{% include "modals/objects/edit_compound_structure_modal.html" %}
{% include "modals/objects/generic_set_aliases_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_set_external_reference_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="compound-structure-detail">
<div class="panel panel-default">
@ -32,34 +34,52 @@
<!-- Image -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound-image-link" data-toggle="collapse" data-parent="#compound-detail"
href="#compound-image">Image Representation</a>
<a id="compound-structure-image-link" data-toggle="collapse" data-parent="#compound-structure-detail"
href="#compound-structure-image">Image Representation</a>
</h4>
</div>
<div id="compound-image" class="panel-collapse collapse in">
<div id="compound-structure-image" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<div id="image-div" align="center">
{{ compound_structure.as_svg|safe }}
</div>
</div>
</div>
<!-- SMILES -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound-smiles-link" data-toggle="collapse" data-parent="#compound-detail"
href="#compound-smiles">SMILES Representation</a>
<a id="compound-structure-smiles-link" data-toggle="collapse" data-parent="#compound-structure-detail"
href="#compound-structure-smiles">SMILES Representation</a>
</h4>
</div>
<div id="compound-smiles" class="panel-collapse collapse in">
<div id="compound-structure-smiles" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{{ compound_structure.smiles }}
</div>
</div>
{% if compound_structure.aliases %}
<!-- Aliases -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound-structure-aliases-link" data-toggle="collapse" data-parent="#compound-structure-detail"
href="#compound-structure-aliases">Aliases</a>
</h4>
</div>
<div id="compound-structure-aliases" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for alias in compound_structure.aliases %}
<a class="list-group-item">{{ alias }}</a>
{% endfor %}
</div>
</div>
{% endif %}
{% if compound_structure.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound_structure-scenario-link" data-toggle="collapse" data-parent="#compound-structure-detail"
<a id="compound-structure-scenario-link" data-toggle="collapse" data-parent="#compound-structure-detail"
href="#compound-structure-scenario">Scenarios</a>
</h4>
</div>

View File

@ -4,6 +4,7 @@
{% block action_modals %}
{# {% include "modals/objects/edit_edge_modal.html" %}#}
{% include "modals/objects/generic_set_aliases_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
@ -39,6 +40,23 @@
</div>
</div>
{% if edge.aliases %}
<!-- Aliases -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="edge-aliases-link" data-toggle="collapse" data-parent="#edge-detail"
href="#edge-aliases">Aliases</a>
</h4>
</div>
<div id="edge-aliases" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for alias in edge.aliases %}
<a class="list-group-item">{{ alias }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Image -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">

View File

@ -0,0 +1,105 @@
{% extends "framework.html" %}
{% block content %}
<div class="panel-group" id="enzyme-detail">
<div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
{{ enzymelink.ec_number }}
</div>
<!-- Name -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-name-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-name">Enzyme Name</a>
</h4>
</div>
<div id="enzyme-name" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{{ enzymelink.name }}
</div>
</div>
<!-- Linking Method -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-linking-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-linking">Linking Method</a>
</h4>
</div>
<div id="enzyme-linking" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{{ enzymelink.linking_method }}. &nbsp;<a
href="https://wiki.envipath.org/index.php/Rules#EnzymeLinks" target="#">Learn more &gt;&gt;</a>
</div>
</div>
{% if enzymelink.kegg_reaction_links %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-evidence-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-evidence">Linking Evidence</a>
</h4>
</div>
<div id="enzyme-evidence" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for kl in enzymelink.kegg_reaction_links %}
<a class="list-group-item"
href="{{ kl.external_url }}">{{ kl.identifier_value }}</a>
{% endfor %}
</div>
</div>
{% endif %}
{% if enzymelink.reaction_evidence.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-reaction-evidence-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-reaction-evidence">Linking Evidence - enviPath Reactions</a>
</h4>
</div>
<div id="enzyme-reaction-evidence" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for r in enzymelink.reaction_evidence.all %}
<a class="list-group-item" href="{{ r.url }}">{{ r.name }} <i>({{ r.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
{% if enzymelink.edge_evidence.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-edge-evidence-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-edge-evidence">Linking Evidence - enviPath Pathways</a>
</h4>
</div>
<div id="enzyme-edge-evidence" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for e in enzymelink.edge_evidence.all %}
<a class="list-group-item" href="{{ e.pathway.url }}">{{ e.pathway.name }}
<i>({{ r.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- External DB Reference -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="enzyme-external-identifier-link" data-toggle="collapse" data-parent="#enzyme-detail"
href="#enzyme-external-identifier">External DB References</a>
</h4>
</div>
<div id="enzyme-external-identifier" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<a class="list-group-item"
href="http://www.brenda-enzymes.org/enzyme.php?ecno={{ enzymelink.ec_number }}"
target="_blank"> Brenda entry for {{ enzymelink.ec_number }}</a>
</div>
</div>
</div>
</div>
{% endblock content %}

View File

@ -315,12 +315,18 @@
$("#predict-button").on("click", function (e) {
e.preventDefault();
clear("predictResultTable");
data = {
"smiles": $("#smiles-to-predict").val(),
"classify": "ILikeCats!"
}
clear("predictResultTable");
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({
@ -332,17 +338,17 @@
$("#predictLoading").empty();
handlePredictionResponse(data);
} catch (error) {
console.log("Error");
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append("Error while processing request :/");
$("#predictResultTable").append("Error while processing response :/");
}
},
error: function (jqXHR, textStatus, errorThrown) {
error: function (jqXHR, textStatus, errorThrown, x) {
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append("Error while processing request :/");
}
$("#predictResultTable").append(jqXHR.responseJSON.error);
},
});
});
}
@ -351,12 +357,20 @@
$("#assess-button").on("click", function (e) {
e.preventDefault();
clear("appDomainAssessmentResultTable");
data = {
"smiles": $("#smiles-to-assess").val(),
"app-domain-assessment": "ILikeCats!"
}
clear("appDomainAssessmentResultTable");
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({
@ -369,16 +383,15 @@
handleAssessmentResponse("{% url 'depict' %}", data);
console.log(data);
} catch (error) {
console.log("Error");
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Error while processing request :/");
$("#appDomainAssessmentResultTable").append("Error while processing response :/");
}
},
error: function (jqXHR, textStatus, errorThrown) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Error while processing request :/");
$("#appDomainAssessmentResultTable").append(jqXHR.responseJSON.error);
}
});
});

View File

@ -4,6 +4,7 @@
{% block action_modals %}
{% include "modals/objects/edit_node_modal.html" %}
{% include "modals/objects/generic_set_aliases_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
@ -42,6 +43,23 @@
</div>
</div>
{% if node.aliases %}
<!-- Aliases -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="node-aliases-link" data-toggle="collapse" data-parent="#node-detail"
href="#node-aliases">Aliases</a>
</h4>
</div>
<div id="node-aliases" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for alias in node.aliases %}
<a class="list-group-item">{{ alias }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Image -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">

View File

@ -29,7 +29,7 @@
</div>
</div>
<div class="panel-body">
<p> {{ package.description }} </p>
<p> {{ package.description|safe }} </p>
</div>
<ul class="list-group">
<li class="list-group-item">

View File

@ -85,6 +85,7 @@
{% include "modals/objects/download_pathway_image_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" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/delete_pathway_node_modal.html" %}
{% include "modals/objects/delete_pathway_edge_modal.html" %}
@ -176,9 +177,6 @@
</nav>
<div id="vizdiv" >
<svg id="pwsvg">
{% if debug %}
<rect width="100%" height="100%" fill="aliceblue"/>
{% endif %}
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="43" refY="5" markerWidth="6" markerHeight="6"
orient="auto-start-reverse" markerUnits="userSpaceOnUse">
@ -210,6 +208,23 @@
</div>
</div>
{% if pathway.aliases %}
<!-- Aliases -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="pathway-aliases-link" data-toggle="collapse" data-parent="#pathway-detail"
href="#pathway-aliases">Aliases</a>
</h4>
</div>
<div id="pathway-aliases" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for alias in pathway.aliases %}
<a class="list-group-item">{{ alias }}</a>
{% endfor %}
</div>
</div>
{% endif %}
{% if pathway.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">

View File

@ -4,8 +4,10 @@
{% block action_modals %}
{% include "modals/objects/edit_reaction_modal.html" %}
{% include "modals/objects/generic_set_aliases_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_set_external_reference_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
@ -40,6 +42,23 @@
</div>
</div>
{% if reaction.aliases %}
<!-- Aliases -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="reaction-aliases-link" data-toggle="collapse" data-parent="#reaction-detail"
href="#reaction-aliases">Aliases</a>
</h4>
</div>
<div id="reaction-aliases" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for alias in reaction.aliases %}
<a class="list-group-item">{{ alias }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Image -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
@ -105,6 +124,23 @@
</div>
{% endif %}
{% if reaction.get_related_enzymes %}
<!-- EC Numbers -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-ec-numbers-link" data-toggle="collapse" data-parent="#rule-detail"
href="#rule-ec-numbers">EC Numbers</a>
</h4>
</div>
<div id="rule-ec-numbers" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for e in reaction.get_related_enzymes %}
<a class="list-group-item" href="http://www.brenda-enzymes.org/enzyme.php?ecno={{ e.ec_number }}">{{ e.name }}</a>
{% endfor %}
</div>
</div>
{% endif %}
{% if reaction.related_pathways %}
<!-- Pathways -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">

View File

@ -4,6 +4,7 @@
{% block action_modals %}
{% include "modals/objects/edit_rule_modal.html" %}
{% include "modals/objects/generic_set_aliases_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
@ -32,6 +33,23 @@
</p>
</div>
{% if rule.aliases %}
<!-- Aliases -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-aliases-link" data-toggle="collapse" data-parent="#rule-detail"
href="#rule-aliases">Aliases</a>
</h4>
</div>
<div id="rule-aliases" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for alias in rule.aliases %}
<a class="list-group-item">{{ alias }}</a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Representation -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
@ -183,6 +201,43 @@
</div>
</div>
{% endif %}
{% if rule.enzymelinks %}
<!-- EC Numbers -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-ec-numbers-link" data-toggle="collapse" data-parent="#rule-detail"
href="#rule-ec-numbers">EC Numbers</a>
</h4>
</div>
<div id="rule-ec-numbers" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for k, v in rule.get_grouped_enzymelinks.items %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">
<h4 class="panel-title">
<a id="{{ k|slugify }}_Link" data-toggle="collapse"
data-parent="#{{ k|slugify }}_Accordion"
href="#{{ k|slugify }}">
{{ k }}
</a>
</h4>
</div>
<div id="{{ k|slugify }}" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for enzyme in v %}
<a class="list-group-item" href="{{ enzyme.url }}">
{{ enzyme.ec_number }}
<div style="position:absolute;bottom:10px;left:100px;">{{ enzyme.name }}</div>
<div style="float:right;">{{ enzyme.linking_method }}</div>
</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</div>
{% endif %}
</div>
</div>
{% endblock content %}

View File

@ -13,91 +13,80 @@ class CompoundTest(TestCase):
@classmethod
def setUpClass(cls):
super(CompoundTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
def test_smoke(self):
c = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='No Desc'
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="Afoxolaner",
description="No Desc",
)
self.assertEqual(c.default_structure.smiles,
'C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F')
self.assertEqual(c.name, 'Afoxolaner')
self.assertEqual(c.description, 'No Desc')
self.assertEqual(
c.default_structure.smiles,
"C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
)
self.assertEqual(c.name, "Afoxolaner")
self.assertEqual(c.description, "No Desc")
def test_missing_smiles(self):
with self.assertRaises(ValueError):
_ = Compound.create(
self.package,
smiles=None,
name='Afoxolaner',
description='No Desc'
)
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
with self.assertRaises(ValueError):
_ = Compound.create(
self.package,
smiles='',
name='Afoxolaner',
description='No Desc'
)
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
with self.assertRaises(ValueError):
_ = Compound.create(
self.package,
smiles=' ',
name='Afoxolaner',
description='No Desc'
)
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
def test_smiles_are_trimmed(self):
c = Compound.create(
self.package,
smiles=' C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F ',
name='Afoxolaner',
description='No Desc'
smiles=" C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F ",
name="Afoxolaner",
description="No Desc",
)
self.assertEqual(c.default_structure.smiles,
'C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F')
self.assertEqual(
c.default_structure.smiles,
"C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
)
def test_name_and_description_optional(self):
c = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
)
self.assertEqual(c.name, 'Compound 1')
self.assertEqual(c.description, 'no description')
self.assertEqual(c.name, "Compound 1")
self.assertEqual(c.description, "no description")
def test_empty_name_and_description_are_ignored(self):
c = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='',
description='',
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="",
description="",
)
self.assertEqual(c.name, 'Compound 1')
self.assertEqual(c.description, 'no description')
self.assertEqual(c.name, "Compound 1")
self.assertEqual(c.description, "no description")
def test_deduplication(self):
c1 = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='No Desc'
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="Afoxolaner",
description="No Desc",
)
c2 = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='No Desc'
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="Afoxolaner",
description="No Desc",
)
# Check if create detects that this Compound already exist
@ -109,36 +98,36 @@ class CompoundTest(TestCase):
with self.assertRaises(ValueError):
_ = Compound.create(
self.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='No Desc'
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="Afoxolaner",
description="No Desc",
)
def test_create_with_standardized_smiles(self):
c = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardized SMILES',
description='No Desc'
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardized SMILES",
description="No Desc",
)
self.assertEqual(len(c.structures.all()), 1)
cs = c.structures.all()[0]
self.assertEqual(cs.normalized_structure, True)
self.assertEqual(cs.smiles, 'O=C(O)C1=CC=C([N+](=O)[O-])C=C1')
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
def test_create_with_non_standardized_smiles(self):
c = Compound.create(
self.package,
smiles='[O-][N+](=O)c1ccc(C(=O)[O-])cc1',
name='Non Standardized SMILES',
description='No Desc'
smiles="[O-][N+](=O)c1ccc(C(=O)[O-])cc1",
name="Non Standardized SMILES",
description="No Desc",
)
self.assertEqual(len(c.structures.all()), 2)
for cs in c.structures.all():
if cs.normalized_structure:
self.assertEqual(cs.smiles, 'O=C(O)C1=CC=C([N+](=O)[O-])C=C1')
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
break
else:
# Loop finished without break, lets fail...
@ -147,51 +136,54 @@ class CompoundTest(TestCase):
def test_add_structure_smoke(self):
c = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardized SMILES',
description='No Desc'
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardized SMILES",
description="No Desc",
)
c.add_structure('[O-][N+](=O)c1ccc(C(=O)[O-])cc1', 'Non Standardized SMILES')
c.add_structure("[O-][N+](=O)c1ccc(C(=O)[O-])cc1", "Non Standardized SMILES")
self.assertEqual(len(c.structures.all()), 2)
def test_add_structure_with_different_normalized_smiles(self):
c = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardized SMILES',
description='No Desc'
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardized SMILES",
description="No Desc",
)
with self.assertRaises(ValueError):
c.add_structure(
'C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
'Different Standardized SMILES')
"C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
"Different Standardized SMILES",
)
def test_delete(self):
c = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardization Test',
description='No Desc'
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardization Test",
description="No Desc",
)
c.delete()
self.assertEqual(Compound.objects.filter(package=self.package).count(), 0)
self.assertEqual(CompoundStructure.objects.filter(compound__package=self.package).count(), 0)
self.assertEqual(
CompoundStructure.objects.filter(compound__package=self.package).count(), 0
)
def test_set_as_default_structure(self):
c1 = Compound.create(
self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Standardized SMILES',
description='No Desc'
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name="Standardized SMILES",
description="No Desc",
)
default_structure = c1.default_structure
c2 = c1.add_structure('[O-][N+](=O)c1ccc(C(=O)[O-])cc1', 'Non Standardized SMILES')
c2 = c1.add_structure("[O-][N+](=O)c1ccc(C(=O)[O-])cc1", "Non Standardized SMILES")
c1.set_default_structure(c2)
self.assertNotEqual(default_structure, c2)

View File

@ -1,6 +1,5 @@
from django.test import TestCase
from django.test import TestCase
from epdb.logic import PackageManager
from epdb.models import Compound, User, Reaction
@ -12,50 +11,47 @@ class CopyTest(TestCase):
@classmethod
def setUpClass(cls):
super(CopyTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Source Package', 'No Desc')
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Source Package", "No Desc")
cls.AFOXOLANER = Compound.create(
cls.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='Test compound for copying'
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
name="Afoxolaner",
description="Test compound for copying",
)
cls.FOUR_NITROBENZOIC_ACID = Compound.create(
cls.package,
smiles='[O-][N+](=O)c1ccc(C(=O)[O-])cc1', # Normalized: O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Test Compound',
description='Compound with multiple structures'
smiles="[O-][N+](=O)c1ccc(C(=O)[O-])cc1", # Normalized: O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name="Test Compound",
description="Compound with multiple structures",
)
cls.ETHANOL = Compound.create(
cls.package,
smiles='CCO',
name='Ethanol',
description='Simple alcohol'
cls.package, smiles="CCO", name="Ethanol", description="Simple alcohol"
)
cls.target_package = PackageManager.create_package(cls.user, 'Target Package', 'No Desc')
cls.target_package = PackageManager.create_package(cls.user, "Target Package", "No Desc")
cls.reaction_educt = Compound.create(
cls.package,
smiles='C(CCl)Cl',
name='1,2-Dichloroethane',
description='Eawag BBD compound c0001'
smiles="C(CCl)Cl",
name="1,2-Dichloroethane",
description="Eawag BBD compound c0001",
).default_structure
cls.reaction_product = Compound.create(
cls.package,
smiles='C(CO)Cl',
name='2-Chloroethanol',
description='Eawag BBD compound c0005'
smiles="C(CO)Cl",
name="2-Chloroethanol",
description="Eawag BBD compound c0005",
).default_structure
cls.REACTION = Reaction.create(
package=cls.package,
name='Eawag BBD reaction r0001',
name="Eawag BBD reaction r0001",
educts=[cls.reaction_educt],
products=[cls.reaction_product],
multi_step=False
multi_step=False,
)
def test_compound_copy_basic(self):
@ -68,7 +64,9 @@ class CopyTest(TestCase):
self.assertEqual(self.AFOXOLANER.description, copied_compound.description)
self.assertEqual(copied_compound.package, self.target_package)
self.assertEqual(self.AFOXOLANER.package, self.package)
self.assertEqual(self.AFOXOLANER.default_structure.smiles, copied_compound.default_structure.smiles)
self.assertEqual(
self.AFOXOLANER.default_structure.smiles, copied_compound.default_structure.smiles
)
def test_compound_copy_with_multiple_structures(self):
"""Test copying a compound with multiple structures"""
@ -86,7 +84,7 @@ class CopyTest(TestCase):
self.assertIsNotNone(copied_compound.default_structure)
self.assertEqual(
copied_compound.default_structure.smiles,
self.FOUR_NITROBENZOIC_ACID.default_structure.smiles
self.FOUR_NITROBENZOIC_ACID.default_structure.smiles,
)
def test_compound_copy_preserves_aliases(self):
@ -95,15 +93,15 @@ class CopyTest(TestCase):
original_compound = self.ETHANOL
# Add aliases if the method exists
if hasattr(original_compound, 'add_alias'):
original_compound.add_alias('Ethyl alcohol')
original_compound.add_alias('Grain alcohol')
if hasattr(original_compound, "add_alias"):
original_compound.add_alias("Ethyl alcohol")
original_compound.add_alias("Grain alcohol")
mapping = dict()
copied_compound = original_compound.copy(self.target_package, mapping)
# Verify aliases were copied if they exist
if hasattr(original_compound, 'aliases') and hasattr(copied_compound, 'aliases'):
if hasattr(original_compound, "aliases") and hasattr(copied_compound, "aliases"):
original_aliases = original_compound.aliases
copied_aliases = copied_compound.aliases
self.assertEqual(original_aliases, copied_aliases)
@ -113,10 +111,10 @@ class CopyTest(TestCase):
original_compound = self.ETHANOL
# Add external identifiers if the methods exist
if hasattr(original_compound, 'add_cas_number'):
original_compound.add_cas_number('64-17-5')
if hasattr(original_compound, 'add_pubchem_compound_id'):
original_compound.add_pubchem_compound_id('702')
if hasattr(original_compound, "add_cas_number"):
original_compound.add_cas_number("64-17-5")
if hasattr(original_compound, "add_pubchem_compound_id"):
original_compound.add_pubchem_compound_id("702")
mapping = dict()
copied_compound = original_compound.copy(self.target_package, mapping)
@ -146,7 +144,9 @@ class CopyTest(TestCase):
self.assertEqual(original_structure.smiles, copied_structure.smiles)
self.assertEqual(original_structure.canonical_smiles, copied_structure.canonical_smiles)
self.assertEqual(original_structure.inchikey, copied_structure.inchikey)
self.assertEqual(original_structure.normalized_structure, copied_structure.normalized_structure)
self.assertEqual(
original_structure.normalized_structure, copied_structure.normalized_structure
)
# Verify they are different objects
self.assertNotEqual(original_structure.uuid, copied_structure.uuid)
@ -177,7 +177,9 @@ class CopyTest(TestCase):
self.assertEqual(orig_educt.compound.package, self.package)
self.assertEqual(orig_educt.smiles, copy_educt.smiles)
for orig_product, copy_product in zip(self.REACTION.products.all(), copied_reaction.products.all()):
for orig_product, copy_product in zip(
self.REACTION.products.all(), copied_reaction.products.all()
):
self.assertNotEqual(orig_product.uuid, copy_product.uuid)
self.assertEqual(orig_product.name, copy_product.name)
self.assertEqual(orig_product.description, copy_product.description)

View File

@ -11,21 +11,21 @@ class DatasetTest(TestCase):
def setUp(self):
self.cs1 = Compound.create(
self.package,
name='2,6-Dibromohydroquinone',
description='http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1/compound/d6435251-1a54-4327-b4b1-fd6e9a8f4dc9/structure/d8a0225c-dbb5-4e6c-a642-730081c09c5b',
smiles='C1=C(C(=C(C=C1O)Br)O)Br',
name="2,6-Dibromohydroquinone",
description="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1/compound/d6435251-1a54-4327-b4b1-fd6e9a8f4dc9/structure/d8a0225c-dbb5-4e6c-a642-730081c09c5b",
smiles="C1=C(C(=C(C=C1O)Br)O)Br",
).default_structure
self.cs2 = Compound.create(
self.package,
smiles='O=C(O)CC(=O)/C=C(/Br)C(=O)O',
smiles="O=C(O)CC(=O)/C=C(/Br)C(=O)O",
).default_structure
self.rule1 = Rule.create(
rule_type='SimpleAmbitRule',
rule_type="SimpleAmbitRule",
package=self.package,
smirks='[#8:8]([H])-[c:4]1[c:3]([H])[c:2](-[#1,#17,#35:9])[c:1](-[#8:7]([H]))[c:6](-[#1,#17,#35])[c:5]([H])1>>[#8-]-[#6:6](=O)-[#6:5]-[#6:4](=[O:8])\[#6:3]=[#6:2](\[#1,#17,#35:9])-[#6:1](-[#8-])=[O:7]',
description='http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1/simple-ambit-rule/f6a56c0f-a4a0-4ee3-b006-d765b4767cf6'
smirks="[#8:8]([H])-[c:4]1[c:3]([H])[c:2](-[#1,#17,#35:9])[c:1](-[#8:7]([H]))[c:6](-[#1,#17,#35])[c:5]([H])1>>[#8-]-[#6:6](=O)-[#6:5]-[#6:4](=[O:8])\\[#6:3]=[#6:2](\\[#1,#17,#35:9])-[#6:1](-[#8-])=[O:7]",
description="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1/simple-ambit-rule/f6a56c0f-a4a0-4ee3-b006-d765b4767cf6",
)
self.reaction1 = Reaction.create(
@ -33,14 +33,14 @@ class DatasetTest(TestCase):
educts=[self.cs1],
products=[self.cs2],
rules=[self.rule1],
multi_step=False
multi_step=False,
)
@classmethod
def setUpClass(cls):
super(DatasetTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
def test_smoke(self):
reactions = [r for r in Reaction.objects.filter(package=self.package)]

35
tests/test_enviformer.py Normal file
View File

@ -0,0 +1,35 @@
from tempfile import TemporaryDirectory
from django.test import TestCase, tag
from epdb.logic import PackageManager
from epdb.models import User, EnviFormer, Package
@tag("slow")
class EnviFormerTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"]
@classmethod
def setUpClass(cls):
super(EnviFormerTest, cls).setUpClass()
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
cls.BBD_SUBSET = Package.objects.get(name="Fixtures")
def test_model_flow(self):
"""Test the full flow of EnviFormer, dataset build -> model finetune -> model evaluate -> model inference"""
with TemporaryDirectory() as tmpdir:
with self.settings(MODEL_DIR=tmpdir):
threshold = float(0.5)
data_package_objs = [self.BBD_SUBSET]
eval_packages_objs = [self.BBD_SUBSET]
mod = EnviFormer.create(
self.package, data_package_objs, eval_packages_objs, threshold=threshold
)
mod.build_dataset()
mod.build_model()
mod.multigen_eval = True
mod.save()
mod.evaluate_model()
mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C")

View File

@ -4,8 +4,7 @@ from utilities.chem import FormatConverter
class FormatConverterTestCase(TestCase):
def test_standardization(self):
smiles = 'C[n+]1c([N-](C))cccc1'
smiles = "C[n+]1c([N-](C))cccc1"
standardized_smiles = FormatConverter.standardize(smiles)
self.assertEqual(standardized_smiles, 'CN=C1C=CC=CN1C')
self.assertEqual(standardized_smiles, "CN=C1C=CC=CN1C")

View File

@ -4,7 +4,7 @@ import numpy as np
from django.test import TestCase
from epdb.logic import PackageManager
from epdb.models import User, MLRelativeReasoning, RuleBasedRelativeReasoning, Package
from epdb.models import User, MLRelativeReasoning, Package
class ModelTest(TestCase):
@ -13,9 +13,9 @@ class ModelTest(TestCase):
@classmethod
def setUpClass(cls):
super(ModelTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
cls.BBD_SUBSET = Package.objects.get(name='Fixtures')
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
cls.BBD_SUBSET = Package.objects.get(name="Fixtures")
def test_smoke(self):
with TemporaryDirectory() as tmpdir:
@ -24,7 +24,7 @@ class ModelTest(TestCase):
rule_package_objs = [self.BBD_SUBSET]
data_package_objs = [self.BBD_SUBSET]
eval_packages_objs = []
eval_packages_objs = [self.BBD_SUBSET]
mod = MLRelativeReasoning.create(
self.package,
@ -32,8 +32,8 @@ class ModelTest(TestCase):
data_package_objs,
eval_packages_objs,
threshold=threshold,
name='ECC - BBD - 0.5',
description='Created MLRelativeReasoning in Testcase',
name="ECC - BBD - 0.5",
description="Created MLRelativeReasoning in Testcase",
)
# mod = RuleBasedRelativeReasoning.create(
@ -52,9 +52,9 @@ class ModelTest(TestCase):
mod.build_model()
mod.multigen_eval = True
mod.save()
# mod.evaluate_model()
mod.evaluate_model()
results = mod.predict('CCN(CC)C(=O)C1=CC(=CC=C1)C')
results = mod.predict("CCN(CC)C(=O)C1=CC(=CC=C1)C")
products = dict()
for r in results:
@ -62,8 +62,11 @@ class ModelTest(TestCase):
products[tuple(sorted(ps.product_set))] = (r.rule.name, r.probability)
expected = {
('CC=O', 'CCNC(=O)C1=CC(C)=CC=C1'): ('bt0243-4301', np.float64(0.33333333333333337)),
('CC1=CC=CC(C(=O)O)=C1', 'CCNCC'): ('bt0430-4011', np.float64(0.25)),
("CC=O", "CCNC(=O)C1=CC(C)=CC=C1"): (
"bt0243-4301",
np.float64(0.33333333333333337),
),
("CC1=CC=CC(C(=O)O)=C1", "CCNCC"): ("bt0430-4011", np.float64(0.25)),
}
self.assertEqual(products, expected)

View File

@ -1,4 +1,3 @@
import json
from django.test import TestCase
from networkx.utils.misc import graphs_equal
from epdb.logic import PackageManager, SPathway
@ -12,9 +11,11 @@ class MultiGenTest(TestCase):
@classmethod
def setUpClass(cls):
super(MultiGenTest, cls).setUpClass()
cls.user: 'User' = User.objects.get(username='anonymous')
cls.package: 'Package' = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
cls.BBD_SUBSET: 'Package' = Package.objects.get(name='Fixtures')
cls.user: "User" = User.objects.get(username="anonymous")
cls.package: "Package" = PackageManager.create_package(
cls.user, "Anon Test Package", "No Desc"
)
cls.BBD_SUBSET: "Package" = Package.objects.get(name="Fixtures")
def test_equal_pathways(self):
"""Test that two identical pathways return a precision and recall of 1.0"""
@ -23,14 +24,23 @@ class MultiGenTest(TestCase):
if len(pathway.edge_set.all()) == 0: # Do not test pathways with no edges
continue
score, precision, recall = multigen_eval(pathway, pathway)
self.assertEqual(precision, 1.0, f"Precision should be one for identical pathways. "
f"Failed on pathway: {pathway.name}")
self.assertEqual(recall, 1.0, f"Recall should be one for identical pathways. "
f"Failed on pathway: {pathway.name}")
self.assertEqual(
precision,
1.0,
f"Precision should be one for identical pathways. "
f"Failed on pathway: {pathway.name}",
)
self.assertEqual(
recall,
1.0,
f"Recall should be one for identical pathways. Failed on pathway: {pathway.name}",
)
def test_intermediates(self):
"""Test that an intermediate can be correctly identified and the metrics are correctly adjusted"""
score, precision, recall, intermediates = multigen_eval(*self.intermediate_case(), return_intermediates=True)
score, precision, recall, intermediates = multigen_eval(
*self.intermediate_case(), return_intermediates=True
)
self.assertEqual(len(intermediates), 1, "There should be 1 found intermediate")
self.assertEqual(precision, 1, "Precision should be 1")
self.assertEqual(recall, 1, "Recall should be 1")
@ -49,7 +59,9 @@ class MultiGenTest(TestCase):
def test_all(self):
"""Test an intermediate, false-positive and false-negative together"""
score, precision, recall, intermediates = multigen_eval(*self.all_case(), return_intermediates=True)
score, precision, recall, intermediates = multigen_eval(
*self.all_case(), return_intermediates=True
)
self.assertEqual(len(intermediates), 1, "There should be 1 found intermediate")
self.assertAlmostEqual(precision, 0.6, 3, "Precision should be 0.6")
self.assertAlmostEqual(recall, 0.75, 3, "Recall should be 0.75")
@ -57,19 +69,23 @@ class MultiGenTest(TestCase):
def test_shallow_pathway(self):
pathways = self.BBD_SUBSET.pathways.all()
for pathway in pathways:
pathway_name = pathway.name
if len(pathway.edge_set.all()) == 0: # Do not test pathways with no edges
continue
shallow_pathway = graph_from_pathway(SPathway.from_pathway(pathway))
pathway = graph_from_pathway(pathway)
if not graphs_equal(shallow_pathway, pathway):
print('\n\nS', shallow_pathway.adj)
print('\n\nPW', pathway.adj)
print("\n\nS", shallow_pathway.adj)
print("\n\nPW", pathway.adj)
# print(shallow_pathway.nodes, pathway.nodes)
# print(shallow_pathway.graph, pathway.graph)
self.assertTrue(graphs_equal(shallow_pathway, pathway), f"Networkx graph from shallow pathway not "
f"equal to pathway for pathway {pathway.name}")
self.assertTrue(
graphs_equal(shallow_pathway, pathway),
f"Networkx graph from shallow pathway not "
f"equal to pathway for pathway {pathway.name}",
)
def test_graph_edit_eval(self):
"""Performs all the previous tests but with graph_edit_eval
@ -79,10 +95,16 @@ class MultiGenTest(TestCase):
if len(pathway.edge_set.all()) == 0: # Do not test pathways with no edges
continue
score = pathway_edit_eval(pathway, pathway)
self.assertEqual(score, 0.0, "Pathway edit distance should be zero for identical pathways. "
f"Failed on pathway: {pathway.name}")
self.assertEqual(
score,
0.0,
"Pathway edit distance should be zero for identical pathways. "
f"Failed on pathway: {pathway.name}",
)
inter_score = pathway_edit_eval(*self.intermediate_case())
self.assertAlmostEqual(inter_score, 1.75, 3, "Pathway edit distance failed on intermediate case")
self.assertAlmostEqual(
inter_score, 1.75, 3, "Pathway edit distance failed on intermediate case"
)
fp_score = pathway_edit_eval(*self.fp_case())
self.assertAlmostEqual(fp_score, 1.25, 3, "Pathway edit distance failed on fp case")
fn_score = pathway_edit_eval(*self.fn_case())
@ -93,22 +115,30 @@ class MultiGenTest(TestCase):
def intermediate_case(self):
"""Create an example with an intermediate in the predicted pathway"""
true_pathway = Pathway.create(self.package, "CCO")
true_pathway.add_edge([true_pathway.root_nodes.all()[0]], [true_pathway.add_node("CC(=O)O", depth=1)])
true_pathway.add_edge(
[true_pathway.root_nodes.all()[0]], [true_pathway.add_node("CC(=O)O", depth=1)]
)
pred_pathway = Pathway.create(self.package, "CCO")
pred_pathway.add_edge([pred_pathway.root_nodes.all()[0]],
[acetaldehyde := pred_pathway.add_node("CC=O", depth=1)])
pred_pathway.add_edge(
[pred_pathway.root_nodes.all()[0]],
[acetaldehyde := pred_pathway.add_node("CC=O", depth=1)],
)
pred_pathway.add_edge([acetaldehyde], [pred_pathway.add_node("CC(=O)O", depth=2)])
return true_pathway, pred_pathway
def fp_case(self):
"""Create an example with an extra compound in the predicted pathway"""
true_pathway = Pathway.create(self.package, "CCO")
true_pathway.add_edge([true_pathway.root_nodes.all()[0]],
[acetaldehyde := true_pathway.add_node("CC=O", depth=1)])
true_pathway.add_edge(
[true_pathway.root_nodes.all()[0]],
[acetaldehyde := true_pathway.add_node("CC=O", depth=1)],
)
true_pathway.add_edge([acetaldehyde], [true_pathway.add_node("CC(=O)O", depth=2)])
pred_pathway = Pathway.create(self.package, "CCO")
pred_pathway.add_edge([pred_pathway.root_nodes.all()[0]],
[acetaldehyde := pred_pathway.add_node("CC=O", depth=1)])
pred_pathway.add_edge(
[pred_pathway.root_nodes.all()[0]],
[acetaldehyde := pred_pathway.add_node("CC=O", depth=1)],
)
pred_pathway.add_edge([acetaldehyde], [pred_pathway.add_node("CC(=O)O", depth=2)])
pred_pathway.add_edge([acetaldehyde], [pred_pathway.add_node("C", depth=2)])
return true_pathway, pred_pathway
@ -116,22 +146,30 @@ class MultiGenTest(TestCase):
def fn_case(self):
"""Create an example with a missing compound in the predicted pathway"""
true_pathway = Pathway.create(self.package, "CCO")
true_pathway.add_edge([true_pathway.root_nodes.all()[0]],
[acetaldehyde := true_pathway.add_node("CC=O", depth=1)])
true_pathway.add_edge(
[true_pathway.root_nodes.all()[0]],
[acetaldehyde := true_pathway.add_node("CC=O", depth=1)],
)
true_pathway.add_edge([acetaldehyde], [true_pathway.add_node("CC(=O)O", depth=2)])
pred_pathway = Pathway.create(self.package, "CCO")
pred_pathway.add_edge([pred_pathway.root_nodes.all()[0]], [pred_pathway.add_node("CC=O", depth=1)])
pred_pathway.add_edge(
[pred_pathway.root_nodes.all()[0]], [pred_pathway.add_node("CC=O", depth=1)]
)
return true_pathway, pred_pathway
def all_case(self):
"""Create an example with an intermediate, extra compound and missing compound"""
true_pathway = Pathway.create(self.package, "CCO")
true_pathway.add_edge([true_pathway.root_nodes.all()[0]],
[acetaldehyde := true_pathway.add_node("CC=O", depth=1)])
true_pathway.add_edge(
[true_pathway.root_nodes.all()[0]],
[acetaldehyde := true_pathway.add_node("CC=O", depth=1)],
)
true_pathway.add_edge([acetaldehyde], [true_pathway.add_node("C", depth=2)])
true_pathway.add_edge([acetaldehyde], [true_pathway.add_node("CC(=O)O", depth=2)])
pred_pathway = Pathway.create(self.package, "CCO")
pred_pathway.add_edge([pred_pathway.root_nodes.all()[0]], [methane := pred_pathway.add_node("C", depth=1)])
pred_pathway.add_edge(
[pred_pathway.root_nodes.all()[0]], [methane := pred_pathway.add_node("C", depth=1)]
)
pred_pathway.add_edge([methane], [true_pathway.add_node("CC=O", depth=2)])
pred_pathway.add_edge([methane], [true_pathway.add_node("c1ccccc1", depth=2)])
return true_pathway, pred_pathway

View File

@ -10,127 +10,127 @@ class ReactionTest(TestCase):
@classmethod
def setUpClass(cls):
super(ReactionTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
def test_smoke(self):
educt = Compound.create(
self.package,
smiles='C(CCl)Cl',
name='1,2-Dichloroethane',
description='Eawag BBD compound c0001'
smiles="C(CCl)Cl",
name="1,2-Dichloroethane",
description="Eawag BBD compound c0001",
).default_structure
product = Compound.create(
self.package,
smiles='C(CO)Cl',
name='2-Chloroethanol',
description='Eawag BBD compound c0005'
smiles="C(CO)Cl",
name="2-Chloroethanol",
description="Eawag BBD compound c0005",
).default_structure
r = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
name="Eawag BBD reaction r0001",
educts=[educt],
products=[product],
multi_step=False
multi_step=False,
)
self.assertEqual(r.smirks(), 'C(CCl)Cl>>C(CO)Cl')
self.assertEqual(r.name, 'Eawag BBD reaction r0001')
self.assertEqual(r.description, 'no description')
self.assertEqual(r.smirks(), "C(CCl)Cl>>C(CO)Cl")
self.assertEqual(r.name, "Eawag BBD reaction r0001")
self.assertEqual(r.description, "no description")
def test_string_educts_and_products(self):
r = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
educts=['C(CCl)Cl'],
products=['C(CO)Cl'],
multi_step=False
name="Eawag BBD reaction r0001",
educts=["C(CCl)Cl"],
products=["C(CO)Cl"],
multi_step=False,
)
self.assertEqual(r.smirks(), 'C(CCl)Cl>>C(CO)Cl')
self.assertEqual(r.smirks(), "C(CCl)Cl>>C(CO)Cl")
def test_missing_smiles(self):
educt = Compound.create(
self.package,
smiles='C(CCl)Cl',
name='1,2-Dichloroethane',
description='Eawag BBD compound c0001'
smiles="C(CCl)Cl",
name="1,2-Dichloroethane",
description="Eawag BBD compound c0001",
).default_structure
product = Compound.create(
self.package,
smiles='C(CO)Cl',
name='2-Chloroethanol',
description='Eawag BBD compound c0005'
smiles="C(CO)Cl",
name="2-Chloroethanol",
description="Eawag BBD compound c0005",
).default_structure
with self.assertRaises(ValueError):
_ = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
name="Eawag BBD reaction r0001",
educts=[educt],
products=[],
multi_step=False
multi_step=False,
)
with self.assertRaises(ValueError):
_ = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
name="Eawag BBD reaction r0001",
educts=[],
products=[product],
multi_step=False
multi_step=False,
)
with self.assertRaises(ValueError):
_ = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
name="Eawag BBD reaction r0001",
educts=[],
products=[],
multi_step=False
multi_step=False,
)
def test_empty_name_and_description_are_ignored(self):
r = Reaction.create(
package=self.package,
name='',
description='',
educts=['C(CCl)Cl'],
products=['C(CO)Cl'],
name="",
description="",
educts=["C(CCl)Cl"],
products=["C(CO)Cl"],
multi_step=False,
)
self.assertEqual(r.name, 'no name')
self.assertEqual(r.description, 'no description')
self.assertEqual(r.name, "no name")
self.assertEqual(r.description, "no description")
def test_deduplication(self):
rule = Rule.create(
package=self.package,
rule_type='SimpleAmbitRule',
name='bt0022-2833',
description='Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative',
smirks='[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]',
rule_type="SimpleAmbitRule",
name="bt0022-2833",
description="Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative",
smirks="[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
)
r1 = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
educts=['C(CCl)Cl'],
products=['C(CO)Cl'],
name="Eawag BBD reaction r0001",
educts=["C(CCl)Cl"],
products=["C(CO)Cl"],
rules=[rule],
multi_step=False
multi_step=False,
)
r2 = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
educts=['C(CCl)Cl'],
products=['C(CO)Cl'],
name="Eawag BBD reaction r0001",
educts=["C(CCl)Cl"],
products=["C(CO)Cl"],
rules=[rule],
multi_step=False
multi_step=False,
)
# Check if create detects that this Compound already exist
@ -141,18 +141,18 @@ class ReactionTest(TestCase):
def test_deduplication_without_rules(self):
r1 = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
educts=['C(CCl)Cl'],
products=['C(CO)Cl'],
multi_step=False
name="Eawag BBD reaction r0001",
educts=["C(CCl)Cl"],
products=["C(CO)Cl"],
multi_step=False,
)
r2 = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
educts=['C(CCl)Cl'],
products=['C(CO)Cl'],
multi_step=False
name="Eawag BBD reaction r0001",
educts=["C(CCl)Cl"],
products=["C(CO)Cl"],
multi_step=False,
)
# Check if create detects that this Compound already exist
@ -164,19 +164,19 @@ class ReactionTest(TestCase):
with self.assertRaises(ValueError):
_ = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
educts=['ASDF'],
products=['C(CO)Cl'],
multi_step=False
name="Eawag BBD reaction r0001",
educts=["ASDF"],
products=["C(CO)Cl"],
multi_step=False,
)
def test_delete(self):
r = Reaction.create(
package=self.package,
name='Eawag BBD reaction r0001',
educts=['C(CCl)Cl'],
products=['C(CO)Cl'],
multi_step=False
name="Eawag BBD reaction r0001",
educts=["C(CCl)Cl"],
products=["C(CO)Cl"],
multi_step=False,
)
r.delete()

View File

@ -10,73 +10,79 @@ class RuleTest(TestCase):
@classmethod
def setUpClass(cls):
super(RuleTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
def test_smoke(self):
r = Rule.create(
rule_type='SimpleAmbitRule',
rule_type="SimpleAmbitRule",
package=self.package,
name='bt0022-2833',
description='Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative',
smirks='[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]',
name="bt0022-2833",
description="Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative",
smirks="[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
)
self.assertEqual(r.smirks,
'[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]')
self.assertEqual(r.name, 'bt0022-2833')
self.assertEqual(r.description,
'Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative')
self.assertEqual(
r.smirks,
"[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
)
self.assertEqual(r.name, "bt0022-2833")
self.assertEqual(
r.description,
"Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative",
)
def test_smirks_are_trimmed(self):
r = Rule.create(
rule_type='SimpleAmbitRule',
rule_type="SimpleAmbitRule",
package=self.package,
name='bt0022-2833',
description='Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative',
smirks=' [H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4] ',
name="bt0022-2833",
description="Dihalomethyl derivative + Halomethyl derivative > 1-Halo-1-methylalcohol derivative + 1-Methylalcohol derivative",
smirks=" [H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4] ",
)
self.assertEqual(r.smirks,
'[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]')
self.assertEqual(
r.smirks,
"[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
)
def test_name_and_description_optional(self):
r = Rule.create(
rule_type='SimpleAmbitRule',
rule_type="SimpleAmbitRule",
package=self.package,
smirks='[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]',
smirks="[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
)
self.assertRegex(r.name, 'Rule \\d+')
self.assertEqual(r.description, 'no description')
self.assertRegex(r.name, "Rule \\d+")
self.assertEqual(r.description, "no description")
def test_empty_name_and_description_are_ignored(self):
r = Rule.create(
rule_type='SimpleAmbitRule',
rule_type="SimpleAmbitRule",
package=self.package,
smirks='[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]',
name='',
description='',
smirks="[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
name="",
description="",
)
self.assertRegex(r.name, 'Rule \\d+')
self.assertEqual(r.description, 'no description')
self.assertRegex(r.name, "Rule \\d+")
self.assertEqual(r.description, "no description")
def test_deduplication(self):
r1 = Rule.create(
rule_type='SimpleAmbitRule',
rule_type="SimpleAmbitRule",
package=self.package,
smirks='[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]',
name='',
description='',
smirks="[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
name="",
description="",
)
r2 = Rule.create(
rule_type='SimpleAmbitRule',
rule_type="SimpleAmbitRule",
package=self.package,
smirks='[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]',
name='',
description='',
smirks="[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
name="",
description="",
)
self.assertEqual(r1.pk, r2.pk)
@ -84,21 +90,21 @@ class RuleTest(TestCase):
def test_valid_smirks(self):
with self.assertRaises(ValueError):
r = Rule.create(
rule_type='SimpleAmbitRule',
Rule.create(
rule_type="SimpleAmbitRule",
package=self.package,
smirks='This is not a valid SMIRKS',
name='',
description='',
smirks="This is not a valid SMIRKS",
name="",
description="",
)
def test_delete(self):
r = Rule.create(
rule_type='SimpleAmbitRule',
rule_type="SimpleAmbitRule",
package=self.package,
smirks='[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]',
name='',
description='',
smirks="[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
name="",
description="",
)
r.delete()

File diff suppressed because it is too large Load Diff

View File

@ -12,34 +12,32 @@ class SimpleAmbitRuleTest(TestCase):
@classmethod
def setUpClass(cls):
super(SimpleAmbitRuleTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Simple Ambit Rule Test Package',
'Test Package for SimpleAmbitRule')
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(
cls.user, "Simple Ambit Rule Test Package", "Test Package for SimpleAmbitRule"
)
def test_create_basic_rule(self):
"""Test creating a basic SimpleAmbitRule with minimal parameters."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks
)
rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
self.assertIsInstance(rule, SimpleAmbitRule)
self.assertEqual(rule.smirks, smirks)
self.assertEqual(rule.package, self.package)
self.assertRegex(rule.name, r'Rule \d+')
self.assertEqual(rule.description, 'no description')
self.assertRegex(rule.name, r"Rule \d+")
self.assertEqual(rule.description, "no description")
self.assertIsNone(rule.reactant_filter_smarts)
self.assertIsNone(rule.product_filter_smarts)
def test_create_with_all_parameters(self):
"""Test creating SimpleAmbitRule with all parameters."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
name = 'Test Rule'
description = 'A test biotransformation rule'
reactant_filter = '[CH2X4]'
product_filter = '[OH]'
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
name = "Test Rule"
description = "A test biotransformation rule"
reactant_filter = "[CH2X4]"
product_filter = "[OH]"
rule = SimpleAmbitRule.create(
package=self.package,
@ -47,7 +45,7 @@ class SimpleAmbitRuleTest(TestCase):
description=description,
smirks=smirks,
reactant_filter_smarts=reactant_filter,
product_filter_smarts=product_filter
product_filter_smarts=product_filter,
)
self.assertEqual(rule.name, name)
@ -60,127 +58,114 @@ class SimpleAmbitRuleTest(TestCase):
"""Test that SMIRKS is required for rule creation."""
with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(package=self.package, smirks=None)
self.assertIn('SMIRKS is required', str(cm.exception))
self.assertIn("SMIRKS is required", str(cm.exception))
with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(package=self.package, smirks='')
self.assertIn('SMIRKS is required', str(cm.exception))
SimpleAmbitRule.create(package=self.package, smirks="")
self.assertIn("SMIRKS is required", str(cm.exception))
with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(package=self.package, smirks=' ')
self.assertIn('SMIRKS is required', str(cm.exception))
SimpleAmbitRule.create(package=self.package, smirks=" ")
self.assertIn("SMIRKS is required", str(cm.exception))
@patch('epdb.models.FormatConverter.is_valid_smirks')
@patch("epdb.models.FormatConverter.is_valid_smirks")
def test_invalid_smirks_validation(self, mock_is_valid):
"""Test validation of SMIRKS format."""
mock_is_valid.return_value = False
invalid_smirks = 'invalid_smirks_string'
invalid_smirks = "invalid_smirks_string"
with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(
package=self.package,
smirks=invalid_smirks
)
SimpleAmbitRule.create(package=self.package, smirks=invalid_smirks)
self.assertIn(f'SMIRKS "{invalid_smirks}" is invalid', str(cm.exception))
mock_is_valid.assert_called_once_with(invalid_smirks)
def test_smirks_trimming(self):
"""Test that SMIRKS strings are trimmed during creation."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
smirks_with_whitespace = f' {smirks} '
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
smirks_with_whitespace = f" {smirks} "
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks_with_whitespace
)
rule = SimpleAmbitRule.create(package=self.package, smirks=smirks_with_whitespace)
self.assertEqual(rule.smirks, smirks)
def test_empty_name_and_description_handling(self):
"""Test that empty name and description are handled appropriately."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
name='',
description=' '
package=self.package, smirks=smirks, name="", description=" "
)
self.assertRegex(rule.name, r'Rule \d+')
self.assertEqual(rule.description, 'no description')
self.assertRegex(rule.name, r"Rule \d+")
self.assertEqual(rule.description, "no description")
def test_deduplication_basic(self):
"""Test that identical rules are deduplicated."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
rule1 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
name='Rule 1'
)
rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks, name="Rule 1")
rule2 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
name='Rule 2' # Different name, but same SMIRKS
name="Rule 2", # Different name, but same SMIRKS
)
self.assertEqual(rule1.pk, rule2.pk)
self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 1)
self.assertEqual(
SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 1
)
def test_deduplication_with_filters(self):
"""Test deduplication with filter SMARTS."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
reactant_filter = '[CH2X4]'
product_filter = '[OH]'
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
reactant_filter = "[CH2X4]"
product_filter = "[OH]"
rule1 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts=reactant_filter,
product_filter_smarts=product_filter
product_filter_smarts=product_filter,
)
rule2 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts=reactant_filter,
product_filter_smarts=product_filter
product_filter_smarts=product_filter,
)
self.assertEqual(rule1.pk, rule2.pk)
def test_no_deduplication_different_filters(self):
"""Test that rules with different filters are not deduplicated."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
rule1 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts='[CH2X4]'
package=self.package, smirks=smirks, reactant_filter_smarts="[CH2X4]"
)
rule2 = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts='[CH3X4]'
package=self.package, smirks=smirks, reactant_filter_smarts="[CH3X4]"
)
self.assertNotEqual(rule1.pk, rule2.pk)
self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 2)
self.assertEqual(
SimpleAmbitRule.objects.filter(package=self.package, smirks=smirks).count(), 2
)
def test_filter_smarts_trimming(self):
"""Test that filter SMARTS are trimmed and handled correctly."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
# Test with whitespace-only filters (should be treated as None)
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks,
reactant_filter_smarts=' ',
product_filter_smarts=' '
reactant_filter_smarts=" ",
product_filter_smarts=" ",
)
self.assertIsNone(rule.reactant_filter_smarts)
@ -188,94 +173,85 @@ class SimpleAmbitRuleTest(TestCase):
def test_url_property(self):
"""Test the URL property generation."""
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
expected_url = f'{self.package.url}/simple-ambit-rule/{rule.uuid}'
expected_url = f"{self.package.url}/simple-ambit-rule/{rule.uuid}"
self.assertEqual(rule.url, expected_url)
@patch('epdb.models.FormatConverter.apply')
@patch("epdb.models.FormatConverter.apply")
def test_apply_method(self, mock_apply):
"""Test the apply method delegates to FormatConverter."""
mock_apply.return_value = ['product1', 'product2']
mock_apply.return_value = ["product1", "product2"]
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
test_smiles = 'CCO'
test_smiles = "CCO"
result = rule.apply(test_smiles)
mock_apply.assert_called_once_with(test_smiles, rule.smirks)
self.assertEqual(result, ['product1', 'product2'])
self.assertEqual(result, ["product1", "product2"])
def test_reactants_smarts_property(self):
"""Test reactants_smarts property extracts correct part of SMIRKS."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
expected_reactants = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]'
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
expected_reactants = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]"
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks
)
rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
self.assertEqual(rule.reactants_smarts, expected_reactants)
def test_products_smarts_property(self):
"""Test products_smarts property extracts correct part of SMIRKS."""
smirks = '[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
expected_products = '[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]'
smirks = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
expected_products = "[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
rule = SimpleAmbitRule.create(
package=self.package,
smirks=smirks
)
rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
self.assertEqual(rule.products_smarts, expected_products)
@patch('epdb.models.Package.objects')
@patch("epdb.models.Package.objects")
def test_related_reactions_property(self, mock_package_objects):
"""Test related_reactions property returns correct queryset."""
mock_qs = MagicMock()
mock_package_objects.filter.return_value = mock_qs
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
# Instead of directly assigning, patch the property or use with patch.object
with patch.object(type(rule), 'reaction_rule', new_callable=PropertyMock) as mock_reaction_rule:
mock_reaction_rule.return_value.filter.return_value.order_by.return_value = ['reaction1', 'reaction2']
with patch.object(
type(rule), "reaction_rule", new_callable=PropertyMock
) as mock_reaction_rule:
mock_reaction_rule.return_value.filter.return_value.order_by.return_value = [
"reaction1",
"reaction2",
]
result = rule.related_reactions
mock_package_objects.filter.assert_called_once_with(reviewed=True)
mock_reaction_rule.return_value.filter.assert_called_once_with(package__in=mock_qs)
mock_reaction_rule.return_value.filter.return_value.order_by.assert_called_once_with('name')
self.assertEqual(result, ['reaction1', 'reaction2'])
mock_reaction_rule.return_value.filter.return_value.order_by.assert_called_once_with(
"name"
)
self.assertEqual(result, ["reaction1", "reaction2"])
@patch('epdb.models.Pathway.objects')
@patch('epdb.models.Edge.objects')
@patch("epdb.models.Pathway.objects")
@patch("epdb.models.Edge.objects")
def test_related_pathways_property(self, mock_edge_objects, mock_pathway_objects):
"""Test related_pathways property returns correct queryset."""
mock_related_reactions = ['reaction1', 'reaction2']
mock_related_reactions = ["reaction1", "reaction2"]
with patch.object(SimpleAmbitRule, "related_reactions", new_callable=PropertyMock) as mock_prop:
with patch.object(
SimpleAmbitRule, "related_reactions", new_callable=PropertyMock
) as mock_prop:
mock_prop.return_value = mock_related_reactions
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
# Mock Edge objects query
mock_edge_values = MagicMock()
mock_edge_values.values.return_value = ['pathway_id1', 'pathway_id2']
mock_edge_values.values.return_value = ["pathway_id1", "pathway_id2"]
mock_edge_objects.filter.return_value = mock_edge_values
# Mock Pathway objects query
@ -285,52 +261,49 @@ class SimpleAmbitRuleTest(TestCase):
result = rule.related_pathways
mock_edge_objects.filter.assert_called_once_with(edge_label__in=mock_related_reactions)
mock_edge_values.values.assert_called_once_with('pathway_id')
mock_edge_values.values.assert_called_once_with("pathway_id")
mock_pathway_objects.filter.assert_called_once()
self.assertEqual(result, mock_pathway_qs)
@patch('epdb.models.IndigoUtils.smirks_to_svg')
@patch("epdb.models.IndigoUtils.smirks_to_svg")
def test_as_svg_property(self, mock_smirks_to_svg):
"""Test as_svg property calls IndigoUtils correctly."""
mock_smirks_to_svg.return_value = '<svg>test_svg</svg>'
mock_smirks_to_svg.return_value = "<svg>test_svg</svg>"
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
result = rule.as_svg
mock_smirks_to_svg.assert_called_once_with(rule.smirks, True, width=800, height=400)
self.assertEqual(result, '<svg>test_svg</svg>')
self.assertEqual(result, "<svg>test_svg</svg>")
def test_atomic_transaction(self):
"""Test that rule creation is atomic."""
smirks = '[H:1][C:2]>>[H:1][O:2]'
smirks = "[H:1][C:2]>>[H:1][O:2]"
# This should work normally
rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
self.assertIsInstance(rule, SimpleAmbitRule)
# Test transaction rollback on error
with patch('epdb.models.SimpleAmbitRule.save', side_effect=Exception('Database error')):
with patch("epdb.models.SimpleAmbitRule.save", side_effect=Exception("Database error")):
with self.assertRaises(Exception):
SimpleAmbitRule.create(package=self.package, smirks='[H:3][C:4]>>[H:3][O:4]')
SimpleAmbitRule.create(package=self.package, smirks="[H:3][C:4]>>[H:3][O:4]")
# Verify no partial data was saved
self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package).count(), 1)
def test_multiple_duplicate_warning(self):
"""Test logging when multiple duplicates are found."""
smirks = '[H:1][C:2]>>[H:1][O:2]'
smirks = "[H:1][C:2]>>[H:1][O:2]"
# Create first rule
rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks)
# Manually create a duplicate to simulate the error condition
rule2 = SimpleAmbitRule(package=self.package, smirks=smirks, name='Manual Rule')
rule2 = SimpleAmbitRule(package=self.package, smirks=smirks, name="Manual Rule")
rule2.save()
with patch('epdb.models.logger') as mock_logger:
with patch("epdb.models.logger") as mock_logger:
# This should find the existing rule and log an error about multiple matches
result = SimpleAmbitRule.create(package=self.package, smirks=smirks)
@ -339,24 +312,28 @@ class SimpleAmbitRuleTest(TestCase):
# Should log an error about multiple matches
mock_logger.error.assert_called()
self.assertIn('More than one rule matched', mock_logger.error.call_args[0][0])
self.assertIn("More than one rule matched", mock_logger.error.call_args[0][0])
def test_model_fields(self):
"""Test model field properties."""
rule = SimpleAmbitRule.create(
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]',
reactant_filter_smarts='[CH3]',
product_filter_smarts='[OH]'
smirks="[H:1][C:2]>>[H:1][O:2]",
reactant_filter_smarts="[CH3]",
product_filter_smarts="[OH]",
)
# Test field properties
self.assertFalse(rule._meta.get_field('smirks').blank)
self.assertFalse(rule._meta.get_field('smirks').null)
self.assertTrue(rule._meta.get_field('reactant_filter_smarts').null)
self.assertTrue(rule._meta.get_field('product_filter_smarts').null)
self.assertFalse(rule._meta.get_field("smirks").blank)
self.assertFalse(rule._meta.get_field("smirks").null)
self.assertTrue(rule._meta.get_field("reactant_filter_smarts").null)
self.assertTrue(rule._meta.get_field("product_filter_smarts").null)
# Test verbose names
self.assertEqual(rule._meta.get_field('smirks').verbose_name, 'SMIRKS')
self.assertEqual(rule._meta.get_field('reactant_filter_smarts').verbose_name, 'Reactant Filter SMARTS')
self.assertEqual(rule._meta.get_field('product_filter_smarts').verbose_name, 'Product Filter SMARTS')
self.assertEqual(rule._meta.get_field("smirks").verbose_name, "SMIRKS")
self.assertEqual(
rule._meta.get_field("reactant_filter_smarts").verbose_name, "Reactant Filter SMARTS"
)
self.assertEqual(
rule._meta.get_field("product_filter_smarts").verbose_name, "Product Filter SMARTS"
)

View File

@ -1,32 +1,29 @@
from django.test import TestCase
from epdb.logic import SNode, SEdge, SPathway
from epdb.logic import SNode, SEdge
class SObjectTest(TestCase):
def setUp(self):
pass
def test_snode_eq(self):
snode1 = SNode('CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O', 0)
snode2 = SNode('CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O', 0)
snode1 = SNode("CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O", 0)
snode2 = SNode("CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O", 0)
assert snode1 == snode2
def test_snode_hash(self):
pass
def test_sedge_eq(self):
sedge1 = SEdge([SNode('CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O', 0)],
[SNode('CN1C(=O)NC2=C(C1=O)N(C)C=N2', 1), SNode('C=O', 1)],
rule=None)
sedge2 = SEdge([SNode('CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O', 0)],
[SNode('CN1C(=O)NC2=C(C1=O)N(C)C=N2', 1), SNode('C=O', 1)],
rule=None)
sedge1 = SEdge(
[SNode("CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O", 0)],
[SNode("CN1C(=O)NC2=C(C1=O)N(C)C=N2", 1), SNode("C=O", 1)],
rule=None,
)
sedge2 = SEdge(
[SNode("CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O", 0)],
[SNode("CN1C(=O)NC2=C(C1=O)N(C)C=N2", 1), SNode("C=O", 1)],
rule=None,
)
assert sedge1 == sedge2
def test_sedge_hash(self):
pass
def test_spathway(self):
pw = SPathway()

View File

@ -0,0 +1,396 @@
from django.test import TestCase
from django.urls import reverse
from envipy_additional_information import Temperature, Interval
from epdb.logic import UserManager, PackageManager
from epdb.models import Compound, Scenario, ExternalDatabase
class CompoundViewTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"]
@classmethod
def setUpClass(cls):
super(CompoundViewTest, cls).setUpClass()
cls.user1 = UserManager.create_user(
"user1",
"user1@envipath.com",
"SuperSafe",
set_setting=False,
add_to_group=True,
is_active=True,
)
cls.user1_default_package = cls.user1.default_package
cls.package = PackageManager.create_package(cls.user1, "Test", "Test Pack")
def setUp(self):
self.client.force_login(self.user1)
def test_create_compound(self):
response = self.client.post(
reverse("compounds"),
{
"compound-name": "1,2-Dichloroethane",
"compound-description": "Eawag BBD compound c0001",
"compound-smiles": "C(CCl)Cl",
},
)
self.assertEqual(response.status_code, 302)
compound_url = response.url
c = Compound.objects.get(url=compound_url)
self.assertEqual(c.package, self.user1_default_package)
self.assertEqual(c.name, "1,2-Dichloroethane")
self.assertEqual(c.description, "Eawag BBD compound c0001")
self.assertEqual(c.default_structure.smiles, "C(CCl)Cl")
self.assertEqual(c.default_structure.canonical_smiles, "ClCCCl")
self.assertEqual(c.structures.all().count(), 2)
self.assertEqual(self.user1_default_package.compounds.count(), 1)
# Adding the same rule again should return the existing one, hence not increasing the number of rules
response = self.client.post(
reverse("compounds"),
{
"compound-name": "1,2-Dichloroethane",
"compound-description": "Eawag BBD compound c0001",
"compound-smiles": "C(CCl)Cl",
},
)
self.assertEqual(response.url, compound_url)
self.assertEqual(response.status_code, 302)
self.assertEqual(self.user1_default_package.compounds.count(), 1)
# Adding the same rule in a different package should create a new rule
response = self.client.post(
reverse("package compound list", kwargs={"package_uuid": self.package.uuid}),
{
"compound-name": "1,2-Dichloroethane",
"compound-description": "Eawag BBD compound c0001",
"compound-smiles": "C(CCl)Cl",
},
)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(compound_url, response.url)
# adding another reaction should increase count
response = self.client.post(
reverse("compounds"),
{
"compound-name": "2-Chloroethanol",
"compound-description": "Eawag BBD compound c0005",
"compound-smiles": "C(CO)Cl",
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(self.user1_default_package.compounds.count(), 2)
# Edit
def test_edit_rule(self):
response = self.client.post(
reverse("compounds"),
{
"compound-name": "1,2-Dichloroethane",
"compound-description": "Eawag BBD compound c0001",
"compound-smiles": "C(CCl)Cl",
},
)
self.assertEqual(response.status_code, 302)
compound_url = response.url
c = Compound.objects.get(url=compound_url)
response = self.client.post(
reverse(
"package compound detail",
kwargs={
"package_uuid": str(self.user1_default_package.uuid),
"compound_uuid": str(c.uuid),
},
),
{
"compound-name": "Test Compound Adjusted",
"compound-description": "New Description",
},
)
self.assertEqual(response.status_code, 302)
c = Compound.objects.get(url=compound_url)
self.assertEqual(c.name, "Test Compound Adjusted")
self.assertEqual(c.description, "New Description")
# Rest stays untouched
self.assertEqual(c.default_structure.smiles, "C(CCl)Cl")
self.assertEqual(self.user1_default_package.compounds.count(), 1)
# Scenario
def test_set_scenario(self):
s1 = Scenario.create(
self.user1_default_package,
"Test Scen",
"Test Desc",
"2025-10",
"soil",
[Temperature(interval=Interval(start=20, end=30))],
)
s2 = Scenario.create(
self.user1_default_package,
"Test Scen2",
"Test Desc2",
"2025-10",
"soil",
[Temperature(interval=Interval(start=10, end=20))],
)
response = self.client.post(
reverse("compounds"),
{
"compound-name": "1,2-Dichloroethane",
"compound-description": "Eawag BBD compound c0001",
"compound-smiles": "C(CCl)Cl",
},
)
self.assertEqual(response.status_code, 302)
compound_url = response.url
c = Compound.objects.get(url=compound_url)
response = self.client.post(
reverse(
"package compound detail",
kwargs={"package_uuid": str(c.package.uuid), "compound_uuid": str(c.uuid)},
),
{"selected-scenarios": [s1.url, s2.url]},
)
self.assertEqual(len(c.scenarios.all()), 2)
response = self.client.post(
reverse(
"package compound detail",
kwargs={"package_uuid": str(c.package.uuid), "compound_uuid": str(c.uuid)},
),
{"selected-scenarios": [s1.url]},
)
self.assertEqual(len(c.scenarios.all()), 1)
self.assertEqual(c.scenarios.first().url, s1.url)
response = self.client.post(
reverse(
"package compound detail",
kwargs={"package_uuid": str(c.package.uuid), "compound_uuid": str(c.uuid)},
),
{
# We have to set an empty string to avoid that the parameter is removed
"selected-scenarios": ""
},
)
self.assertEqual(len(c.scenarios.all()), 0)
#
def test_copy(self):
response = self.client.post(
reverse("compounds"),
{
"compound-name": "1,2-Dichloroethane",
"compound-description": "Eawag BBD compound c0001",
"compound-smiles": "C(CCl)Cl",
},
)
self.assertEqual(response.status_code, 302)
compound_url = response.url
c = Compound.objects.get(url=compound_url)
response = self.client.post(
reverse(
"package detail",
kwargs={
"package_uuid": str(self.package.uuid),
},
),
{"hidden": "copy", "object_to_copy": c.url},
)
self.assertEqual(response.status_code, 200)
copied_object_url = response.json()["success"]
copied_compound = Compound.objects.get(url=copied_object_url)
self.assertEqual(copied_compound.name, c.name)
self.assertEqual(copied_compound.description, c.description)
self.assertEqual(copied_compound.default_structure.smiles, c.default_structure.smiles)
# Copy to the same package should fail
response = self.client.post(
reverse(
"package detail",
kwargs={
"package_uuid": str(c.package.uuid),
},
),
{"hidden": "copy", "object_to_copy": c.url},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.json()["error"], f"Can't copy object {compound_url} to the same package!"
)
def test_references(self):
ext_db, _ = ExternalDatabase.objects.get_or_create(
name="PubChem Compound",
defaults={
"full_name": "PubChem Compound Database",
"description": "Chemical database of small organic molecules",
"base_url": "https://pubchem.ncbi.nlm.nih.gov",
"url_pattern": "https://pubchem.ncbi.nlm.nih.gov/compound/{id}",
},
)
ext_db2, _ = ExternalDatabase.objects.get_or_create(
name="PubChem Substance",
defaults={
"full_name": "PubChem Substance Database",
"description": "Database of chemical substances",
"base_url": "https://pubchem.ncbi.nlm.nih.gov",
"url_pattern": "https://pubchem.ncbi.nlm.nih.gov/substance/{id}",
},
)
response = self.client.post(
reverse("compounds"),
{
"compound-name": "1,2-Dichloroethane",
"compound-description": "Eawag BBD compound c0001",
"compound-smiles": "C(CCl)Cl",
},
)
self.assertEqual(response.status_code, 302)
compound_url = response.url
c = Compound.objects.get(url=compound_url)
response = self.client.post(
reverse(
"package compound detail",
kwargs={
"package_uuid": str(c.package.uuid),
"compound_uuid": str(c.uuid),
},
),
{"selected-database": ext_db.pk, "identifier": "25154249"},
)
self.assertEqual(c.external_identifiers.count(), 1)
self.assertEqual(c.external_identifiers.first().database, ext_db)
self.assertEqual(c.external_identifiers.first().identifier_value, "25154249")
self.assertEqual(
c.external_identifiers.first().url, "https://pubchem.ncbi.nlm.nih.gov/compound/25154249"
)
response = self.client.post(
reverse(
"package compound detail",
kwargs={
"package_uuid": str(c.package.uuid),
"compound_uuid": str(c.uuid),
},
),
{"selected-database": ext_db2.pk, "identifier": "25154249"},
)
self.assertEqual(c.external_identifiers.count(), 2)
self.assertEqual(c.external_identifiers.last().database, ext_db2)
self.assertEqual(c.external_identifiers.last().identifier_value, "25154249")
self.assertEqual(
c.external_identifiers.last().url, "https://pubchem.ncbi.nlm.nih.gov/substance/25154249"
)
def test_delete(self):
response = self.client.post(
reverse("compounds"),
{
"compound-name": "1,2-Dichloroethane",
"compound-description": "Eawag BBD compound c0001",
"compound-smiles": "C(CCl)Cl",
},
)
self.assertEqual(response.status_code, 302)
compound_url = response.url
c = Compound.objects.get(url=compound_url)
response = self.client.post(
reverse(
"package compound detail",
kwargs={"package_uuid": str(c.package.uuid), "compound_uuid": str(c.uuid)},
),
{"hidden": "delete"},
)
self.assertEqual(self.user1_default_package.compounds.count(), 0)
def test_set_aliases(self):
alias_1 = "Alias 1"
alias_2 = "Alias 2"
response = self.client.post(
reverse("compounds"),
{
"compound-name": "1,2-Dichloroethane",
"compound-description": "Eawag BBD compound c0001",
"compound-smiles": "C(CCl)Cl",
},
)
self.assertEqual(response.status_code, 302)
compound_url = response.url
c = Compound.objects.get(url=compound_url)
response = self.client.post(
reverse(
"package compound detail",
kwargs={"package_uuid": str(c.package.uuid), "compound_uuid": str(c.uuid)},
),
{"aliases": [alias_1, alias_2]},
)
c = Compound.objects.get(url=compound_url)
self.assertEqual(len(c.aliases), 2)
response = self.client.post(
reverse(
"package compound detail",
kwargs={"package_uuid": str(c.package.uuid), "compound_uuid": str(c.uuid)},
),
{"aliases": [alias_1]},
)
c = Compound.objects.get(url=compound_url)
self.assertEqual(len(c.aliases), 1)
response = self.client.post(
reverse(
"package compound detail",
kwargs={"package_uuid": str(c.package.uuid), "compound_uuid": str(c.uuid)},
),
{
# We have to set an empty string to avoid that the parameter is removed
"aliases": ""
},
)
c = Compound.objects.get(url=compound_url)
self.assertEqual(len(c.aliases), 0)

View File

@ -0,0 +1,125 @@
from django.test import TestCase, override_settings
from django.urls import reverse
from django.conf import settings as s
from epdb.logic import UserManager
from epdb.models import Package, User
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models")
class PathwayViewTest(TestCase):
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod
def setUpClass(cls):
super(PathwayViewTest, cls).setUpClass()
cls.user1 = UserManager.create_user(
"user1",
"user1@envipath.com",
"SuperSafe",
set_setting=True,
add_to_group=True,
is_active=True,
)
cls.user1_default_package = cls.user1.default_package
cls.model_package = Package.objects.get(name="Fixtures")
def setUp(self):
self.client.force_login(self.user1)
def test_predict(self):
self.client.force_login(User.objects.get(username="admin"))
response = self.client.get(
reverse(
"package model detail",
kwargs={
"package_uuid": str(self.model_package.uuid),
"model_uuid": str(self.model_package.models.first().uuid),
},
),
{
"classify": "ILikeCats!",
"smiles": "CCN(CC)C(=O)C1=CC(=CC=C1)CO",
},
)
expected = [
{
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
"probability": 0.25,
"btrule": {
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/0e6e9290-b658-4450-b291-3ec19fa19206",
"name": "bt0430-4011",
},
},
{
"products": [["CCNC(=O)C1=CC(CO)=CC=C1", "CC=O"]],
"probability": 0.0,
"btrule": {
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/27a3a353-0b66-4228-bd16-e407949e90df",
"name": "bt0243-4301",
},
},
{
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
"probability": 0.75,
"btrule": {
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
"name": "bt0001-3568",
},
},
]
actual = response.json()
self.assertEqual(actual, expected)
response = self.client.get(
reverse(
"package model detail",
kwargs={
"package_uuid": str(self.model_package.uuid),
"model_uuid": str(self.model_package.models.first().uuid),
},
),
{
"classify": "ILikeCats!",
"smiles": "",
},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()["error"], "Received empty SMILES")
response = self.client.get(
reverse(
"package model detail",
kwargs={
"package_uuid": str(self.model_package.uuid),
"model_uuid": str(self.model_package.models.first().uuid),
},
),
{
"classify": "ILikeCats!",
"smiles": " ", # Input should be stripped
},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()["error"], "Received empty SMILES")
response = self.client.get(
reverse(
"package model detail",
kwargs={
"package_uuid": str(self.model_package.uuid),
"model_uuid": str(self.model_package.models.first().uuid),
},
),
{
"classify": "ILikeCats!",
"smiles": "RandomInput",
},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(response.json()["error"], '"RandomInput" is not a valid SMILES')

View File

@ -13,19 +13,34 @@ class PackageViewTest(TestCase):
@classmethod
def setUpClass(cls):
super(PackageViewTest, cls).setUpClass()
cls.user1 = UserManager.create_user("user1", "user1@envipath.com", "SuperSafe",
set_setting=False, add_to_group=True, is_active=True)
cls.user2 = UserManager.create_user("user2", "user2@envipath.com", "SuperSafe",
set_setting=False, add_to_group=True, is_active=True)
cls.user1 = UserManager.create_user(
"user1",
"user1@envipath.com",
"SuperSafe",
set_setting=False,
add_to_group=True,
is_active=True,
)
cls.user2 = UserManager.create_user(
"user2",
"user2@envipath.com",
"SuperSafe",
set_setting=False,
add_to_group=True,
is_active=True,
)
def setUp(self):
self.client.force_login(self.user1)
def test_create_package(self):
response = self.client.post(reverse("packages"), {
"package-name": "Test Package",
"package-description": "Just a Description",
})
response = self.client.post(
reverse("packages"),
{
"package-name": "Test Package",
"package-description": "Just a Description",
},
)
self.assertEqual(response.status_code, 302)
package_url = response.url
@ -41,13 +56,12 @@ class PackageViewTest(TestCase):
file = SimpleUploadedFile(
"Fixture_Package.json",
open(s.FIXTURE_DIRS[0] / "Fixture_Package.json", "rb").read(),
content_type="application/json"
content_type="application/json",
)
response = self.client.post(reverse("packages"), {
"file": file,
"hidden": "import-package-json"
})
response = self.client.post(
reverse("packages"), {"file": file, "hidden": "import-package-json"}
)
self.assertEqual(response.status_code, 302)
package_url = response.url
@ -67,13 +81,12 @@ class PackageViewTest(TestCase):
file = SimpleUploadedFile(
"EAWAG-BBD.json",
open(s.FIXTURE_DIRS[0] / "packages" / "2025-07-18" / "EAWAG-BBD.json", "rb").read(),
content_type="application/json"
content_type="application/json",
)
response = self.client.post(reverse("packages"), {
"file": file,
"hidden": "import-legacy-package-json"
})
response = self.client.post(
reverse("packages"), {"file": file, "hidden": "import-legacy-package-json"}
)
self.assertEqual(response.status_code, 302)
package_url = response.url
@ -90,17 +103,23 @@ class PackageViewTest(TestCase):
self.assertEqual(upp.permission, Permission.ALL[0])
def test_edit_package(self):
response = self.client.post(reverse("packages"), {
"package-name": "Test Package",
"package-description": "Just a Description",
})
response = self.client.post(
reverse("packages"),
{
"package-name": "Test Package",
"package-description": "Just a Description",
},
)
self.assertEqual(response.status_code, 302)
package_url = response.url
self.client.post(package_url, {
"package-name": "New Name",
"package-description": "New Description",
})
self.client.post(
package_url,
{
"package-name": "New Name",
"package-description": "New Description",
},
)
p = Package.objects.get(url=package_url)
@ -108,10 +127,13 @@ class PackageViewTest(TestCase):
self.assertEqual(p.description, "New Description")
def test_edit_package_permissions(self):
response = self.client.post(reverse("packages"), {
"package-name": "Test Package",
"package-description": "Just a Description",
})
response = self.client.post(
reverse("packages"),
{
"package-name": "Test Package",
"package-description": "Just a Description",
},
)
self.assertEqual(response.status_code, 302)
package_url = response.url
p = Package.objects.get(url=package_url)
@ -119,57 +141,63 @@ class PackageViewTest(TestCase):
with self.assertRaises(UserPackagePermission.DoesNotExist):
UserPackagePermission.objects.get(package=p, user=self.user2)
self.client.post(package_url, {
"grantee": self.user2.url,
"read": "on",
"write": "on",
})
self.client.post(
package_url,
{
"grantee": self.user2.url,
"read": "on",
"write": "on",
},
)
upp = UserPackagePermission.objects.get(package=p, user=self.user2)
self.assertEqual(upp.permission, Permission.WRITE[0])
def test_publish_package(self):
response = self.client.post(reverse("packages"), {
"package-name": "Test Package",
"package-description": "Just a Description",
})
response = self.client.post(
reverse("packages"),
{
"package-name": "Test Package",
"package-description": "Just a Description",
},
)
self.assertEqual(response.status_code, 302)
package_url = response.url
p = Package.objects.get(url=package_url)
self.client.post(package_url, {
"hidden": "publish-package"
})
self.client.post(package_url, {"hidden": "publish-package"})
self.assertEqual(Group.objects.filter(public=True).count(), 1)
g = Group.objects.get(public=True)
gpp = GroupPackagePermission.objects.get(package=p, group=g)
self.assertEqual(gpp.permission, Permission.READ[0])
def test_set_package_license(self):
response = self.client.post(reverse("packages"), {
"package-name": "Test Package",
"package-description": "Just a Description",
})
response = self.client.post(
reverse("packages"),
{
"package-name": "Test Package",
"package-description": "Just a Description",
},
)
package_url = response.url
p = Package.objects.get(url=package_url)
self.client.post(package_url, {
"license": "no-license"
})
self.client.post(package_url, {"license": "no-license"})
self.assertIsNone(p.license)
# TODO test others
def test_delete_package(self):
response = self.client.post(reverse("packages"), {
"package-name": "Test Package",
"package-description": "Just a Description",
})
response = self.client.post(
reverse("packages"),
{
"package-name": "Test Package",
"package-description": "Just a Description",
},
)
package_url = response.url
p = Package.objects.get(url=package_url)
@ -178,3 +206,15 @@ class PackageViewTest(TestCase):
response = self.client.get(package_url)
self.assertEqual(response.status_code, 404)
def test_delete_default_package(self):
self.client.force_login(self.user1)
# Try to delete the default package
response = self.client.post(self.user1.default_package.url, {"hidden": "delete"})
self.assertEqual(response.status_code, 400)
self.assertTrue(
"You cannot delete the default package. "
"If you want to delete this package you have to "
"set another default package first" in response.content.decode()
)

View File

@ -0,0 +1,168 @@
from django.test import TestCase, override_settings
from django.urls import reverse
from django.conf import settings as s
from epdb.logic import UserManager, PackageManager
from epdb.models import Pathway, Edge
@override_settings(MODEL_DIR=s.FIXTURE_DIRS[0] / "models")
class PathwayViewTest(TestCase):
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
@classmethod
def setUpClass(cls):
super(PathwayViewTest, cls).setUpClass()
cls.user1 = UserManager.create_user(
"user1",
"user1@envipath.com",
"SuperSafe",
set_setting=True,
add_to_group=True,
is_active=True,
)
cls.user1_default_package = cls.user1.default_package
cls.package = PackageManager.create_package(cls.user1, "Test", "Test Pack")
def setUp(self):
self.client.force_login(self.user1)
def test_predict_pathway(self):
response = self.client.post(
reverse("pathways"),
{
"name": "Test Pathway",
"description": "Just a Description",
"predict": "predict",
"smiles": "CCN(CC)C(=O)C1=CC(=CC=C1)CO",
},
)
self.assertEqual(response.status_code, 302)
pathway_url = response.url
pw = Pathway.objects.get(url=pathway_url)
self.assertEqual(self.user1_default_package, pw.package)
self.assertEqual(pw.name, "Test Pathway")
self.assertEqual(pw.description, "Just a Description")
self.assertEqual(len(pw.root_nodes), 1)
self.assertEqual(
pw.root_nodes.first().default_node_label.smiles, "CCN(CC)C(=O)C1=CC(CO)=CC=C1"
)
first_level_nodes = {
# Edge 1
"CCN(CC)C(=O)C1=CC(C=O)=CC=C1",
# Edge 2
"CCNC(=O)C1=CC(CO)=CC=C1",
"CC=O",
# Edge 3
"CCNCC",
"O=C(O)C1=CC(CO)=CC=C1",
}
predicted_nodes = set()
edges = Edge.objects.filter(start_nodes__in=[pw.root_nodes.first()])
for edge in edges:
for n in edge.end_nodes.all():
predicted_nodes.add(n.default_node_label.smiles)
self.assertEqual(first_level_nodes, predicted_nodes)
def test_predict_package_pathway(self):
response = self.client.post(
reverse("package pathway list", kwargs={"package_uuid": str(self.package.uuid)}),
{
"name": "Test Pathway",
"description": "Just a Description",
"predict": "predict",
"smiles": "CCN(CC)C(=O)C1=CC(=CC=C1)CO",
},
)
self.assertEqual(response.status_code, 302)
pathway_url = response.url
pw = Pathway.objects.get(url=pathway_url)
self.assertEqual(self.package, pw.package)
self.assertEqual(pw.name, "Test Pathway")
self.assertEqual(pw.description, "Just a Description")
self.assertEqual(len(pw.root_nodes), 1)
self.assertEqual(
pw.root_nodes.first().default_node_label.smiles, "CCN(CC)C(=O)C1=CC(CO)=CC=C1"
)
first_level_nodes = {
# Edge 1
"CCN(CC)C(=O)C1=CC(C=O)=CC=C1",
# Edge 2
"CCNC(=O)C1=CC(CO)=CC=C1",
"CC=O",
# Edge 3
"CCNCC",
"O=C(O)C1=CC(CO)=CC=C1",
}
predicted_nodes = set()
edges = Edge.objects.filter(start_nodes__in=[pw.root_nodes.first()])
for edge in edges:
for n in edge.end_nodes.all():
predicted_nodes.add(n.default_node_label.smiles)
self.assertEqual(first_level_nodes, predicted_nodes)
def test_set_aliases(self):
alias_1 = "Alias 1"
alias_2 = "Alias 2"
response = self.client.post(
reverse("package pathway list", kwargs={"package_uuid": str(self.package.uuid)}),
{
"name": "Test Pathway",
"description": "Just a Description",
"predict": "predict",
"smiles": "CCN(CC)C(=O)C1=CC(=CC=C1)CO",
},
)
self.assertEqual(response.status_code, 302)
pathway_url = response.url
pw = Pathway.objects.get(url=pathway_url)
response = self.client.post(
reverse(
"package pathway detail",
kwargs={"package_uuid": str(pw.package.uuid), "pathway_uuid": str(pw.uuid)},
),
{"aliases": [alias_1, alias_2]},
)
pw = Pathway.objects.get(url=pathway_url)
self.assertEqual(len(pw.aliases), 2)
response = self.client.post(
reverse(
"package pathway detail",
kwargs={"package_uuid": str(pw.package.uuid), "pathway_uuid": str(pw.uuid)},
),
{"aliases": [alias_1]},
)
pw = Pathway.objects.get(url=pathway_url)
self.assertEqual(len(pw.aliases), 1)
response = self.client.post(
reverse(
"package pathway detail",
kwargs={"package_uuid": str(pw.package.uuid), "pathway_uuid": str(pw.uuid)},
),
{
# We have to set an empty string to avoid that the parameter is removed
"aliases": ""
},
)
pw = Pathway.objects.get(url=pathway_url)
self.assertEqual(len(pw.aliases), 0)

View File

@ -0,0 +1,390 @@
from django.test import TestCase
from django.urls import reverse
from envipy_additional_information import Temperature, Interval
from epdb.logic import UserManager, PackageManager
from epdb.models import Reaction, Scenario, ExternalDatabase
class ReactionViewTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"]
@classmethod
def setUpClass(cls):
super(ReactionViewTest, cls).setUpClass()
cls.user1 = UserManager.create_user(
"user1",
"user1@envipath.com",
"SuperSafe",
set_setting=False,
add_to_group=True,
is_active=True,
)
cls.user1_default_package = cls.user1.default_package
cls.package = PackageManager.create_package(cls.user1, "Test", "Test Pack")
def setUp(self):
self.client.force_login(self.user1)
def test_create_reaction(self):
response = self.client.post(
reverse("reactions"),
{
"reaction-name": "Eawag BBD reaction r0001",
"reaction-description": "Description for Eawag BBD reaction r0001",
"reaction-smirks": "C(CCl)Cl>>C(CO)Cl",
},
)
self.assertEqual(response.status_code, 302)
reaction_url = response.url
r = Reaction.objects.get(url=reaction_url)
self.assertEqual(r.package, self.user1_default_package)
self.assertEqual(r.name, "Eawag BBD reaction r0001")
self.assertEqual(r.description, "Description for Eawag BBD reaction r0001")
self.assertEqual(r.smirks(), "C(CCl)Cl>>C(CO)Cl")
self.assertEqual(self.user1_default_package.reactions.count(), 1)
# Adding the same rule again should return the existing one, hence not increasing the number of rules
response = self.client.post(
reverse("reactions"),
{
"reaction-name": "Eawag BBD reaction r0001",
"reaction-description": "Description for Eawag BBD reaction r0001",
"reaction-smirks": "C(CCl)Cl>>C(CO)Cl",
},
)
self.assertEqual(response.url, reaction_url)
self.assertEqual(response.status_code, 302)
self.assertEqual(self.user1_default_package.reactions.count(), 1)
# Adding the same rule in a different package should create a new rule
response = self.client.post(
reverse("package reaction list", kwargs={"package_uuid": self.package.uuid}),
{
"reaction-name": "Eawag BBD reaction r0001",
"reaction-description": "Description for Eawag BBD reaction r0001",
"reaction-smirks": "C(CCl)Cl>>C(CO)Cl",
},
)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(reaction_url, response.url)
# adding another reaction should increase count
response = self.client.post(
reverse("reactions"),
{
"reaction-name": "Eawag BBD reaction r0002",
"reaction-description": "Description for Eawag BBD reaction r0002",
"reaction-smirks": "C(CO)Cl>>C(C=O)Cl",
},
)
self.assertEqual(response.status_code, 302)
self.assertEqual(self.user1_default_package.reactions.count(), 2)
# Edit
def test_edit_rule(self):
response = self.client.post(
reverse("reactions"),
{
"reaction-name": "Eawag BBD reaction r0001",
"reaction-description": "Description for Eawag BBD reaction r0001",
"reaction-smirks": "C(CCl)Cl>>C(CO)Cl",
},
)
self.assertEqual(response.status_code, 302)
reaction_url = response.url
r = Reaction.objects.get(url=reaction_url)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={
"package_uuid": str(self.user1_default_package.uuid),
"reaction_uuid": str(r.uuid),
},
),
{
"reaction-name": "Test Reaction Adjusted",
"reaction-description": "New Description",
},
)
self.assertEqual(response.status_code, 302)
r = Reaction.objects.get(url=reaction_url)
self.assertEqual(r.name, "Test Reaction Adjusted")
self.assertEqual(r.description, "New Description")
# Rest stays untouched
self.assertEqual(r.smirks(), "C(CCl)Cl>>C(CO)Cl")
self.assertEqual(self.user1_default_package.reactions.count(), 1)
# Scenario
def test_set_scenario(self):
s1 = Scenario.create(
self.user1_default_package,
"Test Scen",
"Test Desc",
"2025-10",
"soil",
[Temperature(interval=Interval(start=20, end=30))],
)
s2 = Scenario.create(
self.user1_default_package,
"Test Scen2",
"Test Desc2",
"2025-10",
"soil",
[Temperature(interval=Interval(start=10, end=20))],
)
response = self.client.post(
reverse("reactions"),
{
"reaction-name": "Eawag BBD reaction r0001",
"reaction-description": "Description for Eawag BBD reaction r0001",
"reaction-smirks": "C(CCl)Cl>>C(CO)Cl",
},
)
self.assertEqual(response.status_code, 302)
reaction_url = response.url
r = Reaction.objects.get(url=reaction_url)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={"package_uuid": str(r.package.uuid), "reaction_uuid": str(r.uuid)},
),
{"selected-scenarios": [s1.url, s2.url]},
)
self.assertEqual(len(r.scenarios.all()), 2)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={"package_uuid": str(r.package.uuid), "reaction_uuid": str(r.uuid)},
),
{"selected-scenarios": [s1.url]},
)
self.assertEqual(len(r.scenarios.all()), 1)
self.assertEqual(r.scenarios.first().url, s1.url)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={"package_uuid": str(r.package.uuid), "reaction_uuid": str(r.uuid)},
),
{
# We have to set an empty string to avoid that the parameter is removed
"selected-scenarios": ""
},
)
self.assertEqual(len(r.scenarios.all()), 0)
def test_copy(self):
response = self.client.post(
reverse("reactions"),
{
"reaction-name": "Eawag BBD reaction r0001",
"reaction-description": "Description for Eawag BBD reaction r0001",
"reaction-smirks": "C(CCl)Cl>>C(CO)Cl",
},
)
self.assertEqual(response.status_code, 302)
reaction_url = response.url
r = Reaction.objects.get(url=reaction_url)
response = self.client.post(
reverse(
"package detail",
kwargs={
"package_uuid": str(self.package.uuid),
},
),
{"hidden": "copy", "object_to_copy": r.url},
)
self.assertEqual(response.status_code, 200)
copied_object_url = response.json()["success"]
copied_reaction = Reaction.objects.get(url=copied_object_url)
self.assertEqual(copied_reaction.name, r.name)
self.assertEqual(copied_reaction.description, r.description)
self.assertEqual(copied_reaction.smirks(), r.smirks())
# Copy to the same package should fail
response = self.client.post(
reverse(
"package detail",
kwargs={
"package_uuid": str(r.package.uuid),
},
),
{"hidden": "copy", "object_to_copy": r.url},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.json()["error"], f"Can't copy object {reaction_url} to the same package!"
)
def test_references(self):
ext_db, _ = ExternalDatabase.objects.get_or_create(
name="KEGG Reaction",
defaults={
"full_name": "KEGG Reaction Database",
"description": "Database of biochemical reactions",
"base_url": "https://www.genome.jp",
"url_pattern": "https://www.genome.jp/entry/{id}",
},
)
ext_db2, _ = ExternalDatabase.objects.get_or_create(
name="RHEA",
defaults={
"full_name": "RHEA Reaction Database",
"description": "Comprehensive resource of biochemical reactions",
"base_url": "https://www.rhea-db.org",
"url_pattern": "https://www.rhea-db.org/rhea/{id}",
},
)
response = self.client.post(
reverse("reactions"),
{
"reaction-name": "Eawag BBD reaction r0001",
"reaction-description": "Description for Eawag BBD reaction r0001",
"reaction-smirks": "C(CCl)Cl>>C(CO)Cl",
},
)
self.assertEqual(response.status_code, 302)
reaction_url = response.url
r = Reaction.objects.get(url=reaction_url)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={
"package_uuid": str(r.package.uuid),
"reaction_uuid": str(r.uuid),
},
),
{"selected-database": ext_db.pk, "identifier": "C12345"},
)
self.assertEqual(r.external_identifiers.count(), 1)
self.assertEqual(r.external_identifiers.first().database, ext_db)
self.assertEqual(r.external_identifiers.first().identifier_value, "C12345")
# TODO Fixture contains old url template there the real test fails, use old value instead
# self.assertEqual(r.external_identifiers.first().url, 'https://www.genome.jp/entry/C12345')
self.assertEqual(
r.external_identifiers.first().url, "https://www.genome.jp/entry/reaction+C12345"
)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={
"package_uuid": str(r.package.uuid),
"reaction_uuid": str(r.uuid),
},
),
{"selected-database": ext_db2.pk, "identifier": "60116"},
)
self.assertEqual(r.external_identifiers.count(), 2)
self.assertEqual(r.external_identifiers.last().database, ext_db2)
self.assertEqual(r.external_identifiers.last().identifier_value, "60116")
self.assertEqual(r.external_identifiers.last().url, "https://www.rhea-db.org/rhea/60116")
def test_delete(self):
response = self.client.post(
reverse("reactions"),
{
"reaction-name": "Eawag BBD reaction r0001",
"reaction-description": "Description for Eawag BBD reaction r0001",
"reaction-smirks": "C(CCl)Cl>>C(CO)Cl",
},
)
self.assertEqual(response.status_code, 302)
reaction_url = response.url
r = Reaction.objects.get(url=reaction_url)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={"package_uuid": str(r.package.uuid), "reaction_uuid": str(r.uuid)},
),
{"hidden": "delete"},
)
self.assertEqual(self.user1_default_package.reactions.count(), 0)
def test_set_aliases(self):
alias_1 = "Alias 1"
alias_2 = "Alias 2"
response = self.client.post(
reverse("reactions"),
{
"reaction-name": "Eawag BBD reaction r0001",
"reaction-description": "Description for Eawag BBD reaction r0001",
"reaction-smirks": "C(CCl)Cl>>C(CO)Cl",
},
)
self.assertEqual(response.status_code, 302)
reaction_url = response.url
r = Reaction.objects.get(url=reaction_url)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={"package_uuid": str(r.package.uuid), "reaction_uuid": str(r.uuid)},
),
{"aliases": [alias_1, alias_2]},
)
r = Reaction.objects.get(url=reaction_url)
self.assertEqual(len(r.aliases), 2)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={"package_uuid": str(r.package.uuid), "reaction_uuid": str(r.uuid)},
),
{"aliases": [alias_1]},
)
r = Reaction.objects.get(url=reaction_url)
self.assertEqual(len(r.aliases), 1)
response = self.client.post(
reverse(
"package reaction detail",
kwargs={"package_uuid": str(r.package.uuid), "reaction_uuid": str(r.uuid)},
),
{
# We have to set an empty string to avoid that the parameter is removed
"aliases": ""
},
)
r = Reaction.objects.get(url=reaction_url)
self.assertEqual(len(r.aliases), 0)

View File

@ -0,0 +1,314 @@
from django.test import TestCase
from django.urls import reverse
from envipy_additional_information import Temperature, Interval
from epdb.logic import UserManager, PackageManager
from epdb.models import Rule, Scenario
class RuleViewTest(TestCase):
fixtures = ["test_fixtures.jsonl.gz"]
@classmethod
def setUpClass(cls):
super(RuleViewTest, cls).setUpClass()
cls.user1 = UserManager.create_user(
"user1",
"user1@envipath.com",
"SuperSafe",
set_setting=False,
add_to_group=True,
is_active=True,
)
cls.user1_default_package = cls.user1.default_package
cls.package = PackageManager.create_package(cls.user1, "Test", "Test Pack")
def setUp(self):
self.client.force_login(self.user1)
def test_create_rule(self):
response = self.client.post(
reverse("rules"),
{
"rule-name": "Test Rule",
"rule-description": "Just a Description",
"rule-smirks": "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
"rule-type": "SimpleAmbitRule",
},
)
self.assertEqual(response.status_code, 302)
rule_url = response.url
r = Rule.objects.get(url=rule_url)
self.assertEqual(r.package, self.user1_default_package)
self.assertEqual(r.name, "Test Rule")
self.assertEqual(r.description, "Just a Description")
self.assertEqual(
r.smirks,
"[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
)
self.assertEqual(self.user1_default_package.rules.count(), 1)
# Adding the same rule again should return the existing one, hence not increasing the number of rules
response = self.client.post(
reverse("rules"),
{
"rule-name": "Test Rule",
"rule-description": "Just a Description",
"rule-smirks": "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
"rule-type": "SimpleAmbitRule",
},
)
self.assertEqual(response.url, rule_url)
self.assertEqual(response.status_code, 302)
self.assertEqual(self.user1_default_package.rules.count(), 1)
# Adding the same rule in a different package should create a new rule
response = self.client.post(
reverse("package rule list", kwargs={"package_uuid": self.package.uuid}),
{
"rule-name": "Test Rule",
"rule-description": "Just a Description",
"rule-smirks": "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
"rule-type": "SimpleAmbitRule",
},
)
self.assertEqual(response.status_code, 302)
self.assertNotEqual(rule_url, response.url)
# Edit
def test_edit_rule(self):
response = self.client.post(
reverse("rules"),
{
"rule-name": "Test Rule",
"rule-description": "Just a Description",
"rule-smirks": "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
"rule-type": "SimpleAmbitRule",
},
)
self.assertEqual(response.status_code, 302)
rule_url = response.url
r = Rule.objects.get(url=rule_url)
response = self.client.post(
reverse(
"package rule detail",
kwargs={
"package_uuid": str(self.user1_default_package.uuid),
"rule_uuid": str(r.uuid),
},
),
{
"rule-name": "Test Rule Adjusted",
"rule-description": "New Description",
},
)
self.assertEqual(response.status_code, 302)
r = Rule.objects.get(url=rule_url)
self.assertEqual(r.name, "Test Rule Adjusted")
self.assertEqual(r.description, "New Description")
# Scenario
def test_set_scenario(self):
s1 = Scenario.create(
self.user1_default_package,
"Test Scen",
"Test Desc",
"2025-10",
"soil",
[Temperature(interval=Interval(start=20, end=30))],
)
s2 = Scenario.create(
self.user1_default_package,
"Test Scen2",
"Test Desc2",
"2025-10",
"soil",
[Temperature(interval=Interval(start=10, end=20))],
)
response = self.client.post(
reverse("rules"),
{
"rule-name": "Test Rule",
"rule-description": "Just a Description",
"rule-smirks": "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
"rule-type": "SimpleAmbitRule",
},
)
self.assertEqual(response.status_code, 302)
rule_url = response.url
r = Rule.objects.get(url=rule_url)
response = self.client.post(
reverse(
"package rule detail",
kwargs={"package_uuid": str(r.package.uuid), "rule_uuid": str(r.uuid)},
),
{"selected-scenarios": [s1.url, s2.url]},
)
self.assertEqual(len(r.scenarios.all()), 2)
response = self.client.post(
reverse(
"package rule detail",
kwargs={"package_uuid": str(r.package.uuid), "rule_uuid": str(r.uuid)},
),
{"selected-scenarios": [s1.url]},
)
self.assertEqual(len(r.scenarios.all()), 1)
self.assertEqual(r.scenarios.first().url, s1.url)
response = self.client.post(
reverse(
"package rule detail",
kwargs={"package_uuid": str(r.package.uuid), "rule_uuid": str(r.uuid)},
),
{
# We have to set an empty string to avoid that the parameter is removed
"selected-scenarios": ""
},
)
self.assertEqual(len(r.scenarios.all()), 0)
def test_copy(self):
response = self.client.post(
reverse("rules"),
{
"rule-name": "Test Rule",
"rule-description": "Just a Description",
"rule-smirks": "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
"rule-type": "SimpleAmbitRule",
},
)
self.assertEqual(response.status_code, 302)
rule_url = response.url
r = Rule.objects.get(url=rule_url)
response = self.client.post(
reverse(
"package detail",
kwargs={
"package_uuid": str(self.package.uuid),
},
),
{"hidden": "copy", "object_to_copy": r.url},
)
self.assertEqual(response.status_code, 200)
copied_object_url = response.json()["success"]
copied_rule = Rule.objects.get(url=copied_object_url)
self.assertEqual(copied_rule.name, r.name)
self.assertEqual(copied_rule.description, r.description)
self.assertEqual(copied_rule.smirks, r.smirks)
# Copy to the same package should fail
response = self.client.post(
reverse(
"package detail",
kwargs={
"package_uuid": str(r.package.uuid),
},
),
{"hidden": "copy", "object_to_copy": r.url},
)
self.assertEqual(response.status_code, 400)
self.assertEqual(
response.json()["error"], f"Can't copy object {rule_url} to the same package!"
)
def test_delete(self):
response = self.client.post(
reverse("rules"),
{
"rule-name": "Test Rule",
"rule-description": "Just a Description",
"rule-smirks": "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
"rule-type": "SimpleAmbitRule",
},
)
self.assertEqual(response.status_code, 302)
rule_url = response.url
r = Rule.objects.get(url=rule_url)
response = self.client.post(
reverse(
"package rule detail",
kwargs={"package_uuid": str(r.package.uuid), "rule_uuid": str(r.uuid)},
),
{"hidden": "delete"},
)
self.assertEqual(self.user1_default_package.rules.count(), 0)
def test_set_aliases(self):
alias_1 = "Alias 1"
alias_2 = "Alias 2"
response = self.client.post(
reverse("rules"),
{
"rule-name": "Test Rule",
"rule-description": "Just a Description",
"rule-smirks": "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]>>[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]",
"rule-type": "SimpleAmbitRule",
},
)
self.assertEqual(response.status_code, 302)
rule_url = response.url
r = Rule.objects.get(url=rule_url)
response = self.client.post(
reverse(
"package rule detail",
kwargs={"package_uuid": str(r.package.uuid), "rule_uuid": str(r.uuid)},
),
{"aliases": [alias_1, alias_2]},
)
r = Rule.objects.get(url=rule_url)
self.assertEqual(len(r.aliases), 2)
response = self.client.post(
reverse(
"package rule detail",
kwargs={"package_uuid": str(r.package.uuid), "rule_uuid": str(r.uuid)},
),
{"aliases": [alias_1]},
)
r = Rule.objects.get(url=rule_url)
self.assertEqual(len(r.aliases), 1)
response = self.client.post(
reverse(
"package rule detail",
kwargs={"package_uuid": str(r.package.uuid), "rule_uuid": str(r.uuid)},
),
{
# We have to set an empty string to avoid that the parameter is removed
"aliases": ""
},
)
r = Rule.objects.get(url=rule_url)
self.assertEqual(len(r.aliases), 0)

View File

@ -11,70 +11,81 @@ class UserViewTest(TestCase):
@classmethod
def setUpClass(cls):
super(UserViewTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
cls.BBD_SUBSET = Package.objects.get(name='Fixtures')
cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
cls.BBD_SUBSET = Package.objects.get(name="Fixtures")
def test_login_with_valid_credentials(self):
response = self.client.post(reverse("login"), {
"username": "user0",
"password": 'SuperSafe',
})
response = self.client.post(
reverse("login"),
{
"username": "user0",
"password": "SuperSafe",
},
)
self.assertRedirects(response, reverse("index"))
self.assertTrue(response.wsgi_request.user.is_authenticated)
def test_login_with_invalid_credentials(self):
response = self.client.post(reverse("login"), {
"username": "user0",
"password": "wrongpassword",
})
response = self.client.post(
reverse("login"),
{
"username": "user0",
"password": "wrongpassword",
},
)
self.assertEqual(response.status_code, 200)
self.assertFalse(response.wsgi_request.user.is_authenticated)
def test_register(self):
response = self.client.post(reverse("register"), {
"username": "user1",
"email": "user1@envipath.com",
"password": "SuperSafe",
"rpassword": "SuperSafe",
})
response = self.client.post(
reverse("register"),
{
"username": "user1",
"email": "user1@envipath.com",
"password": "SuperSafe",
"rpassword": "SuperSafe",
},
)
self.assertEqual(response.status_code, 200)
# TODO currently fails as the fixture does not provide a global setting...
self.assertContains(response, "Registration failed!")
def test_register_password_mismatch(self):
response = self.client.post(reverse("register"), {
"username": "user1",
"email": "user1@envipath.com",
"password": "SuperSafe",
"rpassword": "SuperSaf3",
})
response = self.client.post(
reverse("register"),
{
"username": "user1",
"email": "user1@envipath.com",
"password": "SuperSafe",
"rpassword": "SuperSaf3",
},
)
self.assertEqual(response.status_code, 200)
self.assertContains(response, "Registration failed, provided passwords differ")
def test_logout(self):
response = self.client.post(reverse("login"), {
"username": "user0",
"password": 'SuperSafe',
"login": "true"
})
response = self.client.post(
reverse("login"), {"username": "user0", "password": "SuperSafe", "login": "true"}
)
self.assertTrue(response.wsgi_request.user.is_authenticated)
response = self.client.post(reverse('logout'), {
"logout": "true",
})
response = self.client.post(
reverse("logout"),
{
"logout": "true",
},
)
self.assertFalse(response.wsgi_request.user.is_authenticated)
def test_next_param_properly_handled(self):
response = self.client.get(reverse('packages'))
response = self.client.get(reverse("packages"))
self.assertRedirects(response, f"{reverse('login')}/?next=/package")
response = self.client.post(reverse('login'), {
"username": "user0",
"password": 'SuperSafe',
"login": "true",
"next": "/package"
})
response = self.client.post(
reverse("login"),
{"username": "user0", "password": "SuperSafe", "login": "true", "next": "/package"},
)
self.assertRedirects(response, reverse('packages'))
self.assertRedirects(response, reverse("packages"))

View File

@ -1,13 +0,0 @@
import abc
from enviPy.epdb import Pathway
class PredictionSchema(abc.ABC):
pass
class DFS(PredictionSchema):
def __init__(self, pw: Pathway, settings=None):
self.setting = settings or pw.prediction_settings
def predict(self):
pass

View File

@ -2,12 +2,11 @@ import logging
import re
from abc import ABC
from collections import defaultdict
from typing import List, Optional, Dict
from typing import List, Optional, Dict, TYPE_CHECKING
from indigo import Indigo, IndigoException, IndigoObject
from indigo.renderer import IndigoRenderer
from rdkit import Chem
from rdkit import RDLogger
from rdkit import Chem, rdBase
from rdkit.Chem import MACCSkeys, Descriptors
from rdkit.Chem import rdChemReactions
from rdkit.Chem.Draw import rdMolDraw2D
@ -15,9 +14,11 @@ from rdkit.Chem.MolStandardize import rdMolStandardize
from rdkit.Chem.rdmolops import GetMolFrags
from rdkit.Contrib.IFG import ifg
logger = logging.getLogger(__name__)
RDLogger.DisableLog('rdApp.*')
if TYPE_CHECKING:
from epdb.models import Rule
logger = logging.getLogger(__name__)
rdBase.DisableLog("rdApp.*")
# from rdkit import rdBase
# rdBase.LogToPythonLogger()
@ -28,7 +29,6 @@ RDLogger.DisableLog('rdApp.*')
class ProductSet(object):
def __init__(self, product_set: List[str]):
self.product_set = product_set
@ -42,15 +42,18 @@ class ProductSet(object):
return iter(self.product_set)
def __eq__(self, other):
return isinstance(other, ProductSet) and sorted(self.product_set) == sorted(other.product_set)
return isinstance(other, ProductSet) and sorted(self.product_set) == sorted(
other.product_set
)
def __hash__(self):
return hash('-'.join(sorted(self.product_set)))
return hash("-".join(sorted(self.product_set)))
class PredictionResult(object):
def __init__(self, product_sets: List['ProductSet'], probability: float, rule: Optional['Rule'] = None):
def __init__(
self, product_sets: List["ProductSet"], probability: float, rule: Optional["Rule"] = None
):
self.product_sets = product_sets
self.probability = probability
self.rule = rule
@ -66,7 +69,6 @@ class PredictionResult(object):
class FormatConverter(object):
@staticmethod
def mass(smiles):
return Descriptors.MolWt(FormatConverter.from_smiles(smiles))
@ -127,7 +129,7 @@ class FormatConverter(object):
if kekulize:
try:
mol = Chem.Kekulize(mol)
except:
except Exception:
mol = Chem.Mol(mol.ToBinary())
if not mol.GetNumConformers():
@ -139,8 +141,8 @@ class FormatConverter(object):
opts.clearBackground = False
drawer.DrawMolecule(mol)
drawer.FinishDrawing()
svg = drawer.GetDrawingText().replace('svg:', '')
svg = re.sub("<\?xml.*\?>", '', svg)
svg = drawer.GetDrawingText().replace("svg:", "")
svg = re.sub("<\?xml.*\?>", "", svg)
return svg
@ -151,7 +153,7 @@ class FormatConverter(object):
if kekulize:
try:
Chem.Kekulize(mol)
except:
except Exception:
mc = Chem.Mol(mol.ToBinary())
if not mc.GetNumConformers():
@ -178,7 +180,7 @@ class FormatConverter(object):
smiles = tmp_smiles
if change is False:
print(f"nothing changed")
print("nothing changed")
return smiles
@ -198,7 +200,9 @@ class FormatConverter(object):
parent_clean_mol = rdMolStandardize.FragmentParent(clean_mol)
# try to neutralize molecule
uncharger = rdMolStandardize.Uncharger() # annoying, but necessary as no convenience method exists
uncharger = (
rdMolStandardize.Uncharger()
) # annoying, but necessary as no convenience method exists
uncharged_parent_clean_mol = uncharger.uncharge(parent_clean_mol)
# note that no attempt is made at reionization at this step
@ -239,17 +243,24 @@ class FormatConverter(object):
try:
rdChemReactions.ReactionFromSmarts(smirks)
return True
except:
except Exception:
return False
@staticmethod
def apply(smiles: str, smirks: str, preprocess_smiles: bool = True, bracketize: bool = True,
standardize: bool = True, kekulize: bool = True) -> List['ProductSet']:
logger.debug(f'Applying {smirks} on {smiles}')
def apply(
smiles: str,
smirks: str,
preprocess_smiles: bool = True,
bracketize: bool = True,
standardize: bool = True,
kekulize: bool = True,
remove_stereo: bool = True,
) -> List["ProductSet"]:
logger.debug(f"Applying {smirks} on {smiles}")
# If explicitly wanted or rule generates multiple products add brackets around products to capture all
if bracketize: # or "." in smirks:
smirks = smirks.split('>>')[0] + ">>(" + smirks.split('>>')[1] + ")"
smirks = smirks.split(">>")[0] + ">>(" + smirks.split(">>")[1] + ")"
# List of ProductSet objects
pss = set()
@ -274,7 +285,9 @@ class FormatConverter(object):
Chem.SanitizeMol(product)
product = GetMolFrags(product, asMols=True)
for p in product:
p = FormatConverter.standardize(Chem.MolToSmiles(p))
p = FormatConverter.standardize(
Chem.MolToSmiles(p), remove_stereo=remove_stereo
)
prods.append(p)
# if kekulize:
@ -300,9 +313,8 @@ class FormatConverter(object):
# # bond.SetIsAromatic(False)
# Chem.Kekulize(product)
except ValueError as e:
logger.error(f'Sanitizing and converting failed:\n{e}')
logger.error(f"Sanitizing and converting failed:\n{e}")
continue
if len(prods):
@ -310,34 +322,14 @@ class FormatConverter(object):
pss.add(ps)
except Exception as e:
logger.error(f'Applying {smirks} on {smiles} failed:\n{e}')
logger.error(f"Applying {smirks} on {smiles} failed:\n{e}")
return pss
# @staticmethod
# def apply(reaction, smiles):
# rxn = AllChem.ReactionFromSmarts(reaction)
# return [Chem.MolToSmiles(x, 1) for x in rxn.RunReactants((Chem.MolFromSmiles(smiles),))[0]]
@staticmethod
def MACCS(smiles):
return MACCSkeys.GenMACCSKeys(FormatConverter.from_smiles(smiles))
@staticmethod
def neutralize_atoms(mol):
pattern = Chem.MolFromSmarts("[+1!h0!$([*]~[-1,-2,-3,-4]),-1!$([*]~[+1,+2,+3,+4])]")
at_matches = mol.GetSubstructMatches(pattern)
at_matches_list = [y[0] for y in at_matches]
if len(at_matches_list) > 0:
for at_idx in at_matches_list:
atom = mol.GetAtomWithIdx(at_idx)
chg = atom.GetFormalCharge()
hcount = atom.GetTotalNumHs()
atom.SetFormalCharge(0)
atom.SetNumExplicitHs(hcount - chg)
atom.UpdatePropertyCache()
return mol
@staticmethod
def sanitize_smiles(smiles_list: List):
parsed_smiles = []
@ -353,28 +345,26 @@ class FormatConverter(object):
# smi = smi.replace("@", "")
mol = Chem.MolFromSmiles(smi)
mol = FormatConverter.neutralize_atoms(mol)
mol = FormatConverter.neutralize_molecule(mol)
Chem.RemoveStereochemistry(mol)
mol = Chem.RemoveAllHs(mol)
Chem.Kekulize(mol)
smi_p = Chem.MolToSmiles(mol, kekuleSmiles=True)
smi_p = Chem.CanonSmiles(smi_p)
if '~' in smi_p:
smi_p1 = smi_p.replace('~', '')
if "~" in smi_p:
smi_p1 = smi_p.replace("~", "")
parsed_smiles.append(smi_p1)
else:
parsed_smiles.append(smi_p)
except Exception as e:
except Exception:
errors += 1
pass
return parsed_smiles, errors
class Standardizer(ABC):
def __init__(self, name):
self.name = name
@ -383,7 +373,6 @@ class Standardizer(ABC):
class RuleStandardizer(Standardizer):
def __init__(self, name, smirks):
super().__init__(name)
self.smirks = smirks
@ -392,8 +381,8 @@ class RuleStandardizer(Standardizer):
standardized_smiles = list(set(FormatConverter.apply(smiles, self.smirks)))
if len(standardized_smiles) > 1:
logger.warning(f'{self.smirks} generated more than 1 compound {standardized_smiles}')
print(f'{self.smirks} generated more than 1 compound {standardized_smiles}')
logger.warning(f"{self.smirks} generated more than 1 compound {standardized_smiles}")
print(f"{self.smirks} generated more than 1 compound {standardized_smiles}")
standardized_smiles = standardized_smiles[:1]
if standardized_smiles:
@ -403,7 +392,6 @@ class RuleStandardizer(Standardizer):
class RegExStandardizer(Standardizer):
def __init__(self, name, replacements: dict):
super().__init__(name)
self.replacements = replacements
@ -423,28 +411,39 @@ class RegExStandardizer(Standardizer):
return super().standardize(smi)
FLATTEN = [
RegExStandardizer("Remove Stereo", {"@": ""})
]
FLATTEN = [RegExStandardizer("Remove Stereo", {"@": ""})]
UN_CIS_TRANS = [
RegExStandardizer("Un-Cis-Trans", {"/": "", "\\": ""})
]
UN_CIS_TRANS = [RegExStandardizer("Un-Cis-Trans", {"/": "", "\\": ""})]
BASIC = [
RuleStandardizer("ammoniumstandardization", "[H][N+:1]([H])([H])[#6:2]>>[H][#7:1]([H])-[#6:2]"),
RuleStandardizer("cyanate", "[H][#8:1][C:2]#[N:3]>>[#8-:1][C:2]#[N:3]"),
RuleStandardizer("deprotonatecarboxyls", "[H][#8:1]-[#6:2]=[O:3]>>[#8-:1]-[#6:2]=[O:3]"),
RuleStandardizer("forNOOH", "[H][#8:1]-[#7+:2](-[*:3])=[O:4]>>[#8-:1]-[#7+:2](-[*:3])=[O:4]"),
RuleStandardizer("Hydroxylprotonation", "[#6;A:1][#6:2](-[#8-:3])=[#6;A:4]>>[#6:1]-[#6:2](-[#8:3][H])=[#6;A:4]"),
RuleStandardizer("phosphatedeprotonation", "[H][#8:1]-[$([#15]);!$(P([O-])):2]>>[#8-:1]-[#15:2]"),
RuleStandardizer("PicricAcid",
"[H][#8:1]-[c:2]1[c:3][c:4][c:5]([c:6][c:7]1-[#7+:8](-[#8-:9])=[O:10])-[#7+:11](-[#8-:12])=[O:13]>>[#8-:1]-[c:2]1[c:3][c:4][c:5]([c:6][c:7]1-[#7+:8](-[#8-:9])=[O:10])-[#7+:11](-[#8-:12])=[O:13]"),
RuleStandardizer("Sulfate1", "[H][#8:1][S:2]([#8:3][H])(=[O:4])=[O:5]>>[#8-:1][S:2]([#8-:3])(=[O:4])=[O:5]"),
RuleStandardizer("Sulfate2",
"[#6:1]-[#8:2][S:3]([#8:4][H])(=[O:5])=[O:6]>>[#6:1]-[#8:2][S:3]([#8-:4])(=[O:5])=[O:6]"),
RuleStandardizer("Sulfate3", "[H][#8:3][S:2]([#6:1])(=[O:4])=[O:5]>>[#6:1][S:2]([#8-:3])(=[O:4])=[O:5]"),
RuleStandardizer("Transform_c1353forSOOH", "[H][#8:1][S:2]([*:3])=[O:4]>>[#8-:1][S:2]([*:3])=[O:4]"),
RuleStandardizer(
"Hydroxylprotonation",
"[#6;A:1][#6:2](-[#8-:3])=[#6;A:4]>>[#6:1]-[#6:2](-[#8:3][H])=[#6;A:4]",
),
RuleStandardizer(
"phosphatedeprotonation", "[H][#8:1]-[$([#15]);!$(P([O-])):2]>>[#8-:1]-[#15:2]"
),
RuleStandardizer(
"PicricAcid",
"[H][#8:1]-[c:2]1[c:3][c:4][c:5]([c:6][c:7]1-[#7+:8](-[#8-:9])=[O:10])-[#7+:11](-[#8-:12])=[O:13]>>[#8-:1]-[c:2]1[c:3][c:4][c:5]([c:6][c:7]1-[#7+:8](-[#8-:9])=[O:10])-[#7+:11](-[#8-:12])=[O:13]",
),
RuleStandardizer(
"Sulfate1", "[H][#8:1][S:2]([#8:3][H])(=[O:4])=[O:5]>>[#8-:1][S:2]([#8-:3])(=[O:4])=[O:5]"
),
RuleStandardizer(
"Sulfate2",
"[#6:1]-[#8:2][S:3]([#8:4][H])(=[O:5])=[O:6]>>[#6:1]-[#8:2][S:3]([#8-:4])(=[O:5])=[O:6]",
),
RuleStandardizer(
"Sulfate3", "[H][#8:3][S:2]([#6:1])(=[O:4])=[O:5]>>[#6:1][S:2]([#8-:3])(=[O:4])=[O:5]"
),
RuleStandardizer(
"Transform_c1353forSOOH", "[H][#8:1][S:2]([*:3])=[O:4]>>[#8-:1][S:2]([*:3])=[O:4]"
),
]
ENHANCED = BASIC + [
@ -452,28 +451,30 @@ ENHANCED = BASIC + [
]
EXOTIC = ENHANCED + [
RuleStandardizer("ThioPhosphate1", "[H][S:1]-[#15:2]=[$([#16]),$([#8]):3]>>[S-:1]-[#15:2]=[$([#16]),$([#8]):3]")
RuleStandardizer(
"ThioPhosphate1",
"[H][S:1]-[#15:2]=[$([#16]),$([#8]):3]>>[S-:1]-[#15:2]=[$([#16]),$([#8]):3]",
)
]
COA_CUTTER = [
RuleStandardizer("CutCoEnzymeAOff",
"CC(C)(COP(O)(=O)OP(O)(=O)OCC1OC(C(O)C1OP(O)(O)=O)n1cnc2c(N)ncnc12)C(O)C(=O)NCCC(=O)NCCS[$(*):1]>>[O-][$(*):1]")
RuleStandardizer(
"CutCoEnzymeAOff",
"CC(C)(COP(O)(=O)OP(O)(=O)OCC1OC(C(O)C1OP(O)(O)=O)n1cnc2c(N)ncnc12)C(O)C(=O)NCCC(=O)NCCS[$(*):1]>>[O-][$(*):1]",
)
]
ENOL_KETO = [
RuleStandardizer("enol2Ketone", "[H][#8:2]-[#6:3]=[#6:1]>>[#6:1]-[#6:3]=[O:2]")
]
ENOL_KETO = [RuleStandardizer("enol2Ketone", "[H][#8:2]-[#6:3]=[#6:1]>>[#6:1]-[#6:3]=[O:2]")]
MATCH_STANDARDIZER = EXOTIC + FLATTEN + UN_CIS_TRANS + COA_CUTTER + ENOL_KETO
class IndigoUtils(object):
@staticmethod
def layout(mol_data):
i = Indigo()
try:
if mol_data.startswith('$RXN') or '>>' in mol_data:
if mol_data.startswith("$RXN") or ">>" in mol_data:
rxn = i.loadQueryReaction(mol_data)
rxn.layout()
return rxn.rxnfile()
@ -481,14 +482,14 @@ class IndigoUtils(object):
mol = i.loadQueryMolecule(mol_data)
mol.layout()
return mol.molfile()
except IndigoException as e:
except IndigoException:
try:
logger.info("layout() failed, trying loadReactionSMARTS as fallback!")
rxn = IndigoUtils.load_reaction_SMARTS(mol_data)
rxn.layout()
return rxn.molfile()
except IndigoException as e2:
logger.error(f'layout() failed due to {e2}!')
logger.error(f"layout() failed due to {e2}!")
@staticmethod
def load_reaction_SMARTS(mol):
@ -498,7 +499,7 @@ class IndigoUtils(object):
def aromatize(mol_data, is_query):
i = Indigo()
try:
if mol_data.startswith('$RXN'):
if mol_data.startswith("$RXN"):
if is_query:
rxn = i.loadQueryReaction(mol_data)
else:
@ -514,20 +515,20 @@ class IndigoUtils(object):
mol.aromatize()
return mol.molfile()
except IndigoException as e:
except IndigoException:
try:
logger.info("Aromatizing failed, trying loadReactionSMARTS as fallback!")
rxn = IndigoUtils.load_reaction_SMARTS(mol_data)
rxn.aromatize()
return rxn.molfile()
except IndigoException as e2:
logger.error(f'Aromatizing failed due to {e2}!')
logger.error(f"Aromatizing failed due to {e2}!")
@staticmethod
def dearomatize(mol_data, is_query):
i = Indigo()
try:
if mol_data.startswith('$RXN'):
if mol_data.startswith("$RXN"):
if is_query:
rxn = i.loadQueryReaction(mol_data)
else:
@ -543,14 +544,14 @@ class IndigoUtils(object):
mol.dearomatize()
return mol.molfile()
except IndigoException as e:
except IndigoException:
try:
logger.info("De-Aromatizing failed, trying loadReactionSMARTS as fallback!")
rxn = IndigoUtils.load_reaction_SMARTS(mol_data)
rxn.dearomatize()
return rxn.molfile()
except IndigoException as e2:
logger.error(f'De-Aromatizing failed due to {e2}!')
logger.error(f"De-Aromatizing failed due to {e2}!")
@staticmethod
def sanitize_functional_group(functional_group: str):
@ -562,7 +563,7 @@ class IndigoUtils(object):
# special environment handling (amines, hydroxy, esters, ethers)
# the higher substituted should not contain H env.
if functional_group == '[C]=O':
if functional_group == "[C]=O":
functional_group = "[H][C](=O)[CX4,c]"
# aldamines
@ -596,15 +597,20 @@ class IndigoUtils(object):
functional_group = "[nH1,nX2](a)a" # pyrrole (with H) or pyridine (no other connections); currently overlaps with neighboring aromatic atoms
# substituted aromatic nitrogen
functional_group = functional_group.replace("N*(R)R",
"n(a)a") # substituent will be before N*; currently overlaps with neighboring aromatic atoms
functional_group = functional_group.replace(
"N*(R)R", "n(a)a"
) # substituent will be before N*; currently overlaps with neighboring aromatic atoms
# pyridinium
if functional_group == "RN*(R)(R)(R)R":
functional_group = "[CX4,c]n(a)a" # currently overlaps with neighboring aromatic atoms
functional_group = (
"[CX4,c]n(a)a" # currently overlaps with neighboring aromatic atoms
)
# N-oxide
if functional_group == "[H]ON*(R)(R)(R)R":
functional_group = "[O-][n+](a)a" # currently overlaps with neighboring aromatic atoms
functional_group = (
"[O-][n+](a)a" # currently overlaps with neighboring aromatic atoms
)
# other aromatic hetero atoms
functional_group = functional_group.replace("C*", "c")
@ -617,7 +623,9 @@ class IndigoUtils(object):
# other replacement, to accomodate for the standardization rules in enviPath
# This is not the perfect way to do it; there should be a way to replace substructure SMARTS in SMARTS?
# nitro groups are broken, due to charge handling. this SMARTS matches both forms (formal charges and hypervalent); Ertl-CDK still treats both forms separately...
functional_group = functional_group.replace("[H]O[N](=O)R", "[CX4,c][NX3](~[OX1])~[OX1]")
functional_group = functional_group.replace(
"[H]O[N](=O)R", "[CX4,c][NX3](~[OX1])~[OX1]"
)
functional_group = functional_group.replace("O=N(=O)R", "[CX4,c][NX3](~[OX1])~[OX1]")
# carboxylic acid: this SMARTS matches both neutral and anionic form; includes COOH in larger functional_groups
functional_group = functional_group.replace("[H]OC(=O)", "[OD1]C(=O)")
@ -635,7 +643,9 @@ class IndigoUtils(object):
return functional_group
@staticmethod
def _colorize(indigo: Indigo, molecule: IndigoObject, functional_groups: Dict[str, int], is_reaction: bool):
def _colorize(
indigo: Indigo, molecule: IndigoObject, functional_groups: Dict[str, int], is_reaction: bool
):
indigo.setOption("render-atom-color-property", "color")
indigo.setOption("aromaticity-model", "generic")
@ -665,7 +675,6 @@ class IndigoUtils(object):
for match in matcher.iterateMatches(query):
if match is not None:
for atom in query.iterateAtoms():
mappedAtom = match.mapAtom(atom)
if mappedAtom is None or mappedAtom.index() in environment:
@ -674,7 +683,7 @@ class IndigoUtils(object):
counts[mappedAtom.index()] = max(v, counts[mappedAtom.index()])
except IndigoException as e:
logger.debug(f'Colorizing failed due to {e}')
logger.debug(f"Colorizing failed due to {e}")
for k, v in counts.items():
if is_reaction:
@ -688,8 +697,9 @@ class IndigoUtils(object):
molecule.addDataSGroup([k], [], "color", color)
@staticmethod
def mol_to_svg(mol_data: str, width: int = 0, height: int = 0, functional_groups: Dict[str, int] = None):
def mol_to_svg(
mol_data: str, width: int = 0, height: int = 0, functional_groups: Dict[str, int] = None
):
if functional_groups is None:
functional_groups = {}
@ -701,7 +711,7 @@ class IndigoUtils(object):
i.setOption("render-image-size", width, height)
i.setOption("render-bond-line-width", 2.0)
if '~' in mol_data:
if "~" in mol_data:
mol = i.loadSmarts(mol_data)
else:
mol = i.loadMolecule(mol_data)
@ -709,11 +719,18 @@ class IndigoUtils(object):
if len(functional_groups.keys()) > 0:
IndigoUtils._colorize(i, mol, functional_groups, False)
return renderer.renderToBuffer(mol).decode('UTF-8')
return renderer.renderToBuffer(mol).decode("UTF-8")
@staticmethod
def smirks_to_svg(smirks: str, is_query_smirks, width: int = 0, height: int = 0,
educt_functional_groups: Dict[str, int] = None, product_functional_groups: Dict[str, int] = None):
def smirks_to_svg(
smirks: str,
is_query_smirks,
width: int = 0,
height: int = 0,
educt_functional_groups: Dict[str, int] = None,
product_functional_groups: Dict[str, int] = None,
debug: bool = False,
):
if educt_functional_groups is None:
educt_functional_groups = {}
@ -723,6 +740,11 @@ class IndigoUtils(object):
i = Indigo()
renderer = IndigoRenderer(i)
if debug:
i.setOption("render-atom-ids-visible", True)
i.setOption("render-bond-ids-visible", False)
i.setOption("render-atom-bond-ids-from-one", True)
i.setOption("render-output-format", "svg")
i.setOption("render-coloring", True)
i.setOption("render-image-size", width, height)
@ -740,18 +762,18 @@ class IndigoUtils(object):
for prod in obj.iterateProducts():
IndigoUtils._colorize(i, prod, product_functional_groups, True)
return renderer.renderToBuffer(obj).decode('UTF-8')
return renderer.renderToBuffer(obj).decode("UTF-8")
if __name__ == '__main__':
if __name__ == "__main__":
data = {
"struct": "\n Ketcher 2172510 12D 1 1.00000 0.00000 0\n\n 6 6 0 0 0 999 V2000\n 0.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n -1.0000 0.0000 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n -1.5000 -0.8660 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n -1.0000 -1.7321 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.7321 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.5000 -0.8660 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\nM END\n",
"options": {
"smart-layout": True,
"ignore-stereochemistry-errors": True,
"mass-skip-error-on-pseudoatoms": False,
"gross-formula-add-rsites": True
}
"gross-formula-add-rsites": True,
},
}
print(IndigoUtils.aromatize(data['struct'], False))
print(IndigoUtils.aromatize(data["struct"], False))

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