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 # MAIL
EMAIL_HOST_USER= EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD= EMAIL_HOST_PASSWORD=

6
.gitignore vendored
View File

@ -5,4 +5,8 @@ static/admin/
static/django_extensions/ static/django_extensions/
.env .env
debug.log 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 # 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. # Django starts so that shared_task will use this app.
from .celery import app as celery_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() api = NinjaAPI()
from ninja import NinjaAPI
api_v1 = NinjaAPI(title="API V1 Docs", urls_namespace="api-v1") api_v1 = NinjaAPI(title="API V1 Docs", urls_namespace="api-v1")
api_legacy = NinjaAPI(title="Legacy API Docs", urls_namespace="api-legacy") 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 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() application = get_asgi_application()

View File

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

View File

@ -16,7 +16,9 @@ from .models import (
Node, Node,
Edge, Edge,
Scenario, Scenario,
Setting Setting,
ExternalDatabase,
ExternalIdentifier,
) )
@ -37,12 +39,13 @@ class GroupPackagePermissionAdmin(admin.ModelAdmin):
class EPAdmin(admin.ModelAdmin): class EPAdmin(admin.ModelAdmin):
search_fields = ['name', 'description'] search_fields = ["name", "description"]
class PackageAdmin(EPAdmin): class PackageAdmin(EPAdmin):
pass pass
class MLRelativeReasoningAdmin(EPAdmin): class MLRelativeReasoningAdmin(EPAdmin):
pass pass
@ -87,6 +90,14 @@ class SettingAdmin(EPAdmin):
pass pass
class ExternalDatabaseAdmin(admin.ModelAdmin):
pass
class ExternalIdentifierAdmin(admin.ModelAdmin):
pass
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(UserPackagePermission, UserPackagePermissionAdmin) admin.site.register(UserPackagePermission, UserPackagePermissionAdmin)
admin.site.register(Group, GroupAdmin) admin.site.register(Group, GroupAdmin)
@ -103,3 +114,5 @@ admin.site.register(Node, NodeAdmin)
admin.site.register(Edge, EdgeAdmin) admin.site.register(Edge, EdgeAdmin)
admin.site.register(Setting, SettingAdmin) admin.site.register(Setting, SettingAdmin)
admin.site.register(Scenario, ScenarioAdmin) 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): def _anonymous_or_real(request):
if request.user.is_authenticated and not request.user.is_anonymous: if request.user.is_authenticated and not request.user.is_anonymous:
return request.user return request.user
return get_user_model().objects.get(username='anonymous') return get_user_model().objects.get(username="anonymous")
router = Router(auth=BearerTokenAuth()) router = Router(auth=BearerTokenAuth())
@ -85,7 +85,9 @@ def get_package(request, package_uuid):
try: try:
return PackageManager.get_package_by_id(request.auth, package_id=package_uuid) return PackageManager.get_package_by_id(request.auth, package_id=package_uuid)
except ValueError: 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}) @router.get("/compound", response={200: List[CompoundSchema], 403: Error})
@ -97,7 +99,9 @@ def get_compounds(request):
return qs 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 @paginate
def get_package_compounds(request, package_uuid): def get_package_compounds(request, package_uuid):
try: try:
@ -105,4 +109,5 @@ def get_package_compounds(request, package_uuid):
return Compound.objects.filter(package=p) return Compound.objects.filter(package=p)
except ValueError: except ValueError:
return 403, { 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): class EPDBConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = "django.db.models.BigAutoField"
name = 'epdb' name = "epdb"
def ready(self): def ready(self):
import epdb.signals # noqa: F401 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 django.db import transaction
from epdb.logic import UserManager, GroupManager, PackageManager, SettingManager 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): class Command(BaseCommand):
def create_users(self): def create_users(self):
# Anonymous User # Anonymous User
if not User.objects.filter(email='anon@envipath.com').exists(): if not User.objects.filter(email="anon@envipath.com").exists():
anon = UserManager.create_user("anonymous", "anon@envipath.com", "SuperSafe", anon = UserManager.create_user(
is_active=True, add_to_group=False, set_setting=False) "anonymous",
"anon@envipath.com",
"SuperSafe",
is_active=True,
add_to_group=False,
set_setting=False,
)
else: else:
anon = User.objects.get(email='anon@envipath.com') anon = User.objects.get(email="anon@envipath.com")
# Admin User # Admin User
if not User.objects.filter(email='admin@envipath.com').exists(): if not User.objects.filter(email="admin@envipath.com").exists():
admin = UserManager.create_user("admin", "admin@envipath.com", "SuperSafe", admin = UserManager.create_user(
is_active=True, add_to_group=False, set_setting=False) "admin",
"admin@envipath.com",
"SuperSafe",
is_active=True,
add_to_group=False,
set_setting=False,
)
admin.is_staff = True admin.is_staff = True
admin.is_superuser = True admin.is_superuser = True
admin.save() admin.save()
else: else:
admin = User.objects.get(email='admin@envipath.com') admin = User.objects.get(email="admin@envipath.com")
# System Group # 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.public = True
g.save() g.save()
@ -43,14 +60,20 @@ class Command(BaseCommand):
admin.default_group = g admin.default_group = g
admin.save() admin.save()
if not User.objects.filter(email='user0@envipath.com').exists(): if not User.objects.filter(email="user0@envipath.com").exists():
user0 = UserManager.create_user("user0", "user0@envipath.com", "SuperSafe", user0 = UserManager.create_user(
is_active=True, add_to_group=False, set_setting=False) "user0",
"user0@envipath.com",
"SuperSafe",
is_active=True,
add_to_group=False,
set_setting=False,
)
user0.is_staff = True user0.is_staff = True
user0.is_superuser = True user0.is_superuser = True
user0.save() user0.save()
else: else:
user0 = User.objects.get(email='user0@envipath.com') user0 = User.objects.get(email="user0@envipath.com")
g.user_member.add(user0) g.user_member.add(user0)
g.save() g.save()
@ -61,18 +84,20 @@ class Command(BaseCommand):
return anon, admin, g, user0 return anon, admin, g, user0
def import_package(self, data, owner): 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): def create_default_setting(self, owner, packages):
s = SettingManager.create_setting( s = SettingManager.create_setting(
owner, owner,
name='Global Default Setting', name="Global Default Setting",
description='Global Default Setting containing BBD Rules and Max 30 Nodes and Max Depth of 8', description="Global Default Setting containing BBD Rules and Max 30 Nodes and Max Depth of 8",
max_nodes=30, max_nodes=30,
max_depth=5, max_depth=5,
rule_packages=packages, rule_packages=packages,
model=None, model=None,
model_threshold=None model_threshold=None,
) )
return s return s
@ -84,54 +109,51 @@ class Command(BaseCommand):
""" """
databases = [ databases = [
{ {
'name': 'PubChem Compound', "name": "PubChem Compound",
'full_name': 'PubChem Compound Database', "full_name": "PubChem Compound Database",
'description': 'Chemical database of small organic molecules', "description": "Chemical database of small organic molecules",
'base_url': 'https://pubchem.ncbi.nlm.nih.gov', "base_url": "https://pubchem.ncbi.nlm.nih.gov",
'url_pattern': 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}' "url_pattern": "https://pubchem.ncbi.nlm.nih.gov/compound/{id}",
}, },
{ {
'name': 'PubChem Substance', "name": "PubChem Substance",
'full_name': 'PubChem Substance Database', "full_name": "PubChem Substance Database",
'description': 'Database of chemical substances', "description": "Database of chemical substances",
'base_url': 'https://pubchem.ncbi.nlm.nih.gov', "base_url": "https://pubchem.ncbi.nlm.nih.gov",
'url_pattern': 'https://pubchem.ncbi.nlm.nih.gov/substance/{id}' "url_pattern": "https://pubchem.ncbi.nlm.nih.gov/substance/{id}",
}, },
{ {
'name': 'ChEBI', "name": "ChEBI",
'full_name': 'Chemical Entities of Biological Interest', "full_name": "Chemical Entities of Biological Interest",
'description': 'Dictionary of molecular entities', "description": "Dictionary of molecular entities",
'base_url': 'https://www.ebi.ac.uk/chebi', "base_url": "https://www.ebi.ac.uk/chebi",
'url_pattern': 'https://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:{id}' "url_pattern": "https://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:{id}",
}, },
{ {
'name': 'RHEA', "name": "RHEA",
'full_name': 'RHEA Reaction Database', "full_name": "RHEA Reaction Database",
'description': 'Comprehensive resource of biochemical reactions', "description": "Comprehensive resource of biochemical reactions",
'base_url': 'https://www.rhea-db.org', "base_url": "https://www.rhea-db.org",
'url_pattern': 'https://www.rhea-db.org/rhea/{id}' "url_pattern": "https://www.rhea-db.org/rhea/{id}",
}, },
{ {
'name': 'KEGG Reaction', "name": "KEGG Reaction",
'full_name': 'KEGG Reaction Database', "full_name": "KEGG Reaction Database",
'description': 'Database of biochemical reactions', "description": "Database of biochemical reactions",
'base_url': 'https://www.genome.jp', "base_url": "https://www.genome.jp",
'url_pattern': 'https://www.genome.jp/entry/reaction+{id}' "url_pattern": "https://www.genome.jp/entry/{id}",
}, },
{ {
'name': 'UniProt', "name": "UniProt",
'full_name': 'MetaCyc Metabolic Pathway Database', "full_name": "MetaCyc Metabolic Pathway Database",
'description': 'UniProt is a freely accessible database of protein sequence and functional information', "description": "UniProt is a freely accessible database of protein sequence and functional information",
'base_url': 'https://www.uniprot.org', "base_url": "https://www.uniprot.org",
'url_pattern': 'https://www.uniprot.org/uniprotkb?query="{id}"' "url_pattern": 'https://www.uniprot.org/uniprotkb?query="{id}"',
} },
] ]
for db_info in databases: for db_info in databases:
ExternalDatabase.objects.get_or_create( ExternalDatabase.objects.get_or_create(name=db_info["name"], defaults=db_info)
name=db_info['name'],
defaults=db_info
)
@transaction.atomic @transaction.atomic
def handle(self, *args, **options): def handle(self, *args, **options):
@ -142,20 +164,24 @@ class Command(BaseCommand):
# Import Packages # Import Packages
packages = [ packages = [
'EAWAG-BBD.json', "EAWAG-BBD.json",
'EAWAG-SOIL.json', "EAWAG-SOIL.json",
'EAWAG-SLUDGE.json', "EAWAG-SLUDGE.json",
'EAWAG-SEDIMENT.json', "EAWAG-SEDIMENT.json",
] ]
mapping = {} mapping = {}
for p in packages: for p in packages:
print(f"Importing {p}...") 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) 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.public = True
setting.save() setting.save()
setting.make_global_default() setting.make_global_default()
@ -171,26 +197,28 @@ class Command(BaseCommand):
usp.save() usp.save()
# Create Model Package # Create Model Package
pack = PackageManager.create_package(admin, "Public Prediction Models", pack = PackageManager.create_package(
"Package to make Prediction Models publicly available") admin,
"Public Prediction Models",
"Package to make Prediction Models publicly available",
)
pack.reviewed = True pack.reviewed = True
pack.save() pack.save()
# Create RR # Create RR
ml_model = MLRelativeReasoning.create( ml_model = MLRelativeReasoning.create(
package=pack, package=pack,
rule_packages=[mapping['EAWAG-BBD']], rule_packages=[mapping["EAWAG-BBD"]],
data_packages=[mapping['EAWAG-BBD']], data_packages=[mapping["EAWAG-BBD"]],
eval_packages=[], eval_packages=[],
threshold=0.5, threshold=0.5,
name='ECC - BBD - T0.5', name="ECC - BBD - T0.5",
description='ML Relative Reasoning', description="ML Relative Reasoning",
) )
ml_model.build_dataset() ml_model.build_dataset()
ml_model.build_model() ml_model.build_model()
# ml_model.evaluate_model()
# If available, create EnviFormerModel # If available, create EnviFormerModel
if s.ENVIFORMER_PRESENT: 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.core.management.base import BaseCommand
from django.db import transaction
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import * from epdb.models import User
class Command(BaseCommand): class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--data', "--data",
type=str, type=str,
help='Path of the Package to import.', help="Path of the Package to import.",
required=True, required=True,
) )
parser.add_argument( parser.add_argument(
'--owner', "--owner",
type=str, type=str,
help='Username of the desired Owner.', help="Username of the desired Owner.",
required=True, required=True,
) )
@transaction.atomic @transaction.atomic
def handle(self, *args, **options): def handle(self, *args, **options):
owner = User.objects.get(username=options['owner']) owner = User.objects.get(username=options["owner"])
package_data = json.load(open(options['data'])) package_data = json.load(open(options["data"]))
PackageManager.import_legacy_package(package_data, owner) PackageManager.import_legacy_package(package_data, owner)

View File

@ -1,51 +1,64 @@
from django.apps import apps from django.apps import apps
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from django.db.models import F, Value from django.db.models import F, Value, TextField, JSONField
from django.db.models.functions import Replace from django.db.models.functions import Replace, Cast
from epdb.models import EnviPathModel
class Command(BaseCommand): class Command(BaseCommand):
def add_arguments(self, parser): def add_arguments(self, parser):
parser.add_argument( parser.add_argument(
'--old', "--old",
type=str, type=str,
help='Old Host, most likely https://envipath.org/', help="Old Host, most likely https://envipath.org/",
required=True, required=True,
) )
parser.add_argument( parser.add_argument(
'--new', "--new",
type=str, type=str,
help='New Host, most likely http://localhost:8000/', help="New Host, most likely http://localhost:8000/",
required=True, required=True,
) )
def handle(self, *args, **options): def handle(self, *args, **options):
MODELS = [ MODELS = [
'User', "User",
'Group', "Group",
'Package', "Package",
'Compound', "Compound",
'CompoundStructure', "CompoundStructure",
'Pathway', "Pathway",
'Edge', "Edge",
'Node', "Node",
'Reaction', "Reaction",
'SimpleAmbitRule', "SimpleAmbitRule",
'SimpleRDKitRule', "SimpleRDKitRule",
'ParallelRule', "ParallelRule",
'SequentialRule', "SequentialRule",
'Scenario', "Scenario",
'Setting', "Setting",
'MLRelativeReasoning', "MLRelativeReasoning",
'RuleBasedRelativeReasoning', "RuleBasedRelativeReasoning",
'EnviFormer', "EnviFormer",
'ApplicabilityDomain', "ApplicabilityDomain",
"EnzymeLink",
] ]
for model in MODELS: for model in MODELS:
obj_cls = apps.get_model("epdb", model) obj_cls = apps.get_model("epdb", model)
print(f"Localizing urls for {model}") print(f"Localizing urls for {model}")
obj_cls.objects.update( 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 django.urls import reverse
from urllib.parse import quote from urllib.parse import quote
class LoginRequiredMiddleware: class LoginRequiredMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
self.exempt_urls = [ self.exempt_urls = [
reverse('login'), reverse("login"),
reverse('logout'), reverse("logout"),
reverse('admin:login'), reverse("admin:login"),
reverse('admin:index'), reverse("admin:index"),
] + getattr(settings, 'LOGIN_EXEMPT_URLS', []) ] + getattr(settings, "LOGIN_EXEMPT_URLS", [])
def __call__(self, request): def __call__(self, request):
if not request.user.is_authenticated: if not request.user.is_authenticated:
path = request.path_info path = request.path_info
if not any(path.startswith(url) for url in self.exempt_urls): if not any(path.startswith(url) for url in self.exempt_urls):
if request.method == 'GET': if request.method == "GET":
if request.get_full_path() and request.get_full_path() != '/': if request.get_full_path() and request.get_full_path() != "/":
return redirect(f"{settings.LOGIN_URL}?next={quote(request.get_full_path())}") return redirect(
f"{settings.LOGIN_URL}?next={quote(request.get_full_path())}"
)
return redirect(settings.LOGIN_URL) return redirect(settings.LOGIN_URL)
return self.get_response(request) 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 typing import Optional
from celery import shared_task 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 from epdb.logic import SPathway
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@shared_task(queue='background') @shared_task(queue="background")
def mul(a, b): def mul(a, b):
return a * b return a * b
@shared_task(queue='predict') @shared_task(queue="predict")
def predict_simple(model_pk: int, smiles: str): def predict_simple(model_pk: int, smiles: str):
mod = EPModel.objects.get(id=model_pk) mod = EPModel.objects.get(id=model_pk)
res = mod.predict(smiles) res = mod.predict(smiles)
return res return res
@shared_task(queue='background') @shared_task(queue="background")
def send_registration_mail(user_pk: int): def send_registration_mail(user_pk: int):
pass pass
@shared_task(queue='model') @shared_task(queue="model")
def build_model(model_pk: int): def build_model(model_pk: int):
mod = EPModel.objects.get(id=model_pk) mod = EPModel.objects.get(id=model_pk)
mod.build_dataset() mod.build_dataset()
mod.build_model() mod.build_model()
@shared_task(queue='model') @shared_task(queue="model")
def evaluate_model(model_pk: int): def evaluate_model(model_pk: int):
mod = EPModel.objects.get(id=model_pk) mod = EPModel.objects.get(id=model_pk)
mod.evaluate_model() mod.evaluate_model()
@shared_task(queue='model') @shared_task(queue="model")
def retrain(model_pk: int): def retrain(model_pk: int):
mod = EPModel.objects.get(id=model_pk) mod = EPModel.objects.get(id=model_pk)
mod.retrain() mod.retrain()
@shared_task(queue='predict') @shared_task(queue="predict")
def predict(pw_pk: int, pred_setting_pk: int, limit: Optional[int] = None, node_pk: Optional[int] = None) -> Pathway: 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) pw = Pathway.objects.get(id=pw_pk)
setting = Setting.objects.get(id=pred_setting_pk) setting = Setting.objects.get(id=pred_setting_pk)
pw.kv.update(**{'status': 'running'}) pw.kv.update(**{"status": "running"})
pw.save() pw.save()
try: try:
@ -74,12 +76,10 @@ def predict(pw_pk: int, pred_setting_pk: int, limit: Optional[int] = None, node_
else: else:
raise ValueError("Neither limit nor node_pk given!") raise ValueError("Neither limit nor node_pk given!")
except Exception as e: except Exception as e:
pw.kv.update({'status': 'failed'}) pw.kv.update({"status": "failed"})
pw.save() pw.save()
raise e raise e
pw.kv.update(**{'status': 'completed'}) pw.kv.update(**{"status": "completed"})
pw.save() pw.save()

View File

@ -2,6 +2,7 @@ from django import template
register = template.Library() register = template.Library()
@register.filter @register.filter
def classname(obj): 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.contrib.auth import views as auth_views
from django.urls import path, re_path
from . import views as v 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 = [ urlpatterns = [
# Home # Home
re_path(r'^$', v.index, name='index'), re_path(r"^$", v.index, name="index"),
# Login # Login
re_path(r'^login', v.login, name='login'), re_path(r"^login", v.login, name="login"),
re_path(r'^logout', v.logout, name='logout'), re_path(r"^logout", v.logout, name="logout"),
re_path(r'^register', v.register, name='register'), re_path(r"^register", v.register, name="register"),
# Built-In views
# Built In views path(
path('password_reset/', auth_views.PasswordResetView.as_view( "password_reset/",
template_name='static/password_reset_form.html' auth_views.PasswordResetView.as_view(template_name="static/password_reset_form.html"),
), name='password_reset'), name="password_reset",
),
path('password_reset/done/', auth_views.PasswordResetDoneView.as_view( path(
template_name='static/password_reset_done.html' "password_reset/done/",
), name='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' path(
), name='password_reset_confirm'), "reset/<uidb64>/<token>/",
auth_views.PasswordResetConfirmView.as_view(
path('reset/done/', auth_views.PasswordResetCompleteView.as_view( template_name="static/password_reset_confirm.html"
template_name='static/password_reset_complete.html' ),
), name='password_reset_complete'), 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 # Top level urls
re_path(r'^package$', v.packages, name='packages'), re_path(r"^package$", v.packages, name="packages"),
re_path(r'^compound$', v.compounds, name='compounds'), re_path(r"^compound$", v.compounds, name="compounds"),
re_path(r'^rule$', v.rules, name='rules'), re_path(r"^rule$", v.rules, name="rules"),
re_path(r'^reaction$', v.reactions, name='reactions'), re_path(r"^reaction$", v.reactions, name="reactions"),
re_path(r'^pathway$', v.pathways, name='pathways'), re_path(r"^pathway$", v.pathways, name="pathways"),
re_path(r'^scenario$', v.scenarios, name='scenarios'), re_path(r"^scenario$", v.scenarios, name="scenarios"),
re_path(r'^model$', v.models, name='model'), re_path(r"^model$", v.models, name="model"),
re_path(r'^user$', v.users, name='users'), re_path(r"^user$", v.users, name="users"),
re_path(r'^group$', v.groups, name='groups'), re_path(r"^group$", v.groups, name="groups"),
re_path(r'^search$', v.search, name='search'), re_path(r"^search$", v.search, name="search"),
# User Detail # 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 # 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 # "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 # Compound
re_path(rf'^package/(?P<package_uuid>{UUID})/compound$', v.package_compounds, name='package compound list'), re_path(
re_path(rf'^package/(?P<package_uuid>{UUID})/compound/(?P<compound_uuid>{UUID})$', v.package_compound, name='package compound detail'), 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 # 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(
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'), 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 # 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$", 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(
re_path(rf'^package/(?P<package_uuid>{UUID})/simple-ambit-rule/(?P<rule_uuid>{UUID})$', v.package_rule, name='package rule detail'), rf"^package/(?P<package_uuid>{UUID})/rule/(?P<rule_uuid>{UUID})$",
re_path(rf'^package/(?P<package_uuid>{UUID})/simple-rdkit-rule/(?P<rule_uuid>{UUID})$', v.package_rule, name='package rule detail'), v.package_rule,
re_path(rf'^package/(?P<package_uuid>{UUID})/parallel-rule/(?P<rule_uuid>{UUID})$', v.package_rule, name='package rule detail'), 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})/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 # Reaction
re_path(rf'^package/(?P<package_uuid>{UUID})/reaction$', v.package_reactions, name='package reaction list'), re_path(
re_path(rf'^package/(?P<package_uuid>{UUID})/reaction/(?P<reaction_uuid>{UUID})$', v.package_reaction, name='package reaction detail'), 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 # # Pathway
re_path(rf'^package/(?P<package_uuid>{UUID})/pathway$', v.package_pathways, name='package pathway list'), re_path(
re_path(rf'^package/(?P<package_uuid>{UUID})/pathway/(?P<pathway_uuid>{UUID})$', v.package_pathway, name='package pathway detail'), 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 # 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(
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'), 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 # 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(
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'), 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 # Scenario
re_path(rf'^package/(?P<package_uuid>{UUID})/scenario$', v.package_scenarios, name='package scenario list'), re_path(
re_path(rf'^package/(?P<package_uuid>{UUID})/scenario/(?P<scenario_uuid>{UUID})$', v.package_scenario, name='package scenario detail'), 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 # Model
re_path(rf'^package/(?P<package_uuid>{UUID})/model$', v.package_models, name='package model list'), re_path(
re_path(rf'^package/(?P<package_uuid>{UUID})/model/(?P<model_uuid>{UUID})$', v.package_model,name='package model detail'), rf"^package/(?P<package_uuid>{UUID})/model$", v.package_models, name="package model list"
),
re_path(r'^setting$', v.settings, name='settings'), re_path(
re_path(rf'^setting/(?P<setting_uuid>{UUID})', v.setting, name='setting'), rf"^package/(?P<package_uuid>{UUID})/model/(?P<model_uuid>{UUID})$",
v.package_model,
re_path(r'^indigo/info$', v.indigo, name='indigo_info'), name="package model detail",
re_path(r'^indigo/aromatize$', v.aromatize, name='indigo_aromatize'), ),
re_path(r'^indigo/dearomatize$', v.dearomatize, name='indigo_dearomatize'), re_path(r"^setting$", v.settings, name="settings"),
re_path(r'^indigo/layout$', v.layout, name='indigo_layout'), 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'^depict$', v.depict, name='depict'), 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 # OAuth Stuff
path("o/userinfo/", v.userinfo, name="oauth_userinfo"), 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})/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})/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/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 gzip
import json import json
import logging
import os.path import os.path
from datetime import datetime
from django.conf import settings as s from django.conf import settings as s
from django.http import HttpResponseNotAllowed
from django.shortcuts import render from django.shortcuts import render
from epdb.logic import PackageManager 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 epdb.views import get_base_context, _anonymous_or_real
from utilities.chem import FormatConverter 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): def migration(request):
if request.method == 'GET': if request.method == "GET":
context = get_base_context(request) context = get_base_context(request)
if os.path.exists(s.BASE_DIR / 'fixtures' / 'migration_status_per_rule.json') and request.GET.get( if (
"force") is None: os.path.exists(s.BASE_DIR / "fixtures" / "migration_status_per_rule.json")
migration_status = json.load(open(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: else:
data = json.load(gzip.open(s.BASE_DIR / 'fixtures' / 'ambit_rules.json.gz', 'rb')) BBD = Package.objects.get(
url="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1"
results = [] )
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 success = 0
error = 0 error = 0
total = 0 total = 0
num_keys = len(data.keys()) for i, r in enumerate(RULES):
for i, bt_rule_name in enumerate(data.keys()): logger.debug(f"\r{i + 1:03d}/{num_rules}")
print(f"{i + 1}/{num_keys}")
bt_rule = data[bt_rule_name]
smirks = bt_rule['smirks']
all_prods = set()
res = True 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']): res &= set(ambit_res) == set(rdkit_res)
except Exception as e:
products = FormatConverter.apply(comp['smiles'], smirks) logger.error(e)
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
results.append( results.append(
{ {
'name': bt_rule_name, "name": r.name,
'id': bt_rule['id'].split('/')[-1], "detail_url": s.SERVER_URL
'url': bt_rule['id'], + "/migration/"
'status': res, + r.url.replace("https://envipath.org/", "").replace(
'detail_url': s.SERVER_URL + '/migration/' + bt_rule['id'].replace('https://envipath.org/', '') "http://localhost:8000/", ""
),
"id": str(r.uuid),
"url": r.url,
"status": res,
} }
) )
@ -84,95 +127,82 @@ def migration(request):
error += 1 error += 1
total += 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 = { migration_status = {
'results': results, "results": results,
'success': success, "success": success,
'error': error, "error": error,
'total': total "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']: for r in migration_status["results"]:
r['detail_url'] = r['detail_url'].replace('http://localhost:8000', s.SERVER_URL) r["detail_url"] = r["detail_url"].replace(
"http://localhost:8000", s.SERVER_URL
)
context.update(**migration_status) context.update(**migration_status)
return render(request, 'migration.html', context) return render(request, "migration.html", context)
def migration_detail(request, package_uuid, rule_uuid): def migration_detail(request, package_uuid, rule_uuid):
current_user = _anonymous_or_real(request) current_user = _anonymous_or_real(request)
if request.method == 'GET': if request.method == "GET":
context = get_base_context(request) context = get_base_context(request)
p = PackageManager.get_package_by_id(current_user, package_uuid) BBD = Package.objects.get(name="EAWAG-BBD")
rule = Rule.objects.get(package=p, uuid=rule_uuid) STRUCTURES = CompoundStructure.objects.filter(compound__package=BBD)
rule = Rule.objects.get(package=BBD, uuid=rule_uuid)
bt_rule_name = rule.name bt_rule_name = rule.name
smirks = rule.smirks
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 = []
res = True res = True
results = []
all_prods = set() all_prods = set()
for comp, ambit_prod in zip(bt_rule['compounds'], bt_rule['products']): for structure in STRUCTURES:
# if comp['smiles'] != 'CC1=C(C(=C(C=N1)CO)C=O)O': ambit_smiles, ambit_errors, rdkit_smiles, rdkit_errors = run_both_engines(
# continue structure.smiles, smirks
)
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: for x in ambit_smiles:
all_prods.add(x) all_prods.add(x)
# TODO mode "intersection" # TODO mode "intersection"
# partial_res = (len(set(ambit_smiles).intersection(set(rdkit_smiles))) > 0) or (len(ambit_smiles) == 0) # 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" # TODO mode = "full ambit"
# partial_res = len(set(ambit_smiles).intersection(set(rdkit_smiles))) == len(ambit_smiles) # partial_res = len(set(ambit_smiles).intersection(set(rdkit_smiles))) == len(set(ambit_smiles))
# FAILED (failures=46) # FAILED (failures=34)
# TODO mode = "equality" # TODO mode = "equality"
partial_res = set(ambit_smiles) == set(rdkit_smiles) partial_res = set(ambit_smiles) == set(rdkit_smiles)
# FAILED (failures=69) # FAILED (failures=30)
#
if len(ambit_smiles) or len(rdkit_smiles): if len(ambit_smiles) or len(rdkit_smiles):
temp = { temp = {
'url': comp['id'], "url": structure.url,
'id': comp['id'].split('/')[-1], "id": str(structure.uuid),
'name': comp['name'], "name": structure.name,
'initial_smiles': comp['smiles'], "initial_smiles": structure.smiles,
'ambit_smiles': sorted(list(ambit_smiles)), "ambit_smiles": sorted(list(ambit_smiles)),
'rdkit_smiles': sorted(list(rdkit_smiles)), "rdkit_smiles": sorted(list(rdkit_smiles)),
'status': set(ambit_smiles) == set(rdkit_smiles), "status": set(ambit_smiles) == set(rdkit_smiles),
} }
detail = f"""
if set(ambit_smiles) != set(rdkit_smiles):
detail = f"""
BT: {bt_rule_name} BT: {bt_rule_name}
SMIRKS: {bt_rule['smirks']} SMIRKS: {smirks}
Compound: {comp['smiles']} Compound: {structure.smiles}
Compound URL: {comp['id']} Compound URL: {structure.url}
Num ambit: {len(set(ambit_smiles))} Num ambit: {len(set(ambit_smiles))}
Num rdkit: {len(set(rdkit_smiles))} Num rdkit: {len(set(rdkit_smiles))}
Num Intersection A: {len(set(ambit_smiles).intersection(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} rdkit_errors: {rdkit_errors}
""" """
temp['detail'] = '\n'.join([x.strip() for x in detail.split('\n')]) temp["detail"] = "\n".join([x.strip() for x in detail.split("\n")])
# print(detail.strip())
results.append(temp) results.append(temp)
res &= partial_res res &= partial_res
results = sorted(results, key=lambda x: x['status']) results = sorted(results, key=lambda x: x["status"])
context['results'] = results context["results"] = results
context['res'] = res context["res"] = res
context['bt_rule_name'] = bt_rule_name context["bt_rule_name"] = bt_rule_name
return render(request, 'migration_detail.html', context) 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" version = "0.1.0"
description = "Add your description here" description = "Add your description here"
readme = "README.md" readme = "README.md"
requires-python = ">=3.10" requires-python = ">=3.12"
dependencies = [ dependencies = [
"celery>=5.5.2", "celery>=5.5.2",
"django>=5.2.1", "django>=5.2.1",
@ -12,9 +12,9 @@ dependencies = [
"django-ninja>=1.4.1", "django-ninja>=1.4.1",
"django-oauth-toolkit>=3.0.1", "django-oauth-toolkit>=3.0.1",
"django-polymorphic>=4.1.0", "django-polymorphic>=4.1.0",
"django-stubs>=5.2.4",
"enviformer", "enviformer",
"envipy-additional-information", "envipy-additional-information",
"envipy-ambit>=0.1.0",
"envipy-plugins", "envipy-plugins",
"epam-indigo>=1.30.1", "epam-indigo>=1.30.1",
"gunicorn>=23.0.0", "gunicorn>=23.0.0",
@ -30,9 +30,62 @@ dependencies = [
] ]
[tool.uv.sources] [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-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] [project.optional-dependencies]
ms-login = ["msal>=1.33.0"] 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; var reactivityCentersImgSrc = null;
if (data['assessment']['node'] !== undefined) { if (data['assessment']['node'] !== undefined) {
functionalGroupsImgSrc = "<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'] + "'>" reactivityCentersImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "&highlightReactivity=true'>"
} else { } else {
functionalGroupsImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">"; functionalGroupsImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">";
reactivityCentersImgSrc = "<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 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 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 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']) + "'>"; 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); $("#appDomainAssessmentResultTable").append(res);
} }

View File

@ -444,6 +444,13 @@ function serializeSVG(svgElement) {
line.setAttribute("fill", style.fill); 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(); const serializer = new XMLSerializer();
let svgString = serializer.serializeToString(svgElement); let svgString = serializer.serializeToString(svgElement);
@ -455,7 +462,26 @@ function serializeSVG(svgElement) {
return svgString; 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') { function downloadSVG(svgElement, filename = 'chart.svg') {
shrinkSVG("#" + svgElement.id);
const svgString = serializeSVG(svgElement); const svgString = serializeSVG(svgElement);
const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'}); const blob = new Blob([svgString], {type: 'image/svg+xml;charset=utf-8'});
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);

View File

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

View File

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

View File

@ -1,4 +1,8 @@
{% if meta.can_edit %} {% 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> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a> <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"> <a role="button" data-toggle="modal" data-target="#edit_node_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Node</a> <i class="glyphicon glyphicon-edit"></i> Edit Node</a>
</li> </li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a> <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"> <a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a> <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li> </li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
{# <li>#} {# <li>#}
{# <a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal">#} {# <a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal">#}
{# <i class="glyphicon glyphicon-plus"></i> Calculate Compound Properties</a>#} {# <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"> <a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Pathway</a> <i class="glyphicon glyphicon-trash"></i> Delete Pathway</a>
</li> </li>
{% endif %} {% endif %}

View File

@ -3,10 +3,18 @@
<a role="button" data-toggle="modal" data-target="#edit_reaction_modal"> <a role="button" data-toggle="modal" data-target="#edit_reaction_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Reaction</a> <i class="glyphicon glyphicon-edit"></i> Edit Reaction</a>
</li> </li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a> <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li> </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 %} {% endif %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal"> <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"> <a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a> <i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
</li> </li>
{% endif %} {% endif %}

View File

@ -3,6 +3,10 @@
<a role="button" data-toggle="modal" data-target="#edit_rule_modal"> <a role="button" data-toggle="modal" data-target="#edit_rule_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Rule</a> <i class="glyphicon glyphicon-edit"></i> Edit Rule</a>
</li> </li>
<li>
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
</li>
<li> <li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a> <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
@ -17,4 +21,4 @@
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Rule</a> <i class="glyphicon glyphicon-trash"></i> Delete Rule</a>
</li> </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> <h4 class="alert-heading">{{ error_message }}</h4>
<hr> <hr>
<p class="mb-0"> <p class="mb-0">
{{ error_detail }}<br> {{ error_detail }}
The error was logged and will be investigated.
</p> </p>
</div> </div>

View File

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

View File

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

View File

@ -1,50 +1,45 @@
{% extends "framework.html" %} {% extends "framework.html" %}
{% block content %} {% block content %}
<div class="panel-group" id="migration-detail"> <div class="panel-group" id="migration-detail">
<div class="panel panel-default"> <div class="panel panel-default">
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px"> <div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
Migration Status for {{ bt_rule_name }} 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> </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> </div>
{% endfor %}
</div> </div>
</div>
<script> <script>
</script> </script>
{% endblock content %} {% endblock content %}

View File

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

View File

@ -29,11 +29,11 @@
<tr> <tr>
<th> <th>
<input type="number" id="dateYear" name="scenario-date-year" class="form-control" <input type="number" id="dateYear" name="scenario-date-year" class="form-control"
placeholder="YYYY"> placeholder="YYYY" max="{% now "Y" %}">
</th> </th>
<th> <th>
<input type="number" id="dateMonth" name="scenario-date-month" min="1" max="12" <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>
<th> <th>
<input type="number" id="dateDay" name="scenario-date-day" min="1" max="31" class="form-control" <input type="number" id="dateDay" name="scenario-date-day" min="1" max="31" class="form-control"
@ -88,8 +88,15 @@
$('#new_scenario_form').submit(); $('#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> </script>

View File

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

View File

@ -23,6 +23,8 @@
</select> </select>
<input type="hidden" name="hidden" value="copy"> <input type="hidden" name="hidden" value="copy">
</form> </form>
<div id="copy-object-error-message" class="alert alert-danger" role="alert" style="display: none">
</div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <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) { $('#generic-copy-object-modal-form-submit').click(function (e) {
e.preventDefault(); e.preventDefault();
$('#copy-object-error-message').hide()
const packageUrl = $('#target-package').find(":selected").val(); const packageUrl = $('#target-package').find(":selected").val();
@ -49,12 +52,22 @@
object_to_copy: '{{ current_object.url }}', object_to_copy: '{{ current_object.url }}',
} }
$.post(packageUrl, formData, function (response) { $.ajax({
if (response.success) { type: 'post',
window.location.href = response.success; 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" <select id="scenario-select" name="selected-scenarios" data-actions-box='true' class="form-control"
multiple data-width='100%'> multiple data-width='100%'>
<option disabled>Select Scenarios</option> <option disabled>Select Scenarios</option>
<option value="" hidden></option>
</select> </select>
</form> </form>
</div> </div>
@ -64,6 +65,9 @@
$('#set_scenario_modal_form_submit').on('click', function (e) { $('#set_scenario_modal_form_submit').on('click', function (e) {
e.preventDefault(); e.preventDefault();
if ($('#scenario-select').val().length == 0) {
$('#scenario-select').val("")
}
$('#set_scenario_modal_form').submit(); $('#set_scenario_modal_form').submit();
}); });
}); });

View File

@ -2,12 +2,13 @@
{% block content %} {% block content %}
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_rule_modal.html" %} {% include "modals/objects/edit_rule_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %} {% include "modals/objects/generic_set_aliases_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %} {% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %} {% include "modals/objects/generic_copy_object_modal.html" %}
{% endblock action_modals %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="rule-detail"> <div class="panel-group" id="rule-detail">
<div class="panel panel-default"> <div class="panel panel-default">
@ -28,10 +29,27 @@
</div> </div>
<div class="panel-body"> <div class="panel-body">
<p> <p>
{{ rule.description }} {{ rule.description|safe }}
</p> </p>
</div> </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 --> <!-- Reaction Patterns -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">
@ -69,19 +87,41 @@
</div> </div>
{% endif %} {% endif %}
<!-- EC Numbers --> {% if rule.enzymelinks %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <!-- EC Numbers -->
<h4 class="panel-title"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<a id="rule-ec-numbers-link" data-toggle="collapse" data-parent="#rule-detail" <h4 class="panel-title">
href="#rule-ec-numbers">EC Numbers</a> <a id="rule-ec-numbers-link" data-toggle="collapse" data-parent="#rule-detail"
</h4> href="#rule-ec-numbers">EC Numbers</a>
</div> </h4>
<div id="rule-ec-numbers" class="panel-collapse collapse">
<div class="panel-body list-group-item">
</div> </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>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -4,8 +4,10 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_compound_modal.html" %} {% 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/add_structure_modal.html" %}
{% include "modals/objects/generic_set_scenario_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_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}
@ -36,6 +38,23 @@
</p> </p>
</div> </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 --> <!-- Description -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">

View File

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

View File

@ -4,6 +4,7 @@
{% block action_modals %} {% block action_modals %}
{# {% include "modals/objects/edit_edge_modal.html" %}#} {# {% 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_set_scenario_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}
@ -39,6 +40,23 @@
</div> </div>
</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 --> <!-- Image -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <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) { $("#predict-button").on("click", function (e) {
e.preventDefault(); e.preventDefault();
clear("predictResultTable");
data = { data = {
"smiles": $("#smiles-to-predict").val(), "smiles": $("#smiles-to-predict").val(),
"classify": "ILikeCats!" "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' %}"); makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
$.ajax({ $.ajax({
@ -332,17 +338,17 @@
$("#predictLoading").empty(); $("#predictLoading").empty();
handlePredictionResponse(data); handlePredictionResponse(data);
} catch (error) { } catch (error) {
console.log("Error");
$("#predictLoading").empty(); $("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger"); $("#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(); $("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger"); $("#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) { $("#assess-button").on("click", function (e) {
e.preventDefault(); e.preventDefault();
clear("appDomainAssessmentResultTable");
data = { data = {
"smiles": $("#smiles-to-assess").val(), "smiles": $("#smiles-to-assess").val(),
"app-domain-assessment": "ILikeCats!" "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' %}"); makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
$.ajax({ $.ajax({
@ -369,16 +383,15 @@
handleAssessmentResponse("{% url 'depict' %}", data); handleAssessmentResponse("{% url 'depict' %}", data);
console.log(data); console.log(data);
} catch (error) { } catch (error) {
console.log("Error");
$("#appDomainLoading").empty(); $("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger"); $("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Error while processing request :/"); $("#appDomainAssessmentResultTable").append("Error while processing response :/");
} }
}, },
error: function (jqXHR, textStatus, errorThrown) { error: function (jqXHR, textStatus, errorThrown) {
$("#appDomainLoading").empty(); $("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger"); $("#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 %} {% block action_modals %}
{% include "modals/objects/edit_node_modal.html" %} {% 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_set_scenario_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}
@ -42,6 +43,23 @@
</div> </div>
</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 --> <!-- Image -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">

View File

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

View File

@ -85,6 +85,7 @@
{% include "modals/objects/download_pathway_image_modal.html" %} {% include "modals/objects/download_pathway_image_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %} {% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/edit_pathway_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/generic_set_scenario_modal.html" %}
{% include "modals/objects/delete_pathway_node_modal.html" %} {% include "modals/objects/delete_pathway_node_modal.html" %}
{% include "modals/objects/delete_pathway_edge_modal.html" %} {% include "modals/objects/delete_pathway_edge_modal.html" %}
@ -176,9 +177,6 @@
</nav> </nav>
<div id="vizdiv" > <div id="vizdiv" >
<svg id="pwsvg"> <svg id="pwsvg">
{% if debug %}
<rect width="100%" height="100%" fill="aliceblue"/>
{% endif %}
<defs> <defs>
<marker id="arrow" viewBox="0 0 10 10" refX="43" refY="5" markerWidth="6" markerHeight="6" <marker id="arrow" viewBox="0 0 10 10" refX="43" refY="5" markerWidth="6" markerHeight="6"
orient="auto-start-reverse" markerUnits="userSpaceOnUse"> orient="auto-start-reverse" markerUnits="userSpaceOnUse">
@ -210,6 +208,23 @@
</div> </div>
</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 %} {% if pathway.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">

View File

@ -4,8 +4,10 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_reaction_modal.html" %} {% 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_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_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" %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}
@ -40,6 +42,23 @@
</div> </div>
</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 --> <!-- Image -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">
@ -105,6 +124,23 @@
</div> </div>
{% endif %} {% 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 %} {% if reaction.related_pathways %}
<!-- Pathways --> <!-- Pathways -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">

View File

@ -4,6 +4,7 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_rule_modal.html" %} {% 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_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %} {% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %} {% include "modals/objects/generic_delete_modal.html" %}
@ -32,6 +33,23 @@
</p> </p>
</div> </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 --> <!-- Representation -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">
@ -183,6 +201,43 @@
</div> </div>
</div> </div>
{% endif %} {% 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>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -13,91 +13,80 @@ class CompoundTest(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(CompoundTest, cls).setUpClass() super(CompoundTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous') cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc') cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
def test_smoke(self): def test_smoke(self):
c = Compound.create( c = Compound.create(
self.package, 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",
name='Afoxolaner', name="Afoxolaner",
description='No Desc' description="No Desc",
) )
self.assertEqual(c.default_structure.smiles, self.assertEqual(
'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') c.default_structure.smiles,
self.assertEqual(c.name, 'Afoxolaner') "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.description, 'No Desc') )
self.assertEqual(c.name, "Afoxolaner")
self.assertEqual(c.description, "No Desc")
def test_missing_smiles(self): def test_missing_smiles(self):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
_ = Compound.create( _ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
self.package,
smiles=None,
name='Afoxolaner',
description='No Desc'
)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
_ = Compound.create( _ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
self.package,
smiles='',
name='Afoxolaner',
description='No Desc'
)
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
_ = Compound.create( _ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
self.package,
smiles=' ',
name='Afoxolaner',
description='No Desc'
)
def test_smiles_are_trimmed(self): def test_smiles_are_trimmed(self):
c = Compound.create( c = Compound.create(
self.package, 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 ",
name='Afoxolaner', name="Afoxolaner",
description='No Desc' description="No Desc",
) )
self.assertEqual(c.default_structure.smiles, self.assertEqual(
'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') 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): def test_name_and_description_optional(self):
c = Compound.create( c = Compound.create(
self.package, 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.name, "Compound 1")
self.assertEqual(c.description, 'no description') self.assertEqual(c.description, "no description")
def test_empty_name_and_description_are_ignored(self): def test_empty_name_and_description_are_ignored(self):
c = Compound.create( c = Compound.create(
self.package, 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",
name='', name="",
description='', description="",
) )
self.assertEqual(c.name, 'Compound 1') self.assertEqual(c.name, "Compound 1")
self.assertEqual(c.description, 'no description') self.assertEqual(c.description, "no description")
def test_deduplication(self): def test_deduplication(self):
c1 = Compound.create( c1 = Compound.create(
self.package, 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",
name='Afoxolaner', name="Afoxolaner",
description='No Desc' description="No Desc",
) )
c2 = Compound.create( c2 = Compound.create(
self.package, 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",
name='Afoxolaner', name="Afoxolaner",
description='No Desc' description="No Desc",
) )
# Check if create detects that this Compound already exist # Check if create detects that this Compound already exist
@ -109,36 +98,36 @@ class CompoundTest(TestCase):
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
_ = Compound.create( _ = Compound.create(
self.package, 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', 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', name="Afoxolaner",
description='No Desc' description="No Desc",
) )
def test_create_with_standardized_smiles(self): def test_create_with_standardized_smiles(self):
c = Compound.create( c = Compound.create(
self.package, self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1', smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name='Standardized SMILES', name="Standardized SMILES",
description='No Desc' description="No Desc",
) )
self.assertEqual(len(c.structures.all()), 1) self.assertEqual(len(c.structures.all()), 1)
cs = c.structures.all()[0] cs = c.structures.all()[0]
self.assertEqual(cs.normalized_structure, True) 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): def test_create_with_non_standardized_smiles(self):
c = Compound.create( c = Compound.create(
self.package, self.package,
smiles='[O-][N+](=O)c1ccc(C(=O)[O-])cc1', smiles="[O-][N+](=O)c1ccc(C(=O)[O-])cc1",
name='Non Standardized SMILES', name="Non Standardized SMILES",
description='No Desc' description="No Desc",
) )
self.assertEqual(len(c.structures.all()), 2) self.assertEqual(len(c.structures.all()), 2)
for cs in c.structures.all(): for cs in c.structures.all():
if cs.normalized_structure: 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 break
else: else:
# Loop finished without break, lets fail... # Loop finished without break, lets fail...
@ -147,51 +136,54 @@ class CompoundTest(TestCase):
def test_add_structure_smoke(self): def test_add_structure_smoke(self):
c = Compound.create( c = Compound.create(
self.package, self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1', smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name='Standardized SMILES', name="Standardized SMILES",
description='No Desc' 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) self.assertEqual(len(c.structures.all()), 2)
def test_add_structure_with_different_normalized_smiles(self): def test_add_structure_with_different_normalized_smiles(self):
c = Compound.create( c = Compound.create(
self.package, self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1', smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name='Standardized SMILES', name="Standardized SMILES",
description='No Desc' description="No Desc",
) )
with self.assertRaises(ValueError): with self.assertRaises(ValueError):
c.add_structure( 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', "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') "Different Standardized SMILES",
)
def test_delete(self): def test_delete(self):
c = Compound.create( c = Compound.create(
self.package, self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1', smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name='Standardization Test', name="Standardization Test",
description='No Desc' description="No Desc",
) )
c.delete() c.delete()
self.assertEqual(Compound.objects.filter(package=self.package).count(), 0) 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): def test_set_as_default_structure(self):
c1 = Compound.create( c1 = Compound.create(
self.package, self.package,
smiles='O=C(O)C1=CC=C([N+](=O)[O-])C=C1', smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
name='Standardized SMILES', name="Standardized SMILES",
description='No Desc' description="No Desc",
) )
default_structure = c1.default_structure 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) c1.set_default_structure(c2)
self.assertNotEqual(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 django.test import TestCase
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import Compound, User, Reaction from epdb.models import Compound, User, Reaction
@ -12,50 +11,47 @@ class CopyTest(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(CopyTest, cls).setUpClass() super(CopyTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous') cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, 'Source Package', 'No Desc') cls.package = PackageManager.create_package(cls.user, "Source Package", "No Desc")
cls.AFOXOLANER = Compound.create( cls.AFOXOLANER = Compound.create(
cls.package, 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', 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', name="Afoxolaner",
description='Test compound for copying' description="Test compound for copying",
) )
cls.FOUR_NITROBENZOIC_ACID = Compound.create( cls.FOUR_NITROBENZOIC_ACID = Compound.create(
cls.package, cls.package,
smiles='[O-][N+](=O)c1ccc(C(=O)[O-])cc1', # Normalized: O=C(O)C1=CC=C([N+](=O)[O-])C=C1', smiles="[O-][N+](=O)c1ccc(C(=O)[O-])cc1", # Normalized: O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Test Compound', name="Test Compound",
description='Compound with multiple structures' description="Compound with multiple structures",
) )
cls.ETHANOL = Compound.create( cls.ETHANOL = Compound.create(
cls.package, cls.package, smiles="CCO", name="Ethanol", description="Simple alcohol"
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.reaction_educt = Compound.create(
cls.package, cls.package,
smiles='C(CCl)Cl', smiles="C(CCl)Cl",
name='1,2-Dichloroethane', name="1,2-Dichloroethane",
description='Eawag BBD compound c0001' description="Eawag BBD compound c0001",
).default_structure ).default_structure
cls.reaction_product = Compound.create( cls.reaction_product = Compound.create(
cls.package, cls.package,
smiles='C(CO)Cl', smiles="C(CO)Cl",
name='2-Chloroethanol', name="2-Chloroethanol",
description='Eawag BBD compound c0005' description="Eawag BBD compound c0005",
).default_structure ).default_structure
cls.REACTION = Reaction.create( cls.REACTION = Reaction.create(
package=cls.package, package=cls.package,
name='Eawag BBD reaction r0001', name="Eawag BBD reaction r0001",
educts=[cls.reaction_educt], educts=[cls.reaction_educt],
products=[cls.reaction_product], products=[cls.reaction_product],
multi_step=False multi_step=False,
) )
def test_compound_copy_basic(self): def test_compound_copy_basic(self):
@ -68,7 +64,9 @@ class CopyTest(TestCase):
self.assertEqual(self.AFOXOLANER.description, copied_compound.description) self.assertEqual(self.AFOXOLANER.description, copied_compound.description)
self.assertEqual(copied_compound.package, self.target_package) self.assertEqual(copied_compound.package, self.target_package)
self.assertEqual(self.AFOXOLANER.package, self.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): def test_compound_copy_with_multiple_structures(self):
"""Test copying a compound with multiple structures""" """Test copying a compound with multiple structures"""
@ -86,7 +84,7 @@ class CopyTest(TestCase):
self.assertIsNotNone(copied_compound.default_structure) self.assertIsNotNone(copied_compound.default_structure)
self.assertEqual( self.assertEqual(
copied_compound.default_structure.smiles, 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): def test_compound_copy_preserves_aliases(self):
@ -95,15 +93,15 @@ class CopyTest(TestCase):
original_compound = self.ETHANOL original_compound = self.ETHANOL
# Add aliases if the method exists # Add aliases if the method exists
if hasattr(original_compound, 'add_alias'): if hasattr(original_compound, "add_alias"):
original_compound.add_alias('Ethyl alcohol') original_compound.add_alias("Ethyl alcohol")
original_compound.add_alias('Grain alcohol') original_compound.add_alias("Grain alcohol")
mapping = dict() mapping = dict()
copied_compound = original_compound.copy(self.target_package, mapping) copied_compound = original_compound.copy(self.target_package, mapping)
# Verify aliases were copied if they exist # 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 original_aliases = original_compound.aliases
copied_aliases = copied_compound.aliases copied_aliases = copied_compound.aliases
self.assertEqual(original_aliases, copied_aliases) self.assertEqual(original_aliases, copied_aliases)
@ -113,10 +111,10 @@ class CopyTest(TestCase):
original_compound = self.ETHANOL original_compound = self.ETHANOL
# Add external identifiers if the methods exist # Add external identifiers if the methods exist
if hasattr(original_compound, 'add_cas_number'): if hasattr(original_compound, "add_cas_number"):
original_compound.add_cas_number('64-17-5') original_compound.add_cas_number("64-17-5")
if hasattr(original_compound, 'add_pubchem_compound_id'): if hasattr(original_compound, "add_pubchem_compound_id"):
original_compound.add_pubchem_compound_id('702') original_compound.add_pubchem_compound_id("702")
mapping = dict() mapping = dict()
copied_compound = original_compound.copy(self.target_package, mapping) 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.smiles, copied_structure.smiles)
self.assertEqual(original_structure.canonical_smiles, copied_structure.canonical_smiles) self.assertEqual(original_structure.canonical_smiles, copied_structure.canonical_smiles)
self.assertEqual(original_structure.inchikey, copied_structure.inchikey) 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 # Verify they are different objects
self.assertNotEqual(original_structure.uuid, copied_structure.uuid) 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.compound.package, self.package)
self.assertEqual(orig_educt.smiles, copy_educt.smiles) 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.assertNotEqual(orig_product.uuid, copy_product.uuid)
self.assertEqual(orig_product.name, copy_product.name) self.assertEqual(orig_product.name, copy_product.name)
self.assertEqual(orig_product.description, copy_product.description) self.assertEqual(orig_product.description, copy_product.description)

View File

@ -11,21 +11,21 @@ class DatasetTest(TestCase):
def setUp(self): def setUp(self):
self.cs1 = Compound.create( self.cs1 = Compound.create(
self.package, self.package,
name='2,6-Dibromohydroquinone', 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', 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', smiles="C1=C(C(=C(C=C1O)Br)O)Br",
).default_structure ).default_structure
self.cs2 = Compound.create( self.cs2 = Compound.create(
self.package, 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 ).default_structure
self.rule1 = Rule.create( self.rule1 = Rule.create(
rule_type='SimpleAmbitRule', rule_type="SimpleAmbitRule",
package=self.package, 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]', 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' description="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1/simple-ambit-rule/f6a56c0f-a4a0-4ee3-b006-d765b4767cf6",
) )
self.reaction1 = Reaction.create( self.reaction1 = Reaction.create(
@ -33,14 +33,14 @@ class DatasetTest(TestCase):
educts=[self.cs1], educts=[self.cs1],
products=[self.cs2], products=[self.cs2],
rules=[self.rule1], rules=[self.rule1],
multi_step=False multi_step=False,
) )
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(DatasetTest, cls).setUpClass() super(DatasetTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous') cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc') cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
def test_smoke(self): def test_smoke(self):
reactions = [r for r in Reaction.objects.filter(package=self.package)] 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): class FormatConverterTestCase(TestCase):
def test_standardization(self): def test_standardization(self):
smiles = 'C[n+]1c([N-](C))cccc1' smiles = "C[n+]1c([N-](C))cccc1"
standardized_smiles = FormatConverter.standardize(smiles) 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 django.test import TestCase
from epdb.logic import PackageManager from epdb.logic import PackageManager
from epdb.models import User, MLRelativeReasoning, RuleBasedRelativeReasoning, Package from epdb.models import User, MLRelativeReasoning, Package
class ModelTest(TestCase): class ModelTest(TestCase):
@ -13,9 +13,9 @@ class ModelTest(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(ModelTest, cls).setUpClass() super(ModelTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous') cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc') cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
cls.BBD_SUBSET = Package.objects.get(name='Fixtures') cls.BBD_SUBSET = Package.objects.get(name="Fixtures")
def test_smoke(self): def test_smoke(self):
with TemporaryDirectory() as tmpdir: with TemporaryDirectory() as tmpdir:
@ -24,7 +24,7 @@ class ModelTest(TestCase):
rule_package_objs = [self.BBD_SUBSET] rule_package_objs = [self.BBD_SUBSET]
data_package_objs = [self.BBD_SUBSET] data_package_objs = [self.BBD_SUBSET]
eval_packages_objs = [] eval_packages_objs = [self.BBD_SUBSET]
mod = MLRelativeReasoning.create( mod = MLRelativeReasoning.create(
self.package, self.package,
@ -32,8 +32,8 @@ class ModelTest(TestCase):
data_package_objs, data_package_objs,
eval_packages_objs, eval_packages_objs,
threshold=threshold, threshold=threshold,
name='ECC - BBD - 0.5', name="ECC - BBD - 0.5",
description='Created MLRelativeReasoning in Testcase', description="Created MLRelativeReasoning in Testcase",
) )
# mod = RuleBasedRelativeReasoning.create( # mod = RuleBasedRelativeReasoning.create(
@ -52,9 +52,9 @@ class ModelTest(TestCase):
mod.build_model() mod.build_model()
mod.multigen_eval = True mod.multigen_eval = True
mod.save() 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() products = dict()
for r in results: for r in results:
@ -62,8 +62,11 @@ class ModelTest(TestCase):
products[tuple(sorted(ps.product_set))] = (r.rule.name, r.probability) products[tuple(sorted(ps.product_set))] = (r.rule.name, r.probability)
expected = { expected = {
('CC=O', 'CCNC(=O)C1=CC(C)=CC=C1'): ('bt0243-4301', np.float64(0.33333333333333337)), ("CC=O", "CCNC(=O)C1=CC(C)=CC=C1"): (
('CC1=CC=CC(C(=O)O)=C1', 'CCNCC'): ('bt0430-4011', np.float64(0.25)), "bt0243-4301",
np.float64(0.33333333333333337),
),
("CC1=CC=CC(C(=O)O)=C1", "CCNCC"): ("bt0430-4011", np.float64(0.25)),
} }
self.assertEqual(products, expected) self.assertEqual(products, expected)

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@ -12,34 +12,32 @@ class SimpleAmbitRuleTest(TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(SimpleAmbitRuleTest, cls).setUpClass() super(SimpleAmbitRuleTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous') cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, 'Simple Ambit Rule Test Package', cls.package = PackageManager.create_package(
'Test Package for SimpleAmbitRule') cls.user, "Simple Ambit Rule Test Package", "Test Package for SimpleAmbitRule"
)
def test_create_basic_rule(self): def test_create_basic_rule(self):
"""Test creating a basic SimpleAmbitRule with minimal parameters.""" """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( rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
package=self.package,
smirks=smirks
)
self.assertIsInstance(rule, SimpleAmbitRule) self.assertIsInstance(rule, SimpleAmbitRule)
self.assertEqual(rule.smirks, smirks) self.assertEqual(rule.smirks, smirks)
self.assertEqual(rule.package, self.package) self.assertEqual(rule.package, self.package)
self.assertRegex(rule.name, r'Rule \d+') self.assertRegex(rule.name, r"Rule \d+")
self.assertEqual(rule.description, 'no description') self.assertEqual(rule.description, "no description")
self.assertIsNone(rule.reactant_filter_smarts) self.assertIsNone(rule.reactant_filter_smarts)
self.assertIsNone(rule.product_filter_smarts) self.assertIsNone(rule.product_filter_smarts)
def test_create_with_all_parameters(self): def test_create_with_all_parameters(self):
"""Test creating SimpleAmbitRule with all parameters.""" """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]' 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' name = "Test Rule"
description = 'A test biotransformation rule' description = "A test biotransformation rule"
reactant_filter = '[CH2X4]' reactant_filter = "[CH2X4]"
product_filter = '[OH]' product_filter = "[OH]"
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(
package=self.package, package=self.package,
@ -47,7 +45,7 @@ class SimpleAmbitRuleTest(TestCase):
description=description, description=description,
smirks=smirks, smirks=smirks,
reactant_filter_smarts=reactant_filter, reactant_filter_smarts=reactant_filter,
product_filter_smarts=product_filter product_filter_smarts=product_filter,
) )
self.assertEqual(rule.name, name) self.assertEqual(rule.name, name)
@ -60,127 +58,114 @@ class SimpleAmbitRuleTest(TestCase):
"""Test that SMIRKS is required for rule creation.""" """Test that SMIRKS is required for rule creation."""
with self.assertRaises(ValueError) as cm: with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(package=self.package, smirks=None) 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: with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(package=self.package, smirks='') SimpleAmbitRule.create(package=self.package, smirks="")
self.assertIn('SMIRKS is required', str(cm.exception)) self.assertIn("SMIRKS is required", str(cm.exception))
with self.assertRaises(ValueError) as cm: with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create(package=self.package, smirks=' ') SimpleAmbitRule.create(package=self.package, smirks=" ")
self.assertIn('SMIRKS is required', str(cm.exception)) 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): def test_invalid_smirks_validation(self, mock_is_valid):
"""Test validation of SMIRKS format.""" """Test validation of SMIRKS format."""
mock_is_valid.return_value = False mock_is_valid.return_value = False
invalid_smirks = 'invalid_smirks_string' invalid_smirks = "invalid_smirks_string"
with self.assertRaises(ValueError) as cm: with self.assertRaises(ValueError) as cm:
SimpleAmbitRule.create( SimpleAmbitRule.create(package=self.package, smirks=invalid_smirks)
package=self.package,
smirks=invalid_smirks
)
self.assertIn(f'SMIRKS "{invalid_smirks}" is invalid', str(cm.exception)) self.assertIn(f'SMIRKS "{invalid_smirks}" is invalid', str(cm.exception))
mock_is_valid.assert_called_once_with(invalid_smirks) mock_is_valid.assert_called_once_with(invalid_smirks)
def test_smirks_trimming(self): def test_smirks_trimming(self):
"""Test that SMIRKS strings are trimmed during creation.""" """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 = "[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_with_whitespace = f" {smirks} "
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(package=self.package, smirks=smirks_with_whitespace)
package=self.package,
smirks=smirks_with_whitespace
)
self.assertEqual(rule.smirks, smirks) self.assertEqual(rule.smirks, smirks)
def test_empty_name_and_description_handling(self): def test_empty_name_and_description_handling(self):
"""Test that empty name and description are handled appropriately.""" """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( rule = SimpleAmbitRule.create(
package=self.package, package=self.package, smirks=smirks, name="", description=" "
smirks=smirks,
name='',
description=' '
) )
self.assertRegex(rule.name, r'Rule \d+') self.assertRegex(rule.name, r"Rule \d+")
self.assertEqual(rule.description, 'no description') self.assertEqual(rule.description, "no description")
def test_deduplication_basic(self): def test_deduplication_basic(self):
"""Test that identical rules are deduplicated.""" """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( rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks, name="Rule 1")
package=self.package,
smirks=smirks,
name='Rule 1'
)
rule2 = SimpleAmbitRule.create( rule2 = SimpleAmbitRule.create(
package=self.package, package=self.package,
smirks=smirks, 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(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): def test_deduplication_with_filters(self):
"""Test deduplication with filter SMARTS.""" """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]' 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]' reactant_filter = "[CH2X4]"
product_filter = '[OH]' product_filter = "[OH]"
rule1 = SimpleAmbitRule.create( rule1 = SimpleAmbitRule.create(
package=self.package, package=self.package,
smirks=smirks, smirks=smirks,
reactant_filter_smarts=reactant_filter, reactant_filter_smarts=reactant_filter,
product_filter_smarts=product_filter product_filter_smarts=product_filter,
) )
rule2 = SimpleAmbitRule.create( rule2 = SimpleAmbitRule.create(
package=self.package, package=self.package,
smirks=smirks, smirks=smirks,
reactant_filter_smarts=reactant_filter, reactant_filter_smarts=reactant_filter,
product_filter_smarts=product_filter product_filter_smarts=product_filter,
) )
self.assertEqual(rule1.pk, rule2.pk) self.assertEqual(rule1.pk, rule2.pk)
def test_no_deduplication_different_filters(self): def test_no_deduplication_different_filters(self):
"""Test that rules with different filters are not deduplicated.""" """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( rule1 = SimpleAmbitRule.create(
package=self.package, package=self.package, smirks=smirks, reactant_filter_smarts="[CH2X4]"
smirks=smirks,
reactant_filter_smarts='[CH2X4]'
) )
rule2 = SimpleAmbitRule.create( rule2 = SimpleAmbitRule.create(
package=self.package, package=self.package, smirks=smirks, reactant_filter_smarts="[CH3X4]"
smirks=smirks,
reactant_filter_smarts='[CH3X4]'
) )
self.assertNotEqual(rule1.pk, rule2.pk) 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): def test_filter_smarts_trimming(self):
"""Test that filter SMARTS are trimmed and handled correctly.""" """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) # Test with whitespace-only filters (should be treated as None)
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(
package=self.package, package=self.package,
smirks=smirks, smirks=smirks,
reactant_filter_smarts=' ', reactant_filter_smarts=" ",
product_filter_smarts=' ' product_filter_smarts=" ",
) )
self.assertIsNone(rule.reactant_filter_smarts) self.assertIsNone(rule.reactant_filter_smarts)
@ -188,94 +173,85 @@ class SimpleAmbitRuleTest(TestCase):
def test_url_property(self): def test_url_property(self):
"""Test the URL property generation.""" """Test the URL property generation."""
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
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) self.assertEqual(rule.url, expected_url)
@patch('epdb.models.FormatConverter.apply') @patch("epdb.models.FormatConverter.apply")
def test_apply_method(self, mock_apply): def test_apply_method(self, mock_apply):
"""Test the apply method delegates to FormatConverter.""" """Test the apply method delegates to FormatConverter."""
mock_apply.return_value = ['product1', 'product2'] mock_apply.return_value = ["product1", "product2"]
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
test_smiles = 'CCO' test_smiles = "CCO"
result = rule.apply(test_smiles) result = rule.apply(test_smiles)
mock_apply.assert_called_once_with(test_smiles, rule.smirks) 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): def test_reactants_smarts_property(self):
"""Test reactants_smarts property extracts correct part of SMIRKS.""" """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]' 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]' expected_reactants = "[H:5][C:1]([#6:6])([#1,#9,#17,#35,#53:4])[#9,#17,#35,#53]"
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
package=self.package,
smirks=smirks
)
self.assertEqual(rule.reactants_smarts, expected_reactants) self.assertEqual(rule.reactants_smarts, expected_reactants)
def test_products_smarts_property(self): def test_products_smarts_property(self):
"""Test products_smarts property extracts correct part of SMIRKS.""" """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]' 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]' expected_products = "[H:5][C:1]([#6:6])([#8])[#1,#9,#17,#35,#53:4]"
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
package=self.package,
smirks=smirks
)
self.assertEqual(rule.products_smarts, expected_products) 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): def test_related_reactions_property(self, mock_package_objects):
"""Test related_reactions property returns correct queryset.""" """Test related_reactions property returns correct queryset."""
mock_qs = MagicMock() mock_qs = MagicMock()
mock_package_objects.filter.return_value = mock_qs mock_package_objects.filter.return_value = mock_qs
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
# Instead of directly assigning, patch the property or use with patch.object # 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: with patch.object(
mock_reaction_rule.return_value.filter.return_value.order_by.return_value = ['reaction1', 'reaction2'] 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 result = rule.related_reactions
mock_package_objects.filter.assert_called_once_with(reviewed=True) 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.assert_called_once_with(package__in=mock_qs)
mock_reaction_rule.return_value.filter.return_value.order_by.assert_called_once_with('name') mock_reaction_rule.return_value.filter.return_value.order_by.assert_called_once_with(
self.assertEqual(result, ['reaction1', 'reaction2']) "name"
)
self.assertEqual(result, ["reaction1", "reaction2"])
@patch('epdb.models.Pathway.objects') @patch("epdb.models.Pathway.objects")
@patch('epdb.models.Edge.objects') @patch("epdb.models.Edge.objects")
def test_related_pathways_property(self, mock_edge_objects, mock_pathway_objects): def test_related_pathways_property(self, mock_edge_objects, mock_pathway_objects):
"""Test related_pathways property returns correct queryset.""" """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 mock_prop.return_value = mock_related_reactions
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
# Mock Edge objects query # Mock Edge objects query
mock_edge_values = MagicMock() 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_edge_objects.filter.return_value = mock_edge_values
# Mock Pathway objects query # Mock Pathway objects query
@ -285,52 +261,49 @@ class SimpleAmbitRuleTest(TestCase):
result = rule.related_pathways result = rule.related_pathways
mock_edge_objects.filter.assert_called_once_with(edge_label__in=mock_related_reactions) 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() mock_pathway_objects.filter.assert_called_once()
self.assertEqual(result, mock_pathway_qs) 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): def test_as_svg_property(self, mock_smirks_to_svg):
"""Test as_svg property calls IndigoUtils correctly.""" """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( rule = SimpleAmbitRule.create(package=self.package, smirks="[H:1][C:2]>>[H:1][O:2]")
package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]'
)
result = rule.as_svg result = rule.as_svg
mock_smirks_to_svg.assert_called_once_with(rule.smirks, True, width=800, height=400) 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): def test_atomic_transaction(self):
"""Test that rule creation is atomic.""" """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 # This should work normally
rule = SimpleAmbitRule.create(package=self.package, smirks=smirks) rule = SimpleAmbitRule.create(package=self.package, smirks=smirks)
self.assertIsInstance(rule, SimpleAmbitRule) self.assertIsInstance(rule, SimpleAmbitRule)
# Test transaction rollback on error # 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): 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 # Verify no partial data was saved
self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package).count(), 1) self.assertEqual(SimpleAmbitRule.objects.filter(package=self.package).count(), 1)
def test_multiple_duplicate_warning(self): def test_multiple_duplicate_warning(self):
"""Test logging when multiple duplicates are found.""" """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 # Create first rule
rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks) rule1 = SimpleAmbitRule.create(package=self.package, smirks=smirks)
# Manually create a duplicate to simulate the error condition # 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() 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 # This should find the existing rule and log an error about multiple matches
result = SimpleAmbitRule.create(package=self.package, smirks=smirks) result = SimpleAmbitRule.create(package=self.package, smirks=smirks)
@ -339,24 +312,28 @@ class SimpleAmbitRuleTest(TestCase):
# Should log an error about multiple matches # Should log an error about multiple matches
mock_logger.error.assert_called() 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): def test_model_fields(self):
"""Test model field properties.""" """Test model field properties."""
rule = SimpleAmbitRule.create( rule = SimpleAmbitRule.create(
package=self.package, package=self.package,
smirks='[H:1][C:2]>>[H:1][O:2]', smirks="[H:1][C:2]>>[H:1][O:2]",
reactant_filter_smarts='[CH3]', reactant_filter_smarts="[CH3]",
product_filter_smarts='[OH]' product_filter_smarts="[OH]",
) )
# Test field properties # Test field properties
self.assertFalse(rule._meta.get_field('smirks').blank) self.assertFalse(rule._meta.get_field("smirks").blank)
self.assertFalse(rule._meta.get_field('smirks').null) self.assertFalse(rule._meta.get_field("smirks").null)
self.assertTrue(rule._meta.get_field('reactant_filter_smarts').null) self.assertTrue(rule._meta.get_field("reactant_filter_smarts").null)
self.assertTrue(rule._meta.get_field('product_filter_smarts').null) self.assertTrue(rule._meta.get_field("product_filter_smarts").null)
# Test verbose names # Test verbose names
self.assertEqual(rule._meta.get_field('smirks').verbose_name, 'SMIRKS') 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(
self.assertEqual(rule._meta.get_field('product_filter_smarts').verbose_name, 'Product Filter SMARTS') 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 django.test import TestCase
from epdb.logic import SNode, SEdge, SPathway from epdb.logic import SNode, SEdge
class SObjectTest(TestCase): class SObjectTest(TestCase):
def setUp(self): def setUp(self):
pass pass
def test_snode_eq(self): def test_snode_eq(self):
snode1 = 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) snode2 = SNode("CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O", 0)
assert snode1 == snode2 assert snode1 == snode2
def test_snode_hash(self): def test_snode_hash(self):
pass pass
def test_sedge_eq(self): def test_sedge_eq(self):
sedge1 = SEdge([SNode('CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O', 0)], sedge1 = SEdge(
[SNode('CN1C(=O)NC2=C(C1=O)N(C)C=N2', 1), SNode('C=O', 1)], [SNode("CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O", 0)],
rule=None) [SNode("CN1C(=O)NC2=C(C1=O)N(C)C=N2", 1), SNode("C=O", 1)],
sedge2 = SEdge([SNode('CN1C2C(N(C(N(C)C=2N=C1)=O)C)=O', 0)], rule=None,
[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 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 @classmethod
def setUpClass(cls): def setUpClass(cls):
super(PackageViewTest, cls).setUpClass() super(PackageViewTest, cls).setUpClass()
cls.user1 = UserManager.create_user("user1", "user1@envipath.com", "SuperSafe", cls.user1 = UserManager.create_user(
set_setting=False, add_to_group=True, is_active=True) "user1",
cls.user2 = UserManager.create_user("user2", "user2@envipath.com", "SuperSafe", "user1@envipath.com",
set_setting=False, add_to_group=True, is_active=True) "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): def setUp(self):
self.client.force_login(self.user1) self.client.force_login(self.user1)
def test_create_package(self): def test_create_package(self):
response = self.client.post(reverse("packages"), { response = self.client.post(
"package-name": "Test Package", reverse("packages"),
"package-description": "Just a Description", {
}) "package-name": "Test Package",
"package-description": "Just a Description",
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
package_url = response.url package_url = response.url
@ -41,13 +56,12 @@ class PackageViewTest(TestCase):
file = SimpleUploadedFile( file = SimpleUploadedFile(
"Fixture_Package.json", "Fixture_Package.json",
open(s.FIXTURE_DIRS[0] / "Fixture_Package.json", "rb").read(), open(s.FIXTURE_DIRS[0] / "Fixture_Package.json", "rb").read(),
content_type="application/json" content_type="application/json",
) )
response = self.client.post(reverse("packages"), { response = self.client.post(
"file": file, reverse("packages"), {"file": file, "hidden": "import-package-json"}
"hidden": "import-package-json" )
})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
package_url = response.url package_url = response.url
@ -67,13 +81,12 @@ class PackageViewTest(TestCase):
file = SimpleUploadedFile( file = SimpleUploadedFile(
"EAWAG-BBD.json", "EAWAG-BBD.json",
open(s.FIXTURE_DIRS[0] / "packages" / "2025-07-18" / "EAWAG-BBD.json", "rb").read(), 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"), { response = self.client.post(
"file": file, reverse("packages"), {"file": file, "hidden": "import-legacy-package-json"}
"hidden": "import-legacy-package-json" )
})
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
package_url = response.url package_url = response.url
@ -90,17 +103,23 @@ class PackageViewTest(TestCase):
self.assertEqual(upp.permission, Permission.ALL[0]) self.assertEqual(upp.permission, Permission.ALL[0])
def test_edit_package(self): def test_edit_package(self):
response = self.client.post(reverse("packages"), { response = self.client.post(
"package-name": "Test Package", reverse("packages"),
"package-description": "Just a Description", {
}) "package-name": "Test Package",
"package-description": "Just a Description",
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
package_url = response.url package_url = response.url
self.client.post(package_url, { self.client.post(
"package-name": "New Name", package_url,
"package-description": "New Description", {
}) "package-name": "New Name",
"package-description": "New Description",
},
)
p = Package.objects.get(url=package_url) p = Package.objects.get(url=package_url)
@ -108,10 +127,13 @@ class PackageViewTest(TestCase):
self.assertEqual(p.description, "New Description") self.assertEqual(p.description, "New Description")
def test_edit_package_permissions(self): def test_edit_package_permissions(self):
response = self.client.post(reverse("packages"), { response = self.client.post(
"package-name": "Test Package", reverse("packages"),
"package-description": "Just a Description", {
}) "package-name": "Test Package",
"package-description": "Just a Description",
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
package_url = response.url package_url = response.url
p = Package.objects.get(url=package_url) p = Package.objects.get(url=package_url)
@ -119,57 +141,63 @@ class PackageViewTest(TestCase):
with self.assertRaises(UserPackagePermission.DoesNotExist): with self.assertRaises(UserPackagePermission.DoesNotExist):
UserPackagePermission.objects.get(package=p, user=self.user2) UserPackagePermission.objects.get(package=p, user=self.user2)
self.client.post(package_url, { self.client.post(
"grantee": self.user2.url, package_url,
"read": "on", {
"write": "on", "grantee": self.user2.url,
}) "read": "on",
"write": "on",
},
)
upp = UserPackagePermission.objects.get(package=p, user=self.user2) upp = UserPackagePermission.objects.get(package=p, user=self.user2)
self.assertEqual(upp.permission, Permission.WRITE[0]) self.assertEqual(upp.permission, Permission.WRITE[0])
def test_publish_package(self): def test_publish_package(self):
response = self.client.post(reverse("packages"), { response = self.client.post(
"package-name": "Test Package", reverse("packages"),
"package-description": "Just a Description", {
}) "package-name": "Test Package",
"package-description": "Just a Description",
},
)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)
package_url = response.url package_url = response.url
p = Package.objects.get(url=package_url) p = Package.objects.get(url=package_url)
self.client.post(package_url, { self.client.post(package_url, {"hidden": "publish-package"})
"hidden": "publish-package"
})
self.assertEqual(Group.objects.filter(public=True).count(), 1) self.assertEqual(Group.objects.filter(public=True).count(), 1)
g = Group.objects.get(public=True) g = Group.objects.get(public=True)
gpp = GroupPackagePermission.objects.get(package=p, group=g) gpp = GroupPackagePermission.objects.get(package=p, group=g)
self.assertEqual(gpp.permission, Permission.READ[0]) self.assertEqual(gpp.permission, Permission.READ[0])
def test_set_package_license(self): def test_set_package_license(self):
response = self.client.post(reverse("packages"), { response = self.client.post(
"package-name": "Test Package", reverse("packages"),
"package-description": "Just a Description", {
}) "package-name": "Test Package",
"package-description": "Just a Description",
},
)
package_url = response.url package_url = response.url
p = Package.objects.get(url=package_url) p = Package.objects.get(url=package_url)
self.client.post(package_url, { self.client.post(package_url, {"license": "no-license"})
"license": "no-license"
})
self.assertIsNone(p.license) self.assertIsNone(p.license)
# TODO test others # TODO test others
def test_delete_package(self): def test_delete_package(self):
response = self.client.post(reverse("packages"), { response = self.client.post(
"package-name": "Test Package", reverse("packages"),
"package-description": "Just a Description", {
}) "package-name": "Test Package",
"package-description": "Just a Description",
},
)
package_url = response.url package_url = response.url
p = Package.objects.get(url=package_url) p = Package.objects.get(url=package_url)
@ -178,3 +206,15 @@ class PackageViewTest(TestCase):
response = self.client.get(package_url) response = self.client.get(package_url)
self.assertEqual(response.status_code, 404) 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 @classmethod
def setUpClass(cls): def setUpClass(cls):
super(UserViewTest, cls).setUpClass() super(UserViewTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous') cls.user = User.objects.get(username="anonymous")
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc') cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
cls.BBD_SUBSET = Package.objects.get(name='Fixtures') cls.BBD_SUBSET = Package.objects.get(name="Fixtures")
def test_login_with_valid_credentials(self): def test_login_with_valid_credentials(self):
response = self.client.post(reverse("login"), { response = self.client.post(
"username": "user0", reverse("login"),
"password": 'SuperSafe', {
}) "username": "user0",
"password": "SuperSafe",
},
)
self.assertRedirects(response, reverse("index")) self.assertRedirects(response, reverse("index"))
self.assertTrue(response.wsgi_request.user.is_authenticated) self.assertTrue(response.wsgi_request.user.is_authenticated)
def test_login_with_invalid_credentials(self): def test_login_with_invalid_credentials(self):
response = self.client.post(reverse("login"), { response = self.client.post(
"username": "user0", reverse("login"),
"password": "wrongpassword", {
}) "username": "user0",
"password": "wrongpassword",
},
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertFalse(response.wsgi_request.user.is_authenticated) self.assertFalse(response.wsgi_request.user.is_authenticated)
def test_register(self): def test_register(self):
response = self.client.post(reverse("register"), { response = self.client.post(
"username": "user1", reverse("register"),
"email": "user1@envipath.com", {
"password": "SuperSafe", "username": "user1",
"rpassword": "SuperSafe", "email": "user1@envipath.com",
}) "password": "SuperSafe",
"rpassword": "SuperSafe",
},
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
# TODO currently fails as the fixture does not provide a global setting... # TODO currently fails as the fixture does not provide a global setting...
self.assertContains(response, "Registration failed!") self.assertContains(response, "Registration failed!")
def test_register_password_mismatch(self): def test_register_password_mismatch(self):
response = self.client.post(reverse("register"), { response = self.client.post(
"username": "user1", reverse("register"),
"email": "user1@envipath.com", {
"password": "SuperSafe", "username": "user1",
"rpassword": "SuperSaf3", "email": "user1@envipath.com",
}) "password": "SuperSafe",
"rpassword": "SuperSaf3",
},
)
self.assertEqual(response.status_code, 200) self.assertEqual(response.status_code, 200)
self.assertContains(response, "Registration failed, provided passwords differ") self.assertContains(response, "Registration failed, provided passwords differ")
def test_logout(self): def test_logout(self):
response = self.client.post(reverse("login"), { response = self.client.post(
"username": "user0", reverse("login"), {"username": "user0", "password": "SuperSafe", "login": "true"}
"password": 'SuperSafe', )
"login": "true"
})
self.assertTrue(response.wsgi_request.user.is_authenticated) self.assertTrue(response.wsgi_request.user.is_authenticated)
response = self.client.post(reverse('logout'), { response = self.client.post(
"logout": "true", reverse("logout"),
}) {
"logout": "true",
},
)
self.assertFalse(response.wsgi_request.user.is_authenticated) self.assertFalse(response.wsgi_request.user.is_authenticated)
def test_next_param_properly_handled(self): 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") self.assertRedirects(response, f"{reverse('login')}/?next=/package")
response = self.client.post(reverse('login'), { response = self.client.post(
"username": "user0", reverse("login"),
"password": 'SuperSafe', {"username": "user0", "password": "SuperSafe", "login": "true", "next": "/package"},
"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 import re
from abc import ABC from abc import ABC
from collections import defaultdict 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 import Indigo, IndigoException, IndigoObject
from indigo.renderer import IndigoRenderer from indigo.renderer import IndigoRenderer
from rdkit import Chem from rdkit import Chem, rdBase
from rdkit import RDLogger
from rdkit.Chem import MACCSkeys, Descriptors from rdkit.Chem import MACCSkeys, Descriptors
from rdkit.Chem import rdChemReactions from rdkit.Chem import rdChemReactions
from rdkit.Chem.Draw import rdMolDraw2D from rdkit.Chem.Draw import rdMolDraw2D
@ -15,9 +14,11 @@ from rdkit.Chem.MolStandardize import rdMolStandardize
from rdkit.Chem.rdmolops import GetMolFrags from rdkit.Chem.rdmolops import GetMolFrags
from rdkit.Contrib.IFG import ifg from rdkit.Contrib.IFG import ifg
logger = logging.getLogger(__name__) if TYPE_CHECKING:
RDLogger.DisableLog('rdApp.*') from epdb.models import Rule
logger = logging.getLogger(__name__)
rdBase.DisableLog("rdApp.*")
# from rdkit import rdBase # from rdkit import rdBase
# rdBase.LogToPythonLogger() # rdBase.LogToPythonLogger()
@ -28,7 +29,6 @@ RDLogger.DisableLog('rdApp.*')
class ProductSet(object): class ProductSet(object):
def __init__(self, product_set: List[str]): def __init__(self, product_set: List[str]):
self.product_set = product_set self.product_set = product_set
@ -42,15 +42,18 @@ class ProductSet(object):
return iter(self.product_set) return iter(self.product_set)
def __eq__(self, other): 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): def __hash__(self):
return hash('-'.join(sorted(self.product_set))) return hash("-".join(sorted(self.product_set)))
class PredictionResult(object): class PredictionResult(object):
def __init__(
def __init__(self, product_sets: List['ProductSet'], probability: float, rule: Optional['Rule'] = None): self, product_sets: List["ProductSet"], probability: float, rule: Optional["Rule"] = None
):
self.product_sets = product_sets self.product_sets = product_sets
self.probability = probability self.probability = probability
self.rule = rule self.rule = rule
@ -66,7 +69,6 @@ class PredictionResult(object):
class FormatConverter(object): class FormatConverter(object):
@staticmethod @staticmethod
def mass(smiles): def mass(smiles):
return Descriptors.MolWt(FormatConverter.from_smiles(smiles)) return Descriptors.MolWt(FormatConverter.from_smiles(smiles))
@ -127,7 +129,7 @@ class FormatConverter(object):
if kekulize: if kekulize:
try: try:
mol = Chem.Kekulize(mol) mol = Chem.Kekulize(mol)
except: except Exception:
mol = Chem.Mol(mol.ToBinary()) mol = Chem.Mol(mol.ToBinary())
if not mol.GetNumConformers(): if not mol.GetNumConformers():
@ -139,8 +141,8 @@ class FormatConverter(object):
opts.clearBackground = False opts.clearBackground = False
drawer.DrawMolecule(mol) drawer.DrawMolecule(mol)
drawer.FinishDrawing() drawer.FinishDrawing()
svg = drawer.GetDrawingText().replace('svg:', '') svg = drawer.GetDrawingText().replace("svg:", "")
svg = re.sub("<\?xml.*\?>", '', svg) svg = re.sub("<\?xml.*\?>", "", svg)
return svg return svg
@ -151,7 +153,7 @@ class FormatConverter(object):
if kekulize: if kekulize:
try: try:
Chem.Kekulize(mol) Chem.Kekulize(mol)
except: except Exception:
mc = Chem.Mol(mol.ToBinary()) mc = Chem.Mol(mol.ToBinary())
if not mc.GetNumConformers(): if not mc.GetNumConformers():
@ -178,7 +180,7 @@ class FormatConverter(object):
smiles = tmp_smiles smiles = tmp_smiles
if change is False: if change is False:
print(f"nothing changed") print("nothing changed")
return smiles return smiles
@ -198,7 +200,9 @@ class FormatConverter(object):
parent_clean_mol = rdMolStandardize.FragmentParent(clean_mol) parent_clean_mol = rdMolStandardize.FragmentParent(clean_mol)
# try to neutralize molecule # 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) uncharged_parent_clean_mol = uncharger.uncharge(parent_clean_mol)
# note that no attempt is made at reionization at this step # note that no attempt is made at reionization at this step
@ -239,17 +243,24 @@ class FormatConverter(object):
try: try:
rdChemReactions.ReactionFromSmarts(smirks) rdChemReactions.ReactionFromSmarts(smirks)
return True return True
except: except Exception:
return False return False
@staticmethod @staticmethod
def apply(smiles: str, smirks: str, preprocess_smiles: bool = True, bracketize: bool = True, def apply(
standardize: bool = True, kekulize: bool = True) -> List['ProductSet']: smiles: str,
logger.debug(f'Applying {smirks} on {smiles}') 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 explicitly wanted or rule generates multiple products add brackets around products to capture all
if bracketize: # or "." in smirks: if bracketize: # or "." in smirks:
smirks = smirks.split('>>')[0] + ">>(" + smirks.split('>>')[1] + ")" smirks = smirks.split(">>")[0] + ">>(" + smirks.split(">>")[1] + ")"
# List of ProductSet objects # List of ProductSet objects
pss = set() pss = set()
@ -274,7 +285,9 @@ class FormatConverter(object):
Chem.SanitizeMol(product) Chem.SanitizeMol(product)
product = GetMolFrags(product, asMols=True) product = GetMolFrags(product, asMols=True)
for p in product: for p in product:
p = FormatConverter.standardize(Chem.MolToSmiles(p)) p = FormatConverter.standardize(
Chem.MolToSmiles(p), remove_stereo=remove_stereo
)
prods.append(p) prods.append(p)
# if kekulize: # if kekulize:
@ -300,9 +313,8 @@ class FormatConverter(object):
# # bond.SetIsAromatic(False) # # bond.SetIsAromatic(False)
# Chem.Kekulize(product) # Chem.Kekulize(product)
except ValueError as e: except ValueError as e:
logger.error(f'Sanitizing and converting failed:\n{e}') logger.error(f"Sanitizing and converting failed:\n{e}")
continue continue
if len(prods): if len(prods):
@ -310,34 +322,14 @@ class FormatConverter(object):
pss.add(ps) pss.add(ps)
except Exception as e: 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 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 @staticmethod
def MACCS(smiles): def MACCS(smiles):
return MACCSkeys.GenMACCSKeys(FormatConverter.from_smiles(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 @staticmethod
def sanitize_smiles(smiles_list: List): def sanitize_smiles(smiles_list: List):
parsed_smiles = [] parsed_smiles = []
@ -353,28 +345,26 @@ class FormatConverter(object):
# smi = smi.replace("@", "") # smi = smi.replace("@", "")
mol = Chem.MolFromSmiles(smi) mol = Chem.MolFromSmiles(smi)
mol = FormatConverter.neutralize_atoms(mol) mol = FormatConverter.neutralize_molecule(mol)
Chem.RemoveStereochemistry(mol)
mol = Chem.RemoveAllHs(mol) mol = Chem.RemoveAllHs(mol)
Chem.Kekulize(mol) Chem.Kekulize(mol)
smi_p = Chem.MolToSmiles(mol, kekuleSmiles=True) smi_p = Chem.MolToSmiles(mol, kekuleSmiles=True)
smi_p = Chem.CanonSmiles(smi_p) smi_p = Chem.CanonSmiles(smi_p)
if '~' in smi_p: if "~" in smi_p:
smi_p1 = smi_p.replace('~', '') smi_p1 = smi_p.replace("~", "")
parsed_smiles.append(smi_p1) parsed_smiles.append(smi_p1)
else: else:
parsed_smiles.append(smi_p) parsed_smiles.append(smi_p)
except Exception as e: except Exception:
errors += 1 errors += 1
pass pass
return parsed_smiles, errors return parsed_smiles, errors
class Standardizer(ABC): class Standardizer(ABC):
def __init__(self, name): def __init__(self, name):
self.name = name self.name = name
@ -383,7 +373,6 @@ class Standardizer(ABC):
class RuleStandardizer(Standardizer): class RuleStandardizer(Standardizer):
def __init__(self, name, smirks): def __init__(self, name, smirks):
super().__init__(name) super().__init__(name)
self.smirks = smirks self.smirks = smirks
@ -392,8 +381,8 @@ class RuleStandardizer(Standardizer):
standardized_smiles = list(set(FormatConverter.apply(smiles, self.smirks))) standardized_smiles = list(set(FormatConverter.apply(smiles, self.smirks)))
if len(standardized_smiles) > 1: if len(standardized_smiles) > 1:
logger.warning(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}') print(f"{self.smirks} generated more than 1 compound {standardized_smiles}")
standardized_smiles = standardized_smiles[:1] standardized_smiles = standardized_smiles[:1]
if standardized_smiles: if standardized_smiles:
@ -403,7 +392,6 @@ class RuleStandardizer(Standardizer):
class RegExStandardizer(Standardizer): class RegExStandardizer(Standardizer):
def __init__(self, name, replacements: dict): def __init__(self, name, replacements: dict):
super().__init__(name) super().__init__(name)
self.replacements = replacements self.replacements = replacements
@ -423,28 +411,39 @@ class RegExStandardizer(Standardizer):
return super().standardize(smi) return super().standardize(smi)
FLATTEN = [ FLATTEN = [RegExStandardizer("Remove Stereo", {"@": ""})]
RegExStandardizer("Remove Stereo", {"@": ""})
]
UN_CIS_TRANS = [ UN_CIS_TRANS = [RegExStandardizer("Un-Cis-Trans", {"/": "", "\\": ""})]
RegExStandardizer("Un-Cis-Trans", {"/": "", "\\": ""})
]
BASIC = [ BASIC = [
RuleStandardizer("ammoniumstandardization", "[H][N+:1]([H])([H])[#6:2]>>[H][#7:1]([H])-[#6:2]"), 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("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("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("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(
RuleStandardizer("phosphatedeprotonation", "[H][#8:1]-[$([#15]);!$(P([O-])):2]>>[#8-:1]-[#15:2]"), "Hydroxylprotonation",
RuleStandardizer("PicricAcid", "[#6;A:1][#6:2](-[#8-:3])=[#6;A:4]>>[#6:1]-[#6:2](-[#8:3][H])=[#6;A:4]",
"[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(
RuleStandardizer("Sulfate2", "phosphatedeprotonation", "[H][#8:1]-[$([#15]);!$(P([O-])):2]>>[#8-:1]-[#15:2]"
"[#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(
RuleStandardizer("Transform_c1353forSOOH", "[H][#8:1][S:2]([*:3])=[O:4]>>[#8-:1][S:2]([*:3])=[O:4]"), "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 + [ ENHANCED = BASIC + [
@ -452,28 +451,30 @@ ENHANCED = BASIC + [
] ]
EXOTIC = ENHANCED + [ 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 = [ COA_CUTTER = [
RuleStandardizer("CutCoEnzymeAOff", RuleStandardizer(
"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]") "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 = [ ENOL_KETO = [RuleStandardizer("enol2Ketone", "[H][#8:2]-[#6:3]=[#6:1]>>[#6:1]-[#6:3]=[O:2]")]
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 MATCH_STANDARDIZER = EXOTIC + FLATTEN + UN_CIS_TRANS + COA_CUTTER + ENOL_KETO
class IndigoUtils(object): class IndigoUtils(object):
@staticmethod @staticmethod
def layout(mol_data): def layout(mol_data):
i = Indigo() i = Indigo()
try: 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 = i.loadQueryReaction(mol_data)
rxn.layout() rxn.layout()
return rxn.rxnfile() return rxn.rxnfile()
@ -481,14 +482,14 @@ class IndigoUtils(object):
mol = i.loadQueryMolecule(mol_data) mol = i.loadQueryMolecule(mol_data)
mol.layout() mol.layout()
return mol.molfile() return mol.molfile()
except IndigoException as e: except IndigoException:
try: try:
logger.info("layout() failed, trying loadReactionSMARTS as fallback!") logger.info("layout() failed, trying loadReactionSMARTS as fallback!")
rxn = IndigoUtils.load_reaction_SMARTS(mol_data) rxn = IndigoUtils.load_reaction_SMARTS(mol_data)
rxn.layout() rxn.layout()
return rxn.molfile() return rxn.molfile()
except IndigoException as e2: except IndigoException as e2:
logger.error(f'layout() failed due to {e2}!') logger.error(f"layout() failed due to {e2}!")
@staticmethod @staticmethod
def load_reaction_SMARTS(mol): def load_reaction_SMARTS(mol):
@ -498,7 +499,7 @@ class IndigoUtils(object):
def aromatize(mol_data, is_query): def aromatize(mol_data, is_query):
i = Indigo() i = Indigo()
try: try:
if mol_data.startswith('$RXN'): if mol_data.startswith("$RXN"):
if is_query: if is_query:
rxn = i.loadQueryReaction(mol_data) rxn = i.loadQueryReaction(mol_data)
else: else:
@ -514,20 +515,20 @@ class IndigoUtils(object):
mol.aromatize() mol.aromatize()
return mol.molfile() return mol.molfile()
except IndigoException as e: except IndigoException:
try: try:
logger.info("Aromatizing failed, trying loadReactionSMARTS as fallback!") logger.info("Aromatizing failed, trying loadReactionSMARTS as fallback!")
rxn = IndigoUtils.load_reaction_SMARTS(mol_data) rxn = IndigoUtils.load_reaction_SMARTS(mol_data)
rxn.aromatize() rxn.aromatize()
return rxn.molfile() return rxn.molfile()
except IndigoException as e2: except IndigoException as e2:
logger.error(f'Aromatizing failed due to {e2}!') logger.error(f"Aromatizing failed due to {e2}!")
@staticmethod @staticmethod
def dearomatize(mol_data, is_query): def dearomatize(mol_data, is_query):
i = Indigo() i = Indigo()
try: try:
if mol_data.startswith('$RXN'): if mol_data.startswith("$RXN"):
if is_query: if is_query:
rxn = i.loadQueryReaction(mol_data) rxn = i.loadQueryReaction(mol_data)
else: else:
@ -543,14 +544,14 @@ class IndigoUtils(object):
mol.dearomatize() mol.dearomatize()
return mol.molfile() return mol.molfile()
except IndigoException as e: except IndigoException:
try: try:
logger.info("De-Aromatizing failed, trying loadReactionSMARTS as fallback!") logger.info("De-Aromatizing failed, trying loadReactionSMARTS as fallback!")
rxn = IndigoUtils.load_reaction_SMARTS(mol_data) rxn = IndigoUtils.load_reaction_SMARTS(mol_data)
rxn.dearomatize() rxn.dearomatize()
return rxn.molfile() return rxn.molfile()
except IndigoException as e2: except IndigoException as e2:
logger.error(f'De-Aromatizing failed due to {e2}!') logger.error(f"De-Aromatizing failed due to {e2}!")
@staticmethod @staticmethod
def sanitize_functional_group(functional_group: str): def sanitize_functional_group(functional_group: str):
@ -562,7 +563,7 @@ class IndigoUtils(object):
# special environment handling (amines, hydroxy, esters, ethers) # special environment handling (amines, hydroxy, esters, ethers)
# the higher substituted should not contain H env. # 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]" functional_group = "[H][C](=O)[CX4,c]"
# aldamines # 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 functional_group = "[nH1,nX2](a)a" # pyrrole (with H) or pyridine (no other connections); currently overlaps with neighboring aromatic atoms
# substituted aromatic nitrogen # substituted aromatic nitrogen
functional_group = functional_group.replace("N*(R)R", functional_group = functional_group.replace(
"n(a)a") # substituent will be before N*; currently overlaps with neighboring aromatic atoms "N*(R)R", "n(a)a"
) # substituent will be before N*; currently overlaps with neighboring aromatic atoms
# pyridinium # pyridinium
if functional_group == "RN*(R)(R)(R)R": 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 # N-oxide
if functional_group == "[H]ON*(R)(R)(R)R": 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 # other aromatic hetero atoms
functional_group = functional_group.replace("C*", "c") functional_group = functional_group.replace("C*", "c")
@ -617,7 +623,9 @@ class IndigoUtils(object):
# other replacement, to accomodate for the standardization rules in enviPath # 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? # 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... # 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]") 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 # 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)") functional_group = functional_group.replace("[H]OC(=O)", "[OD1]C(=O)")
@ -635,7 +643,9 @@ class IndigoUtils(object):
return functional_group return functional_group
@staticmethod @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("render-atom-color-property", "color")
indigo.setOption("aromaticity-model", "generic") indigo.setOption("aromaticity-model", "generic")
@ -665,7 +675,6 @@ class IndigoUtils(object):
for match in matcher.iterateMatches(query): for match in matcher.iterateMatches(query):
if match is not None: if match is not None:
for atom in query.iterateAtoms(): for atom in query.iterateAtoms():
mappedAtom = match.mapAtom(atom) mappedAtom = match.mapAtom(atom)
if mappedAtom is None or mappedAtom.index() in environment: if mappedAtom is None or mappedAtom.index() in environment:
@ -674,7 +683,7 @@ class IndigoUtils(object):
counts[mappedAtom.index()] = max(v, counts[mappedAtom.index()]) counts[mappedAtom.index()] = max(v, counts[mappedAtom.index()])
except IndigoException as e: 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(): for k, v in counts.items():
if is_reaction: if is_reaction:
@ -688,8 +697,9 @@ class IndigoUtils(object):
molecule.addDataSGroup([k], [], "color", color) molecule.addDataSGroup([k], [], "color", color)
@staticmethod @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: if functional_groups is None:
functional_groups = {} functional_groups = {}
@ -701,7 +711,7 @@ class IndigoUtils(object):
i.setOption("render-image-size", width, height) i.setOption("render-image-size", width, height)
i.setOption("render-bond-line-width", 2.0) i.setOption("render-bond-line-width", 2.0)
if '~' in mol_data: if "~" in mol_data:
mol = i.loadSmarts(mol_data) mol = i.loadSmarts(mol_data)
else: else:
mol = i.loadMolecule(mol_data) mol = i.loadMolecule(mol_data)
@ -709,11 +719,18 @@ class IndigoUtils(object):
if len(functional_groups.keys()) > 0: if len(functional_groups.keys()) > 0:
IndigoUtils._colorize(i, mol, functional_groups, False) IndigoUtils._colorize(i, mol, functional_groups, False)
return renderer.renderToBuffer(mol).decode('UTF-8') return renderer.renderToBuffer(mol).decode("UTF-8")
@staticmethod @staticmethod
def smirks_to_svg(smirks: str, is_query_smirks, width: int = 0, height: int = 0, def smirks_to_svg(
educt_functional_groups: Dict[str, int] = None, product_functional_groups: Dict[str, int] = None): 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: if educt_functional_groups is None:
educt_functional_groups = {} educt_functional_groups = {}
@ -723,6 +740,11 @@ class IndigoUtils(object):
i = Indigo() i = Indigo()
renderer = IndigoRenderer(i) 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-output-format", "svg")
i.setOption("render-coloring", True) i.setOption("render-coloring", True)
i.setOption("render-image-size", width, height) i.setOption("render-image-size", width, height)
@ -740,18 +762,18 @@ class IndigoUtils(object):
for prod in obj.iterateProducts(): for prod in obj.iterateProducts():
IndigoUtils._colorize(i, prod, product_functional_groups, True) 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 = { 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", "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": { "options": {
"smart-layout": True, "smart-layout": True,
"ignore-stereochemistry-errors": True, "ignore-stereochemistry-errors": True,
"mass-skip-error-on-pseudoatoms": False, "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