forked from enviPath/enviPy
Compare commits
2 Commits
develop
...
fix/missin
| Author | SHA1 | Date | |
|---|---|---|---|
| db286d14ec | |||
| 999dc15189 |
@ -20,16 +20,3 @@ LOG_LEVEL='INFO'
|
|||||||
SERVER_URL='http://localhost:8000'
|
SERVER_URL='http://localhost:8000'
|
||||||
PLUGINS_ENABLED=True
|
PLUGINS_ENABLED=True
|
||||||
EP_DATA_DIR='data'
|
EP_DATA_DIR='data'
|
||||||
EMAIL_HOST_USER='admin@envipath.com'
|
|
||||||
EMAIL_HOST_PASSWORD='dummy-password'
|
|
||||||
|
|
||||||
DEFAULT_FROM_EMAIL="test@test.com"
|
|
||||||
SERVER_EMAIL='test@test.com'
|
|
||||||
|
|
||||||
# Testing settings VScode
|
|
||||||
DJANGO_SETTINGS_MODULE='envipath.settings'
|
|
||||||
MANAGE_PY_PATH='./manage.py'
|
|
||||||
|
|
||||||
APPLICABILITY_DOMAIN_ENABLED=True
|
|
||||||
ENVIFORMER_PRESENT=True
|
|
||||||
MODEL_BUILDING_ENABLED=True
|
|
||||||
|
|||||||
@ -1,72 +0,0 @@
|
|||||||
name: 'Setup enviPy Environment'
|
|
||||||
description: 'Shared setup for enviPy CI - installs dependencies and prepares environment'
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
skip-frontend:
|
|
||||||
description: 'Skip frontend build steps (pnpm, tailwind)'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
skip-playwright:
|
|
||||||
description: 'Skip Playwright installation'
|
|
||||||
required: false
|
|
||||||
default: 'false'
|
|
||||||
ssh-private-key:
|
|
||||||
description: 'SSH private key for git access'
|
|
||||||
required: true
|
|
||||||
run-migrations:
|
|
||||||
description: 'Run Django migrations after setup'
|
|
||||||
required: false
|
|
||||||
default: 'true'
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: "composite"
|
|
||||||
steps:
|
|
||||||
- name: Setup ssh
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
mkdir -p ~/.ssh
|
|
||||||
echo "${{ inputs.ssh-private-key }}" > ~/.ssh/id_ed25519
|
|
||||||
chmod 600 ~/.ssh/id_ed25519
|
|
||||||
ssh-keyscan git.envipath.com >> ~/.ssh/known_hosts
|
|
||||||
eval $(ssh-agent -s)
|
|
||||||
ssh-add ~/.ssh/id_ed25519
|
|
||||||
|
|
||||||
- name: Setup Python venv
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
uv sync --locked --all-extras --dev
|
|
||||||
|
|
||||||
- name: Install Playwright
|
|
||||||
if: inputs.skip-playwright == 'false'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
source .venv/bin/activate
|
|
||||||
playwright install --with-deps
|
|
||||||
|
|
||||||
- name: Build Frontend
|
|
||||||
if: inputs.skip-frontend == 'false'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
uv run python scripts/pnpm_wrapper.py install
|
|
||||||
cat << 'EOF' > pnpm-workspace.yaml
|
|
||||||
onlyBuiltDependencies:
|
|
||||||
- '@parcel/watcher'
|
|
||||||
- '@tailwindcss/oxide'
|
|
||||||
EOF
|
|
||||||
uv run python scripts/pnpm_wrapper.py run build
|
|
||||||
|
|
||||||
- name: Wait for Postgres
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
until pg_isready -h postgres -U ${{ env.POSTGRES_USER }}; do
|
|
||||||
echo "Waiting for postgres..."
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
echo "Postgres is ready!"
|
|
||||||
|
|
||||||
- name: Run Django Migrations
|
|
||||||
if: inputs.run-migrations == 'true'
|
|
||||||
shell: bash
|
|
||||||
run: |
|
|
||||||
source .venv/bin/activate
|
|
||||||
python manage.py migrate --noinput
|
|
||||||
@ -1,53 +0,0 @@
|
|||||||
# Custom CI Docker image for Gitea runners
|
|
||||||
# Pre-installs Node.js 24, pnpm 10, uv, and system dependencies
|
|
||||||
# to eliminate setup time in CI workflows
|
|
||||||
|
|
||||||
FROM ubuntu:24.04
|
|
||||||
|
|
||||||
# Prevent interactive prompts during package installation
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive
|
|
||||||
|
|
||||||
# Install system dependencies
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y \
|
|
||||||
postgresql-client \
|
|
||||||
redis-tools \
|
|
||||||
openjdk-11-jre-headless \
|
|
||||||
curl \
|
|
||||||
ca-certificates \
|
|
||||||
gnupg \
|
|
||||||
lsb-release \
|
|
||||||
git \
|
|
||||||
ssh \
|
|
||||||
libxrender1 \
|
|
||||||
libxext6 \
|
|
||||||
libfontconfig1 \
|
|
||||||
libfreetype6 \
|
|
||||||
libcairo2 \
|
|
||||||
libglib2.0-0t64 \
|
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Install Node.js 24 via NodeSource
|
|
||||||
RUN curl -fsSL https://deb.nodesource.com/setup_24.x | bash - && \
|
|
||||||
apt-get install -y nodejs && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# Enable corepack and install pnpm 10
|
|
||||||
RUN corepack enable && \
|
|
||||||
corepack prepare pnpm@10 --activate
|
|
||||||
|
|
||||||
# Install uv https://docs.astral.sh/uv/guides/integration/docker/#available-images
|
|
||||||
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
|
|
||||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
|
||||||
|
|
||||||
# Verify installations
|
|
||||||
RUN node --version && \
|
|
||||||
npm --version && \
|
|
||||||
pnpm --version && \
|
|
||||||
uv --version && \
|
|
||||||
pg_isready --version && \
|
|
||||||
redis-cli --version && \
|
|
||||||
java -version
|
|
||||||
|
|
||||||
# Set working directory
|
|
||||||
WORKDIR /workspace
|
|
||||||
@ -1,86 +0,0 @@
|
|||||||
name: API CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
paths:
|
|
||||||
- 'epapi/**'
|
|
||||||
- 'epdb/models.py' # API depends on models
|
|
||||||
- 'epdb/logic.py' # API depends on business logic
|
|
||||||
- 'tests/fixtures/**' # API tests use fixtures
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
api-tests:
|
|
||||||
if: ${{ !contains(gitea.event.pull_request.title, 'WIP') }}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
container:
|
|
||||||
image: git.envipath.com/envipath/envipy-ci:latest
|
|
||||||
|
|
||||||
services:
|
|
||||||
postgres:
|
|
||||||
image: postgres:16
|
|
||||||
env:
|
|
||||||
POSTGRES_USER: ${{ vars.POSTGRES_USER }}
|
|
||||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
||||||
POSTGRES_DB: ${{ vars.POSTGRES_DB }}
|
|
||||||
ports:
|
|
||||||
- ${{ vars.POSTGRES_PORT}}:5432
|
|
||||||
options: >-
|
|
||||||
--health-cmd="pg_isready -U postgres"
|
|
||||||
--health-interval=10s
|
|
||||||
--health-timeout=5s
|
|
||||||
--health-retries=5
|
|
||||||
|
|
||||||
env:
|
|
||||||
RUNNER_TOOL_CACHE: /toolcache
|
|
||||||
EP_DATA_DIR: /opt/enviPy/
|
|
||||||
ALLOWED_HOSTS: 127.0.0.1,localhost
|
|
||||||
DEBUG: True
|
|
||||||
LOG_LEVEL: INFO
|
|
||||||
MODEL_BUILDING_ENABLED: True
|
|
||||||
APPLICABILITY_DOMAIN_ENABLED: True
|
|
||||||
ENVIFORMER_PRESENT: True
|
|
||||||
ENVIFORMER_DEVICE: cpu
|
|
||||||
FLAG_CELERY_PRESENT: False
|
|
||||||
PLUGINS_ENABLED: True
|
|
||||||
SERVER_URL: http://localhost:8000
|
|
||||||
ADMIN_APPROVAL_REQUIRED: True
|
|
||||||
REGISTRATION_MANDATORY: True
|
|
||||||
LOG_DIR: ''
|
|
||||||
# DB
|
|
||||||
POSTGRES_SERVICE_NAME: postgres
|
|
||||||
POSTGRES_DB: ${{ vars.POSTGRES_DB }}
|
|
||||||
POSTGRES_USER: ${{ vars.POSTGRES_USER }}
|
|
||||||
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
|
|
||||||
POSTGRES_PORT: 5432
|
|
||||||
# SENTRY
|
|
||||||
SENTRY_ENABLED: False
|
|
||||||
# MS ENTRA
|
|
||||||
MS_ENTRA_ENABLED: False
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
# Use shared setup action - skips frontend builds for API-only tests
|
|
||||||
- name: Setup enviPy Environment
|
|
||||||
uses: ./.gitea/actions/setup-envipy
|
|
||||||
with:
|
|
||||||
skip-frontend: 'true'
|
|
||||||
skip-playwright: 'false'
|
|
||||||
ssh-private-key: ${{ secrets.ENVIPY_CI_PRIVATE_KEY }}
|
|
||||||
run-migrations: 'true'
|
|
||||||
|
|
||||||
- name: Run API tests
|
|
||||||
run: |
|
|
||||||
.venv/bin/python manage.py test epapi -v 2
|
|
||||||
|
|
||||||
- name: Test API endpoints availability
|
|
||||||
run: |
|
|
||||||
.venv/bin/python manage.py runserver 0.0.0.0:8000 &
|
|
||||||
SERVER_PID=$!
|
|
||||||
sleep 5
|
|
||||||
curl -f http://localhost:8000/api/v1/docs || echo "API docs not available"
|
|
||||||
kill $SERVER_PID
|
|
||||||
@ -1,48 +0,0 @@
|
|||||||
name: Build CI Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- develop
|
|
||||||
- main
|
|
||||||
paths:
|
|
||||||
- '.gitea/docker/Dockerfile.ci'
|
|
||||||
- '.gitea/workflows/build-ci-image.yaml'
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build-and-push:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to container registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: git.envipath.com
|
|
||||||
username: ${{ secrets.CI_REGISTRY_USER }}
|
|
||||||
password: ${{ secrets.CI_REGISTRY_PASSWORD }}
|
|
||||||
|
|
||||||
- name: Extract metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: git.envipath.com/envipath/envipy-ci
|
|
||||||
tags: |
|
|
||||||
type=raw,value=latest
|
|
||||||
type=sha,prefix={{branch}}-
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: .gitea/docker/Dockerfile.ci
|
|
||||||
push: true
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=registry,ref=git.envipath.com/envipath/envipy-ci:latest
|
|
||||||
cache-to: type=inline
|
|
||||||
@ -8,10 +8,7 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
test:
|
test:
|
||||||
if: ${{ !contains(gitea.event.pull_request.title, 'WIP') }}
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
|
||||||
image: git.envipath.com/envipath/envipy-ci:latest
|
|
||||||
|
|
||||||
services:
|
services:
|
||||||
postgres:
|
postgres:
|
||||||
@ -43,7 +40,7 @@ jobs:
|
|||||||
EP_DATA_DIR: /opt/enviPy/
|
EP_DATA_DIR: /opt/enviPy/
|
||||||
ALLOWED_HOSTS: 127.0.0.1,localhost
|
ALLOWED_HOSTS: 127.0.0.1,localhost
|
||||||
DEBUG: True
|
DEBUG: True
|
||||||
LOG_LEVEL: INFO
|
LOG_LEVEL: DEBUG
|
||||||
MODEL_BUILDING_ENABLED: True
|
MODEL_BUILDING_ENABLED: True
|
||||||
APPLICABILITY_DOMAIN_ENABLED: True
|
APPLICABILITY_DOMAIN_ENABLED: True
|
||||||
ENVIFORMER_PRESENT: True
|
ENVIFORMER_PRESENT: True
|
||||||
@ -66,22 +63,54 @@ jobs:
|
|||||||
MS_ENTRA_ENABLED: False
|
MS_ENTRA_ENABLED: False
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
# Use shared setup action - includes all dependencies and migrations
|
- name: Install system tools via apt
|
||||||
- name: Setup enviPy Environment
|
|
||||||
uses: ./.gitea/actions/setup-envipy
|
|
||||||
with:
|
|
||||||
skip-frontend: 'false'
|
|
||||||
skip-playwright: 'false'
|
|
||||||
ssh-private-key: ${{ secrets.ENVIPY_CI_PRIVATE_KEY }}
|
|
||||||
run-migrations: 'true'
|
|
||||||
|
|
||||||
- name: Run frontend tests
|
|
||||||
run: |
|
run: |
|
||||||
.venv/bin/python manage.py test --tag frontend
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y postgresql-client redis-tools openjdk-11-jre-headless
|
||||||
|
|
||||||
|
- name: Setup ssh
|
||||||
|
run: |
|
||||||
|
echo "${{ secrets.ENVIPY_CI_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
|
||||||
|
chmod 600 ~/.ssh/id_ed25519
|
||||||
|
ssh-keyscan git.envipath.com >> ~/.ssh/known_hosts
|
||||||
|
eval $(ssh-agent -s)
|
||||||
|
ssh-add ~/.ssh/id_ed25519
|
||||||
|
|
||||||
|
- name: Install pnpm
|
||||||
|
uses: pnpm/action-setup@v4
|
||||||
|
with:
|
||||||
|
version: 10
|
||||||
|
|
||||||
|
- name: Use Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 20
|
||||||
|
cache: "pnpm"
|
||||||
|
|
||||||
|
- name: Install uv
|
||||||
|
uses: astral-sh/setup-uv@v6
|
||||||
|
with:
|
||||||
|
enable-cache: true
|
||||||
|
|
||||||
|
- name: Setup venv
|
||||||
|
run: |
|
||||||
|
uv sync --locked --all-extras --dev
|
||||||
|
|
||||||
|
- name: Wait for services
|
||||||
|
run: |
|
||||||
|
until pg_isready -h postgres -U postgres; do sleep 2; done
|
||||||
|
# until redis-cli -h redis ping; do sleep 2; done
|
||||||
|
|
||||||
|
- name: Run Django Migrations
|
||||||
|
run: |
|
||||||
|
source .venv/bin/activate
|
||||||
|
python manage.py migrate --noinput
|
||||||
|
|
||||||
- name: Run Django tests
|
- name: Run Django tests
|
||||||
run: |
|
run: |
|
||||||
.venv/bin/python manage.py test tests --exclude-tag slow --exclude-tag frontend
|
source .venv/bin/activate
|
||||||
|
python manage.py test tests --exclude-tag slow
|
||||||
|
|||||||
374
.gitignore
vendored
374
.gitignore
vendored
@ -1,375 +1,17 @@
|
|||||||
|
*.pyc
|
||||||
|
|
||||||
|
|
||||||
### Python ###
|
|
||||||
|
|
||||||
# Byte-compiled / optimized / DLL files
|
|
||||||
__pycache__/
|
|
||||||
*.py[codz]
|
|
||||||
*$py.class
|
|
||||||
|
|
||||||
# C extensions
|
|
||||||
*.so
|
|
||||||
|
|
||||||
# Distribution / packaging
|
|
||||||
.Python
|
|
||||||
build/
|
|
||||||
develop-eggs/
|
|
||||||
dist/
|
|
||||||
downloads/
|
|
||||||
eggs/
|
|
||||||
.eggs/
|
|
||||||
lib/
|
|
||||||
lib64/
|
|
||||||
parts/
|
|
||||||
sdist/
|
|
||||||
var/
|
|
||||||
wheels/
|
|
||||||
share/python-wheels/
|
|
||||||
*.egg-info/
|
|
||||||
.installed.cfg
|
|
||||||
*.egg
|
|
||||||
MANIFEST
|
|
||||||
|
|
||||||
# PyInstaller
|
|
||||||
# Usually these files are written by a python script from a template
|
|
||||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
||||||
*.manifest
|
|
||||||
*.spec
|
|
||||||
|
|
||||||
# Installer logs
|
|
||||||
pip-log.txt
|
|
||||||
pip-delete-this-directory.txt
|
|
||||||
|
|
||||||
# Unit test / coverage reports
|
|
||||||
htmlcov/
|
|
||||||
.tox/
|
|
||||||
.nox/
|
|
||||||
.coverage
|
|
||||||
.coverage.*
|
|
||||||
.cache
|
|
||||||
nosetests.xml
|
|
||||||
coverage.xml
|
|
||||||
*.cover
|
|
||||||
*.py.cover
|
|
||||||
.hypothesis/
|
|
||||||
.pytest_cache/
|
|
||||||
cover/
|
|
||||||
|
|
||||||
# Translations
|
|
||||||
*.mo
|
|
||||||
*.pot
|
|
||||||
|
|
||||||
# Django stuff:
|
|
||||||
*.log
|
|
||||||
local_settings.py
|
|
||||||
db.sqlite3
|
db.sqlite3
|
||||||
db.sqlite3-journal
|
.idea/
|
||||||
static/admin/
|
static/admin/
|
||||||
static/django_extensions/
|
static/django_extensions/
|
||||||
|
|
||||||
# Flask stuff:
|
|
||||||
instance/
|
|
||||||
.webassets-cache
|
|
||||||
|
|
||||||
# Scrapy stuff:
|
|
||||||
.scrapy
|
|
||||||
|
|
||||||
# Sphinx documentation
|
|
||||||
docs/_build/
|
|
||||||
|
|
||||||
# PyBuilder
|
|
||||||
.pybuilder/
|
|
||||||
target/
|
|
||||||
|
|
||||||
# Jupyter Notebook
|
|
||||||
.ipynb_checkpoints
|
|
||||||
|
|
||||||
# IPython
|
|
||||||
profile_default/
|
|
||||||
ipython_config.py
|
|
||||||
|
|
||||||
|
|
||||||
# pdm
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
||||||
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
||||||
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
||||||
# pdm.lock
|
|
||||||
# pdm.toml
|
|
||||||
.pdm-python
|
|
||||||
.pdm-build/
|
|
||||||
|
|
||||||
# pixi
|
|
||||||
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
||||||
# pixi.lock
|
|
||||||
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
||||||
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
||||||
.pixi
|
|
||||||
|
|
||||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
||||||
__pypackages__/
|
|
||||||
|
|
||||||
# Celery stuff
|
|
||||||
celerybeat-schedule
|
|
||||||
celerybeat.pid
|
|
||||||
|
|
||||||
# Redis
|
|
||||||
*.rdb
|
|
||||||
*.aof
|
|
||||||
*.pid
|
|
||||||
|
|
||||||
# RabbitMQ
|
|
||||||
mnesia/
|
|
||||||
rabbitmq/
|
|
||||||
rabbitmq-data/
|
|
||||||
|
|
||||||
# ActiveMQ
|
|
||||||
activemq-data/
|
|
||||||
|
|
||||||
# SageMath parsed files
|
|
||||||
*.sage.py
|
|
||||||
|
|
||||||
# Environments
|
|
||||||
.env
|
.env
|
||||||
.envrc
|
|
||||||
.venv
|
|
||||||
env/
|
|
||||||
venv/
|
|
||||||
ENV/
|
|
||||||
env.bak/
|
|
||||||
venv.bak/
|
|
||||||
|
|
||||||
# Spyder project settings
|
|
||||||
.spyderproject
|
|
||||||
.spyproject
|
|
||||||
|
|
||||||
# Rope project settings
|
|
||||||
.ropeproject
|
|
||||||
|
|
||||||
# mkdocs documentation
|
|
||||||
/site
|
|
||||||
|
|
||||||
# mypy
|
|
||||||
.mypy_cache/
|
|
||||||
.dmypy.json
|
|
||||||
dmypy.json
|
|
||||||
|
|
||||||
# Pyre type checker
|
|
||||||
.pyre/
|
|
||||||
|
|
||||||
# pytype static type analyzer
|
|
||||||
.pytype/
|
|
||||||
|
|
||||||
# Cython debug symbols
|
|
||||||
cython_debug/
|
|
||||||
|
|
||||||
# PyCharm
|
|
||||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
||||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
||||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
||||||
.idea/
|
|
||||||
|
|
||||||
# Abstra
|
|
||||||
# Abstra is an AI-powered process automation framework.
|
|
||||||
# Ignore directories containing user credentials, local state, and settings.
|
|
||||||
# Learn more at https://abstra.io/docs
|
|
||||||
.abstra/
|
|
||||||
|
|
||||||
# Visual Studio Code
|
|
||||||
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
||||||
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
||||||
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
||||||
# you could uncomment the following to ignore the entire vscode folder
|
|
||||||
.vscode/
|
|
||||||
*.code-workspace
|
|
||||||
|
|
||||||
# Ruff stuff:
|
|
||||||
.ruff_cache/
|
|
||||||
|
|
||||||
# UV cache
|
|
||||||
.uv-cache/
|
|
||||||
|
|
||||||
# PyPI configuration file
|
|
||||||
.pypirc
|
|
||||||
|
|
||||||
# Marimo
|
|
||||||
marimo/_static/
|
|
||||||
marimo/_lsp/
|
|
||||||
__marimo__/
|
|
||||||
|
|
||||||
# Streamlit
|
|
||||||
.streamlit/secrets.toml
|
|
||||||
|
|
||||||
### Agents ###
|
|
||||||
.claude/
|
|
||||||
.codex/
|
|
||||||
.cursor/
|
|
||||||
.github/prompts/
|
|
||||||
.junie/
|
|
||||||
.windsurf/
|
|
||||||
|
|
||||||
AGENTS.md
|
|
||||||
CLAUDE.md
|
|
||||||
GEMINI.md
|
|
||||||
.aider.*
|
|
||||||
|
|
||||||
### Node.js ###
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# Diagnostic reports (https://nodejs.org/api/report.html)
|
|
||||||
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
|
||||||
|
|
||||||
# Runtime data
|
|
||||||
pids
|
|
||||||
*.pid
|
|
||||||
*.seed
|
|
||||||
*.pid.lock
|
|
||||||
|
|
||||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
|
||||||
lib-cov
|
|
||||||
|
|
||||||
# Coverage directory used by tools like istanbul
|
|
||||||
coverage
|
|
||||||
*.lcov
|
|
||||||
|
|
||||||
# nyc test coverage
|
|
||||||
.nyc_output
|
|
||||||
|
|
||||||
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
|
||||||
.grunt
|
|
||||||
|
|
||||||
# Bower dependency directory (https://bower.io/)
|
|
||||||
bower_components
|
|
||||||
|
|
||||||
# node-waf configuration
|
|
||||||
.lock-wscript
|
|
||||||
|
|
||||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
|
||||||
build/Release
|
|
||||||
|
|
||||||
# Dependency directories
|
|
||||||
node_modules/
|
|
||||||
jspm_packages/
|
|
||||||
|
|
||||||
# Snowpack dependency directory (https://snowpack.dev/)
|
|
||||||
web_modules/
|
|
||||||
|
|
||||||
# TypeScript cache
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
||||||
# Optional npm cache directory
|
|
||||||
.npm
|
|
||||||
|
|
||||||
# Optional eslint cache
|
|
||||||
.eslintcache
|
|
||||||
|
|
||||||
# Optional stylelint cache
|
|
||||||
.stylelintcache
|
|
||||||
|
|
||||||
# Optional REPL history
|
|
||||||
.node_repl_history
|
|
||||||
|
|
||||||
# Output of 'npm pack'
|
|
||||||
*.tgz
|
|
||||||
|
|
||||||
# Yarn Integrity file
|
|
||||||
.yarn-integrity
|
|
||||||
|
|
||||||
# dotenv environment variable files
|
|
||||||
.env
|
|
||||||
.env.*
|
|
||||||
!.env.example
|
|
||||||
|
|
||||||
# parcel-bundler cache (https://parceljs.org/)
|
|
||||||
.cache
|
|
||||||
.parcel-cache
|
|
||||||
|
|
||||||
# Next.js build output
|
|
||||||
.next
|
|
||||||
out
|
|
||||||
|
|
||||||
# Nuxt.js build / generate output
|
|
||||||
.nuxt
|
|
||||||
dist
|
|
||||||
.output
|
|
||||||
|
|
||||||
# Gatsby files
|
|
||||||
.cache/
|
|
||||||
# Comment in the public line in if your project uses Gatsby and not Next.js
|
|
||||||
# https://nextjs.org/blog/next-9-1#public-directory-support
|
|
||||||
# public
|
|
||||||
|
|
||||||
# vuepress build output
|
|
||||||
.vuepress/dist
|
|
||||||
|
|
||||||
# vuepress v2.x temp and cache directory
|
|
||||||
.temp
|
|
||||||
.cache
|
|
||||||
|
|
||||||
# Sveltekit cache directory
|
|
||||||
.svelte-kit/
|
|
||||||
|
|
||||||
# vitepress build output
|
|
||||||
**/.vitepress/dist
|
|
||||||
|
|
||||||
# vitepress cache directory
|
|
||||||
**/.vitepress/cache
|
|
||||||
|
|
||||||
# Docusaurus cache and generated files
|
|
||||||
.docusaurus
|
|
||||||
|
|
||||||
# Serverless directories
|
|
||||||
.serverless/
|
|
||||||
|
|
||||||
# FuseBox cache
|
|
||||||
.fusebox/
|
|
||||||
|
|
||||||
# DynamoDB Local files
|
|
||||||
.dynamodb/
|
|
||||||
|
|
||||||
# Firebase cache directory
|
|
||||||
.firebase/
|
|
||||||
|
|
||||||
# TernJS port file
|
|
||||||
.tern-port
|
|
||||||
|
|
||||||
# Stores VSCode versions used for testing VSCode extensions
|
|
||||||
.vscode-test
|
|
||||||
|
|
||||||
# yarn v3
|
|
||||||
.pnp.*
|
|
||||||
.yarn/*
|
|
||||||
!.yarn/patches
|
|
||||||
!.yarn/plugins
|
|
||||||
!.yarn/releases
|
|
||||||
!.yarn/sdks
|
|
||||||
!.yarn/versions
|
|
||||||
|
|
||||||
# Vite files
|
|
||||||
vite.config.js.timestamp-*
|
|
||||||
vite.config.ts.timestamp-*
|
|
||||||
.vite/
|
|
||||||
|
|
||||||
### Custom ###
|
|
||||||
|
|
||||||
debug.log
|
debug.log
|
||||||
scratches/
|
scratches/
|
||||||
test-results/
|
|
||||||
data/
|
|
||||||
*.arff
|
|
||||||
|
|
||||||
# Auto generated
|
data/
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
node_modules/
|
||||||
static/css/output.css
|
static/css/output.css
|
||||||
|
|
||||||
# macOS system files
|
*.code-workspace
|
||||||
.DS_Store
|
|
||||||
.Trashes
|
|
||||||
._*
|
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
from epapi.v1.router import router as v1_router # Refactored API from epdb.api_v2
|
from epdb.api import router as epdb_app_router
|
||||||
from epdb.legacy_api import router as epdb_legacy_app_router
|
from epdb.legacy_api import router as epdb_legacy_app_router
|
||||||
from ninja import NinjaAPI
|
from ninja import NinjaAPI
|
||||||
|
|
||||||
@ -8,5 +8,5 @@ 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")
|
||||||
|
|
||||||
# Add routers
|
# Add routers
|
||||||
api_v1.add_router("/", v1_router)
|
api_v1.add_router("/", epdb_app_router)
|
||||||
api_legacy.add_router("/", epdb_legacy_app_router)
|
api_legacy.add_router("/", epdb_legacy_app_router)
|
||||||
|
|||||||
@ -48,25 +48,10 @@ INSTALLED_APPS = [
|
|||||||
"django_extensions",
|
"django_extensions",
|
||||||
"oauth2_provider",
|
"oauth2_provider",
|
||||||
# Custom
|
# Custom
|
||||||
"epapi", # API endpoints (v1, etc.)
|
|
||||||
"epdb",
|
"epdb",
|
||||||
# "migration",
|
"migration",
|
||||||
]
|
]
|
||||||
|
|
||||||
TENANT = os.environ.get("TENANT", "public")
|
|
||||||
|
|
||||||
if TENANT != "public":
|
|
||||||
INSTALLED_APPS.append(TENANT)
|
|
||||||
|
|
||||||
EPDB_PACKAGE_MODEL = os.environ.get("EPDB_PACKAGE_MODEL", "epdb.Package")
|
|
||||||
|
|
||||||
|
|
||||||
def GET_PACKAGE_MODEL():
|
|
||||||
from django.apps import apps
|
|
||||||
|
|
||||||
return apps.get_model(EPDB_PACKAGE_MODEL)
|
|
||||||
|
|
||||||
|
|
||||||
AUTHENTICATION_BACKENDS = [
|
AUTHENTICATION_BACKENDS = [
|
||||||
"django.contrib.auth.backends.ModelBackend",
|
"django.contrib.auth.backends.ModelBackend",
|
||||||
]
|
]
|
||||||
@ -199,12 +184,6 @@ 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")
|
||||||
|
|
||||||
API_PAGINATION_DEFAULT_PAGE_SIZE = int(os.environ.get("API_PAGINATION_DEFAULT_PAGE_SIZE", 50))
|
|
||||||
PAGINATION_MAX_PER_PAGE_SIZE = int(
|
|
||||||
os.environ.get("API_PAGINATION_MAX_PAGE_SIZE", 100)
|
|
||||||
) # Ninja override
|
|
||||||
|
|
||||||
if not os.path.exists(PLUGIN_DIR):
|
if not os.path.exists(PLUGIN_DIR):
|
||||||
os.mkdir(PLUGIN_DIR)
|
os.mkdir(PLUGIN_DIR)
|
||||||
|
|
||||||
@ -362,7 +341,6 @@ FLAGS = {
|
|||||||
# -> /password_reset/done is covered as well
|
# -> /password_reset/done is covered as well
|
||||||
LOGIN_EXEMPT_URLS = [
|
LOGIN_EXEMPT_URLS = [
|
||||||
"/register",
|
"/register",
|
||||||
"/api/v1/", # Let API handle its own authentication
|
|
||||||
"/api/legacy/",
|
"/api/legacy/",
|
||||||
"/o/token/",
|
"/o/token/",
|
||||||
"/o/userinfo/",
|
"/o/userinfo/",
|
||||||
@ -374,7 +352,7 @@ LOGIN_EXEMPT_URLS = [
|
|||||||
"/cookie-policy",
|
"/cookie-policy",
|
||||||
"/about",
|
"/about",
|
||||||
"/contact",
|
"/contact",
|
||||||
"/careers",
|
"/jobs",
|
||||||
"/cite",
|
"/cite",
|
||||||
"/legal",
|
"/legal",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -23,20 +23,12 @@ from .api import api_v1, api_legacy
|
|||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", include("epdb.urls")),
|
path("", include("epdb.urls")),
|
||||||
|
path("", include("migration.urls")),
|
||||||
path("admin/", admin.site.urls),
|
path("admin/", admin.site.urls),
|
||||||
path("api/v1/", api_v1.urls),
|
path("api/v1/", api_v1.urls),
|
||||||
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 "migration" in s.INSTALLED_APPS:
|
|
||||||
urlpatterns.append(path("", include("migration.urls")))
|
|
||||||
|
|
||||||
if s.MS_ENTRA_ENABLED:
|
if s.MS_ENTRA_ENABLED:
|
||||||
urlpatterns.append(path("", include("epauth.urls")))
|
urlpatterns.append(path("", include("epauth.urls")))
|
||||||
|
|
||||||
# Custom error handlers
|
|
||||||
handler400 = "epdb.views.handler400"
|
|
||||||
handler403 = "epdb.views.handler403"
|
|
||||||
handler404 = "epdb.views.handler404"
|
|
||||||
handler500 = "epdb.views.handler500"
|
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
from django.apps import AppConfig
|
|
||||||
|
|
||||||
|
|
||||||
class EpapiConfig(AppConfig):
|
|
||||||
default_auto_field = "django.db.models.BigAutoField"
|
|
||||||
name = "epapi"
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# Tests for epapi app
|
|
||||||
@ -1 +0,0 @@
|
|||||||
# Tests for epapi v1 API
|
|
||||||
@ -1,532 +0,0 @@
|
|||||||
from django.test import TestCase, tag
|
|
||||||
|
|
||||||
from epdb.logic import GroupManager, PackageManager, UserManager
|
|
||||||
from epdb.models import (
|
|
||||||
Compound,
|
|
||||||
GroupPackagePermission,
|
|
||||||
Permission,
|
|
||||||
UserPackagePermission,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class APIPermissionTestBase(TestCase):
|
|
||||||
"""
|
|
||||||
Base class for API permission tests.
|
|
||||||
|
|
||||||
Sets up common test data:
|
|
||||||
- user1: Owner of packages
|
|
||||||
- user2: User with various permissions
|
|
||||||
- user3: User with no permissions
|
|
||||||
- reviewed_package: Public package (reviewed=True)
|
|
||||||
- unreviewed_package_owned: Unreviewed package owned by user1
|
|
||||||
- unreviewed_package_read: Unreviewed package with READ permission for user2
|
|
||||||
- unreviewed_package_write: Unreviewed package with WRITE permission for user2
|
|
||||||
- unreviewed_package_all: Unreviewed package with ALL permission for user2
|
|
||||||
- unreviewed_package_no_access: Unreviewed package with no permissions for user2/user3
|
|
||||||
- group_package: Unreviewed package accessible via group permission
|
|
||||||
- test_group: Group containing user2
|
|
||||||
"""
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
# Create users
|
|
||||||
cls.user1 = UserManager.create_user(
|
|
||||||
"permission-user1",
|
|
||||||
"permission-user1@envipath.com",
|
|
||||||
"SuperSafe",
|
|
||||||
set_setting=False,
|
|
||||||
add_to_group=False,
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
cls.user2 = UserManager.create_user(
|
|
||||||
"permission-user2",
|
|
||||||
"permission-user2@envipath.com",
|
|
||||||
"SuperSafe",
|
|
||||||
set_setting=False,
|
|
||||||
add_to_group=False,
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
cls.user3 = UserManager.create_user(
|
|
||||||
"permission-user3",
|
|
||||||
"permission-user3@envipath.com",
|
|
||||||
"SuperSafe",
|
|
||||||
set_setting=False,
|
|
||||||
add_to_group=False,
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete default packages to ensure clean test data
|
|
||||||
for user in [cls.user1, cls.user2, cls.user3]:
|
|
||||||
default_pkg = user.default_package
|
|
||||||
user.default_package = None
|
|
||||||
user.save()
|
|
||||||
if default_pkg:
|
|
||||||
default_pkg.delete()
|
|
||||||
|
|
||||||
# Create reviewed package (public)
|
|
||||||
cls.reviewed_package = PackageManager.create_package(
|
|
||||||
cls.user1, "Reviewed Package", "Public package"
|
|
||||||
)
|
|
||||||
cls.reviewed_package.reviewed = True
|
|
||||||
cls.reviewed_package.save()
|
|
||||||
|
|
||||||
# Create unreviewed packages with various permissions
|
|
||||||
cls.unreviewed_package_owned = PackageManager.create_package(
|
|
||||||
cls.user1, "User1 Owned Package", "Owned by user1"
|
|
||||||
)
|
|
||||||
|
|
||||||
cls.unreviewed_package_read = PackageManager.create_package(
|
|
||||||
cls.user1, "User2 Read Package", "User2 has READ permission"
|
|
||||||
)
|
|
||||||
UserPackagePermission.objects.create(
|
|
||||||
user=cls.user2, package=cls.unreviewed_package_read, permission=Permission.READ[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
cls.unreviewed_package_write = PackageManager.create_package(
|
|
||||||
cls.user1, "User2 Write Package", "User2 has WRITE permission"
|
|
||||||
)
|
|
||||||
UserPackagePermission.objects.create(
|
|
||||||
user=cls.user2, package=cls.unreviewed_package_write, permission=Permission.WRITE[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
cls.unreviewed_package_all = PackageManager.create_package(
|
|
||||||
cls.user1, "User2 All Package", "User2 has ALL permission"
|
|
||||||
)
|
|
||||||
UserPackagePermission.objects.create(
|
|
||||||
user=cls.user2, package=cls.unreviewed_package_all, permission=Permission.ALL[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
cls.unreviewed_package_no_access = PackageManager.create_package(
|
|
||||||
cls.user1, "No Access Package", "No permissions for user2/user3"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create group and group package
|
|
||||||
cls.test_group = GroupManager.create_group(
|
|
||||||
cls.user1, "Test Group", "Group for permission testing"
|
|
||||||
)
|
|
||||||
cls.test_group.user_member.add(cls.user2)
|
|
||||||
cls.test_group.save()
|
|
||||||
|
|
||||||
cls.group_package = PackageManager.create_package(
|
|
||||||
cls.user1, "Group Package", "Accessible via group permission"
|
|
||||||
)
|
|
||||||
GroupPackagePermission.objects.create(
|
|
||||||
group=cls.test_group, package=cls.group_package, permission=Permission.READ[0]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create test compounds in each package
|
|
||||||
cls.reviewed_compound = Compound.create(
|
|
||||||
cls.reviewed_package, "C", "Reviewed Compound", "Test compound"
|
|
||||||
)
|
|
||||||
cls.owned_compound = Compound.create(
|
|
||||||
cls.unreviewed_package_owned, "CC", "Owned Compound", "Test compound"
|
|
||||||
)
|
|
||||||
cls.read_compound = Compound.create(
|
|
||||||
cls.unreviewed_package_read, "CCC", "Read Compound", "Test compound"
|
|
||||||
)
|
|
||||||
cls.write_compound = Compound.create(
|
|
||||||
cls.unreviewed_package_write, "CCCC", "Write Compound", "Test compound"
|
|
||||||
)
|
|
||||||
cls.all_compound = Compound.create(
|
|
||||||
cls.unreviewed_package_all, "CCCCC", "All Compound", "Test compound"
|
|
||||||
)
|
|
||||||
cls.no_access_compound = Compound.create(
|
|
||||||
cls.unreviewed_package_no_access, "CCCCCC", "No Access Compound", "Test compound"
|
|
||||||
)
|
|
||||||
cls.group_compound = Compound.create(
|
|
||||||
cls.group_package, "CCCCCCC", "Group Compound", "Test compound"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class PackageListPermissionTest(APIPermissionTestBase):
|
|
||||||
"""
|
|
||||||
Test permissions for /api/v1/packages/ endpoint.
|
|
||||||
|
|
||||||
Special case: This endpoint allows anonymous access (auth=None)
|
|
||||||
"""
|
|
||||||
|
|
||||||
ENDPOINT = "/api/v1/packages/"
|
|
||||||
|
|
||||||
def test_anonymous_user_sees_only_reviewed_packages(self):
|
|
||||||
"""Anonymous users should only see reviewed packages."""
|
|
||||||
self.client.logout()
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# Should only see reviewed package
|
|
||||||
self.assertEqual(payload["total_items"], 1)
|
|
||||||
self.assertEqual(payload["items"][0]["uuid"], str(self.reviewed_package.uuid))
|
|
||||||
self.assertEqual(payload["items"][0]["review_status"], "reviewed")
|
|
||||||
|
|
||||||
def test_authenticated_user_sees_all_readable_packages(self):
|
|
||||||
"""Authenticated users see reviewed + packages they have access to."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# user2 should see:
|
|
||||||
# - reviewed_package (public)
|
|
||||||
# - unreviewed_package_read (READ permission)
|
|
||||||
# - unreviewed_package_write (WRITE permission)
|
|
||||||
# - unreviewed_package_all (ALL permission)
|
|
||||||
# - group_package (via group membership)
|
|
||||||
# Total: 5 packages
|
|
||||||
self.assertEqual(payload["total_items"], 5)
|
|
||||||
|
|
||||||
visible_uuids = {item["uuid"] for item in payload["items"]}
|
|
||||||
expected_uuids = {
|
|
||||||
str(self.reviewed_package.uuid),
|
|
||||||
str(self.unreviewed_package_read.uuid),
|
|
||||||
str(self.unreviewed_package_write.uuid),
|
|
||||||
str(self.unreviewed_package_all.uuid),
|
|
||||||
str(self.group_package.uuid),
|
|
||||||
}
|
|
||||||
self.assertEqual(visible_uuids, expected_uuids)
|
|
||||||
|
|
||||||
def test_owner_sees_all_owned_packages(self):
|
|
||||||
"""Package owner sees all packages they created."""
|
|
||||||
self.client.force_login(self.user1)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# user1 owns all packages
|
|
||||||
# Total: 7 packages (all packages created in setUpTestData)
|
|
||||||
self.assertEqual(payload["total_items"], 7)
|
|
||||||
|
|
||||||
def test_filter_by_review_status_true(self):
|
|
||||||
"""Filter to show only reviewed packages."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get(self.ENDPOINT, {"review_status": True})
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# Only reviewed_package
|
|
||||||
self.assertEqual(payload["total_items"], 1)
|
|
||||||
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
|
|
||||||
|
|
||||||
def test_filter_by_review_status_false(self):
|
|
||||||
"""Filter to show only unreviewed packages."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get(self.ENDPOINT, {"review_status": False})
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# user2's accessible unreviewed packages: 4
|
|
||||||
self.assertEqual(payload["total_items"], 4)
|
|
||||||
self.assertTrue(all(item["review_status"] == "unreviewed" for item in payload["items"]))
|
|
||||||
|
|
||||||
def test_anonymous_filter_unreviewed_returns_empty(self):
|
|
||||||
"""Anonymous users get no results when filtering for unreviewed."""
|
|
||||||
self.client.logout()
|
|
||||||
response = self.client.get(self.ENDPOINT, {"review_status": False})
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
self.assertEqual(payload["total_items"], 0)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class GlobalCompoundListPermissionTest(APIPermissionTestBase):
|
|
||||||
"""
|
|
||||||
Test permissions for /api/v1/compounds/ endpoint.
|
|
||||||
|
|
||||||
This endpoint requires authentication.
|
|
||||||
"""
|
|
||||||
|
|
||||||
ENDPOINT = "/api/v1/compounds/"
|
|
||||||
|
|
||||||
def test_anonymous_user_cannot_access(self):
|
|
||||||
"""Anonymous users should get 401 Unauthorized."""
|
|
||||||
self.client.logout()
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 401)
|
|
||||||
|
|
||||||
def test_authenticated_user_sees_compounds_from_readable_packages(self):
|
|
||||||
"""Authenticated users see compounds from packages they can read."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# user2 should see compounds from:
|
|
||||||
# - reviewed_package (public)
|
|
||||||
# - unreviewed_package_read (READ permission)
|
|
||||||
# - unreviewed_package_write (WRITE permission)
|
|
||||||
# - unreviewed_package_all (ALL permission)
|
|
||||||
# - group_package (via group membership)
|
|
||||||
# Total: 5 compounds
|
|
||||||
self.assertEqual(payload["total_items"], 5)
|
|
||||||
|
|
||||||
visible_uuids = {item["uuid"] for item in payload["items"]}
|
|
||||||
expected_uuids = {
|
|
||||||
str(self.reviewed_compound.uuid),
|
|
||||||
str(self.read_compound.uuid),
|
|
||||||
str(self.write_compound.uuid),
|
|
||||||
str(self.all_compound.uuid),
|
|
||||||
str(self.group_compound.uuid),
|
|
||||||
}
|
|
||||||
self.assertEqual(visible_uuids, expected_uuids)
|
|
||||||
|
|
||||||
def test_user_without_permission_cannot_see_compound(self):
|
|
||||||
"""User without permission to package cannot see its compounds."""
|
|
||||||
self.client.force_login(self.user3)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# user3 should only see compounds from reviewed_package
|
|
||||||
self.assertEqual(payload["total_items"], 1)
|
|
||||||
self.assertEqual(payload["items"][0]["uuid"], str(self.reviewed_compound.uuid))
|
|
||||||
|
|
||||||
def test_owner_sees_all_compounds(self):
|
|
||||||
"""Package owner sees all compounds they created."""
|
|
||||||
self.client.force_login(self.user1)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# user1 owns all packages, so sees all compounds
|
|
||||||
self.assertEqual(payload["total_items"], 7)
|
|
||||||
|
|
||||||
def test_read_permission_allows_viewing(self):
|
|
||||||
"""READ permission allows viewing compounds."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# Check that read_compound is included
|
|
||||||
uuids = [item["uuid"] for item in payload["items"]]
|
|
||||||
self.assertIn(str(self.read_compound.uuid), uuids)
|
|
||||||
|
|
||||||
def test_write_permission_allows_viewing(self):
|
|
||||||
"""WRITE permission also allows viewing compounds."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# Check that write_compound is included
|
|
||||||
uuids = [item["uuid"] for item in payload["items"]]
|
|
||||||
self.assertIn(str(self.write_compound.uuid), uuids)
|
|
||||||
|
|
||||||
def test_all_permission_allows_viewing(self):
|
|
||||||
"""ALL permission allows viewing compounds."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# Check that all_compound is included
|
|
||||||
uuids = [item["uuid"] for item in payload["items"]]
|
|
||||||
self.assertIn(str(self.all_compound.uuid), uuids)
|
|
||||||
|
|
||||||
def test_group_permission_allows_viewing(self):
|
|
||||||
"""Group membership grants access to group-permitted packages."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# Check that group_compound is included
|
|
||||||
uuids = [item["uuid"] for item in payload["items"]]
|
|
||||||
self.assertIn(str(self.group_compound.uuid), uuids)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class PackageScopedCompoundListPermissionTest(APIPermissionTestBase):
|
|
||||||
"""
|
|
||||||
Test permissions for /api/v1/package/{uuid}/compound/ endpoint.
|
|
||||||
|
|
||||||
This endpoint requires authentication AND package access.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_anonymous_user_cannot_access_reviewed_package(self):
|
|
||||||
"""Anonymous users should get 401 even for reviewed packages."""
|
|
||||||
self.client.logout()
|
|
||||||
endpoint = f"/api/v1/package/{self.reviewed_package.uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 401)
|
|
||||||
|
|
||||||
def test_authenticated_user_can_access_reviewed_package(self):
|
|
||||||
"""Authenticated users can access reviewed packages."""
|
|
||||||
self.client.force_login(self.user3)
|
|
||||||
endpoint = f"/api/v1/package/{self.reviewed_package.uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
self.assertEqual(payload["total_items"], 1)
|
|
||||||
self.assertEqual(payload["items"][0]["uuid"], str(self.reviewed_compound.uuid))
|
|
||||||
|
|
||||||
def test_user_can_access_package_with_read_permission(self):
|
|
||||||
"""User with READ permission can access package-scoped endpoint."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
endpoint = f"/api/v1/package/{self.unreviewed_package_read.uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
self.assertEqual(payload["total_items"], 1)
|
|
||||||
self.assertEqual(payload["items"][0]["uuid"], str(self.read_compound.uuid))
|
|
||||||
|
|
||||||
def test_user_can_access_package_with_write_permission(self):
|
|
||||||
"""User with WRITE permission can access package-scoped endpoint."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
endpoint = f"/api/v1/package/{self.unreviewed_package_write.uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
self.assertEqual(payload["total_items"], 1)
|
|
||||||
self.assertEqual(payload["items"][0]["uuid"], str(self.write_compound.uuid))
|
|
||||||
|
|
||||||
def test_user_can_access_package_with_all_permission(self):
|
|
||||||
"""User with ALL permission can access package-scoped endpoint."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
endpoint = f"/api/v1/package/{self.unreviewed_package_all.uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
self.assertEqual(payload["total_items"], 1)
|
|
||||||
self.assertEqual(payload["items"][0]["uuid"], str(self.all_compound.uuid))
|
|
||||||
|
|
||||||
def test_user_cannot_access_package_without_permission(self):
|
|
||||||
"""User without permission gets 403 Forbidden."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
endpoint = f"/api/v1/package/{self.unreviewed_package_no_access.uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
def test_nonexistent_package_returns_404(self):
|
|
||||||
"""Request for non-existent package returns 404."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
fake_uuid = "00000000-0000-0000-0000-000000000000"
|
|
||||||
endpoint = f"/api/v1/package/{fake_uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 404)
|
|
||||||
|
|
||||||
def test_owner_can_access_owned_package(self):
|
|
||||||
"""Package owner can access their package."""
|
|
||||||
self.client.force_login(self.user1)
|
|
||||||
endpoint = f"/api/v1/package/{self.unreviewed_package_owned.uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
self.assertEqual(payload["total_items"], 1)
|
|
||||||
self.assertEqual(payload["items"][0]["uuid"], str(self.owned_compound.uuid))
|
|
||||||
|
|
||||||
def test_group_member_can_access_group_package(self):
|
|
||||||
"""Group member can access package via group permission."""
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
endpoint = f"/api/v1/package/{self.group_package.uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
self.assertEqual(payload["total_items"], 1)
|
|
||||||
self.assertEqual(payload["items"][0]["uuid"], str(self.group_compound.uuid))
|
|
||||||
|
|
||||||
def test_non_group_member_cannot_access_group_package(self):
|
|
||||||
"""Non-group member cannot access package with only group permission."""
|
|
||||||
self.client.force_login(self.user3)
|
|
||||||
endpoint = f"/api/v1/package/{self.group_package.uuid}/compound/"
|
|
||||||
response = self.client.get(endpoint)
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 403)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class MultiResourcePermissionTest(APIPermissionTestBase):
|
|
||||||
"""
|
|
||||||
Test that permission system works consistently across all resource types.
|
|
||||||
|
|
||||||
Tests a sample of other endpoints to ensure permission logic is consistent.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def test_rules_endpoint_respects_permissions(self):
|
|
||||||
"""Rules endpoint uses same permission logic."""
|
|
||||||
from epdb.models import SimpleAmbitRule
|
|
||||||
|
|
||||||
# Create rule in no-access package
|
|
||||||
rule = SimpleAmbitRule.create(
|
|
||||||
self.unreviewed_package_no_access, "Test Rule", "Test", "[C:1]>>[C:1]O"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get("/api/v1/rules/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# user2 should not see the rule from no_access_package
|
|
||||||
rule_uuids = [item["uuid"] for item in payload["items"]]
|
|
||||||
self.assertNotIn(str(rule.uuid), rule_uuids)
|
|
||||||
|
|
||||||
def test_reactions_endpoint_respects_permissions(self):
|
|
||||||
"""Reactions endpoint uses same permission logic."""
|
|
||||||
from epdb.models import Reaction
|
|
||||||
|
|
||||||
# Create reaction in no-access package
|
|
||||||
reaction = Reaction.create(
|
|
||||||
self.unreviewed_package_no_access, "Test Reaction", "Test", ["C"], ["CO"]
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get("/api/v1/reactions/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# user2 should not see the reaction from no_access_package
|
|
||||||
reaction_uuids = [item["uuid"] for item in payload["items"]]
|
|
||||||
self.assertNotIn(str(reaction.uuid), reaction_uuids)
|
|
||||||
|
|
||||||
def test_pathways_endpoint_respects_permissions(self):
|
|
||||||
"""Pathways endpoint uses same permission logic."""
|
|
||||||
from epdb.models import Pathway
|
|
||||||
|
|
||||||
# Create pathway in no-access package
|
|
||||||
pathway = Pathway.objects.create(
|
|
||||||
package=self.unreviewed_package_no_access, name="Test Pathway", description="Test"
|
|
||||||
)
|
|
||||||
|
|
||||||
self.client.force_login(self.user2)
|
|
||||||
response = self.client.get("/api/v1/pathways/")
|
|
||||||
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
payload = response.json()
|
|
||||||
|
|
||||||
# user2 should not see the pathway from no_access_package
|
|
||||||
pathway_uuids = [item["uuid"] for item in payload["items"]]
|
|
||||||
self.assertNotIn(str(pathway.uuid), pathway_uuids)
|
|
||||||
@ -1,477 +0,0 @@
|
|||||||
from django.test import TestCase, tag
|
|
||||||
|
|
||||||
from epdb.logic import PackageManager, UserManager
|
|
||||||
from epdb.models import Compound, Reaction, Pathway, EPModel, SimpleAmbitRule, Scenario
|
|
||||||
|
|
||||||
|
|
||||||
class BaseTestAPIGetPaginated:
|
|
||||||
"""
|
|
||||||
Mixin class for API pagination tests.
|
|
||||||
|
|
||||||
Subclasses must inherit from both this class and TestCase, e.g.:
|
|
||||||
class MyTest(BaseTestAPIGetPaginated, TestCase):
|
|
||||||
...
|
|
||||||
|
|
||||||
Subclasses must define:
|
|
||||||
- resource_name: Singular name (e.g., "compound")
|
|
||||||
- resource_name_plural: Plural name (e.g., "compounds")
|
|
||||||
- global_endpoint: Global listing endpoint (e.g., "/api/v1/compounds/")
|
|
||||||
- package_endpoint_template: Template for package-scoped endpoint or None
|
|
||||||
- total_reviewed: Number of reviewed items to create
|
|
||||||
- total_unreviewed: Number of unreviewed items to create
|
|
||||||
- create_reviewed_resource(cls, package, idx): Factory method
|
|
||||||
- create_unreviewed_resource(cls, package, idx): Factory method
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Configuration to be overridden by subclasses
|
|
||||||
resource_name = None
|
|
||||||
resource_name_plural = None
|
|
||||||
global_endpoint = None
|
|
||||||
package_endpoint_template = None
|
|
||||||
total_reviewed = 50
|
|
||||||
total_unreviewed = 20
|
|
||||||
default_page_size = 50
|
|
||||||
max_page_size = 100
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
# Create test user
|
|
||||||
cls.user = UserManager.create_user(
|
|
||||||
f"{cls.resource_name}-user",
|
|
||||||
f"{cls.resource_name}-user@envipath.com",
|
|
||||||
"SuperSafe",
|
|
||||||
set_setting=False,
|
|
||||||
add_to_group=False,
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete the auto-created default package to ensure clean test data
|
|
||||||
default_pkg = cls.user.default_package
|
|
||||||
cls.user.default_package = None
|
|
||||||
cls.user.save()
|
|
||||||
default_pkg.delete()
|
|
||||||
|
|
||||||
# Create reviewed package
|
|
||||||
cls.reviewed_package = PackageManager.create_package(
|
|
||||||
cls.user, "Reviewed Package", f"Reviewed package for {cls.resource_name} tests"
|
|
||||||
)
|
|
||||||
cls.reviewed_package.reviewed = True
|
|
||||||
cls.reviewed_package.save()
|
|
||||||
|
|
||||||
# Create unreviewed package
|
|
||||||
cls.unreviewed_package = PackageManager.create_package(
|
|
||||||
cls.user, "Draft Package", f"Unreviewed package for {cls.resource_name} tests"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Create reviewed resources
|
|
||||||
for idx in range(cls.total_reviewed):
|
|
||||||
cls.create_reviewed_resource(cls.reviewed_package, idx)
|
|
||||||
|
|
||||||
# Create unreviewed resources
|
|
||||||
for idx in range(cls.total_unreviewed):
|
|
||||||
cls.create_unreviewed_resource(cls.unreviewed_package, idx)
|
|
||||||
|
|
||||||
# Set up package-scoped endpoints if applicable
|
|
||||||
if cls.package_endpoint_template:
|
|
||||||
cls.reviewed_package_endpoint = cls.package_endpoint_template.format(
|
|
||||||
uuid=cls.reviewed_package.uuid
|
|
||||||
)
|
|
||||||
cls.unreviewed_package_endpoint = cls.package_endpoint_template.format(
|
|
||||||
uuid=cls.unreviewed_package.uuid
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_reviewed_resource(cls, package, idx):
|
|
||||||
"""
|
|
||||||
Create a single reviewed resource.
|
|
||||||
Must be implemented by subclass.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
package: The package to create the resource in
|
|
||||||
idx: Index of the resource (0-based)
|
|
||||||
"""
|
|
||||||
raise NotImplementedError(f"{cls.__name__} must implement create_reviewed_resource()")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_unreviewed_resource(cls, package, idx):
|
|
||||||
"""
|
|
||||||
Create a single unreviewed resource.
|
|
||||||
Must be implemented by subclass.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
package: The package to create the resource in
|
|
||||||
idx: Index of the resource (0-based)
|
|
||||||
"""
|
|
||||||
raise NotImplementedError(f"{cls.__name__} must implement create_unreviewed_resource()")
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
def test_requires_session_authentication(self):
|
|
||||||
"""Test that the global endpoint requires authentication."""
|
|
||||||
self.client.logout()
|
|
||||||
response = self.client.get(self.global_endpoint)
|
|
||||||
self.assertEqual(response.status_code, 401)
|
|
||||||
|
|
||||||
def test_global_listing_uses_default_page_size(self):
|
|
||||||
"""Test that the global endpoint uses default pagination settings."""
|
|
||||||
response = self.client.get(self.global_endpoint, {"review_status": True})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
self.assertEqual(payload["page"], 1)
|
|
||||||
self.assertEqual(payload["page_size"], self.default_page_size)
|
|
||||||
self.assertEqual(payload["total_items"], self.total_reviewed)
|
|
||||||
|
|
||||||
# Verify only reviewed items are returned
|
|
||||||
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
|
|
||||||
|
|
||||||
def test_can_request_later_page(self):
|
|
||||||
"""Test that pagination works for later pages."""
|
|
||||||
if self.total_reviewed <= self.default_page_size:
|
|
||||||
self.skipTest(
|
|
||||||
f"Not enough items to test pagination "
|
|
||||||
f"({self.total_reviewed} <= {self.default_page_size})"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.client.get(self.global_endpoint, {"page": 2})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
self.assertEqual(payload["page"], 2)
|
|
||||||
|
|
||||||
# Calculate expected items on page 2
|
|
||||||
expected_items = min(self.default_page_size, self.total_reviewed - self.default_page_size)
|
|
||||||
self.assertEqual(len(payload["items"]), expected_items)
|
|
||||||
|
|
||||||
# Verify only reviewed items are returned
|
|
||||||
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
|
|
||||||
|
|
||||||
def test_page_size_is_capped(self):
|
|
||||||
"""Test that page size is capped at the maximum."""
|
|
||||||
if self.total_reviewed <= self.max_page_size:
|
|
||||||
self.skipTest(
|
|
||||||
f"Not enough items to test page size cap "
|
|
||||||
f"({self.total_reviewed} <= {self.max_page_size})"
|
|
||||||
)
|
|
||||||
|
|
||||||
response = self.client.get(self.global_endpoint, {"page_size": 150})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
self.assertEqual(payload["page_size"], self.max_page_size)
|
|
||||||
self.assertEqual(len(payload["items"]), self.max_page_size)
|
|
||||||
|
|
||||||
def test_package_endpoint_for_reviewed_package(self):
|
|
||||||
"""Test the package-scoped endpoint for reviewed packages."""
|
|
||||||
if not self.package_endpoint_template:
|
|
||||||
self.skipTest("No package endpoint for this resource")
|
|
||||||
|
|
||||||
response = self.client.get(self.reviewed_package_endpoint)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
self.assertEqual(payload["total_items"], self.total_reviewed)
|
|
||||||
|
|
||||||
# Verify only reviewed items are returned
|
|
||||||
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
|
|
||||||
|
|
||||||
def test_package_endpoint_for_unreviewed_package(self):
|
|
||||||
"""Test the package-scoped endpoint for unreviewed packages."""
|
|
||||||
if not self.package_endpoint_template:
|
|
||||||
self.skipTest("No package endpoint for this resource")
|
|
||||||
|
|
||||||
response = self.client.get(self.unreviewed_package_endpoint)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
self.assertEqual(payload["total_items"], self.total_unreviewed)
|
|
||||||
|
|
||||||
# Verify only unreviewed items are returned
|
|
||||||
self.assertTrue(all(item["review_status"] == "unreviewed" for item in payload["items"]))
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class PackagePaginationAPITest(TestCase):
|
|
||||||
ENDPOINT = "/api/v1/packages/"
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def setUpTestData(cls):
|
|
||||||
cls.user = UserManager.create_user(
|
|
||||||
"package-user",
|
|
||||||
"package-user@envipath.com",
|
|
||||||
"SuperSafe",
|
|
||||||
set_setting=False,
|
|
||||||
add_to_group=False,
|
|
||||||
is_active=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Delete the auto-created default package to ensure clean test data
|
|
||||||
default_pkg = cls.user.default_package
|
|
||||||
cls.user.default_package = None
|
|
||||||
cls.user.save()
|
|
||||||
default_pkg.delete()
|
|
||||||
|
|
||||||
# Create reviewed packages
|
|
||||||
cls.total_reviewed = 25
|
|
||||||
for idx in range(cls.total_reviewed):
|
|
||||||
package = PackageManager.create_package(
|
|
||||||
cls.user, f"Reviewed Package {idx:03d}", "Reviewed package for tests"
|
|
||||||
)
|
|
||||||
package.reviewed = True
|
|
||||||
package.save()
|
|
||||||
|
|
||||||
# Create unreviewed packages
|
|
||||||
cls.total_unreviewed = 15
|
|
||||||
for idx in range(cls.total_unreviewed):
|
|
||||||
PackageManager.create_package(
|
|
||||||
cls.user, f"Draft Package {idx:03d}", "Unreviewed package for tests"
|
|
||||||
)
|
|
||||||
|
|
||||||
def setUp(self):
|
|
||||||
self.client.force_login(self.user)
|
|
||||||
|
|
||||||
def test_anonymous_can_access_reviewed_packages(self):
|
|
||||||
self.client.logout()
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
# Anonymous users can only see reviewed packages
|
|
||||||
self.assertEqual(payload["total_items"], self.total_reviewed)
|
|
||||||
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
|
|
||||||
|
|
||||||
def test_listing_uses_default_page_size(self):
|
|
||||||
response = self.client.get(self.ENDPOINT)
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
self.assertEqual(payload["page"], 1)
|
|
||||||
self.assertEqual(payload["page_size"], 50)
|
|
||||||
self.assertEqual(payload["total_items"], self.total_reviewed + self.total_unreviewed)
|
|
||||||
|
|
||||||
def test_reviewed_filter_true(self):
|
|
||||||
response = self.client.get(self.ENDPOINT, {"review_status": True})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
self.assertEqual(payload["total_items"], self.total_reviewed)
|
|
||||||
self.assertTrue(all(item["review_status"] == "reviewed" for item in payload["items"]))
|
|
||||||
|
|
||||||
def test_reviewed_filter_false(self):
|
|
||||||
response = self.client.get(self.ENDPOINT, {"review_status": False})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
self.assertEqual(payload["total_items"], self.total_unreviewed)
|
|
||||||
self.assertTrue(all(item["review_status"] == "unreviewed" for item in payload["items"]))
|
|
||||||
|
|
||||||
def test_reviewed_filter_false_anonymous(self):
|
|
||||||
self.client.logout()
|
|
||||||
response = self.client.get(self.ENDPOINT, {"review_status": False})
|
|
||||||
self.assertEqual(response.status_code, 200)
|
|
||||||
|
|
||||||
payload = response.json()
|
|
||||||
# Anonymous users cannot access unreviewed packages
|
|
||||||
self.assertEqual(payload["total_items"], 0)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|
||||||
"""Compound pagination tests using base class."""
|
|
||||||
|
|
||||||
resource_name = "compound"
|
|
||||||
resource_name_plural = "compounds"
|
|
||||||
global_endpoint = "/api/v1/compounds/"
|
|
||||||
package_endpoint_template = "/api/v1/package/{uuid}/compound/"
|
|
||||||
total_reviewed = 125
|
|
||||||
total_unreviewed = 35
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_reviewed_resource(cls, package, idx):
|
|
||||||
simple_smiles = ["C", "CC", "CCC", "CCCC", "CCCCC"]
|
|
||||||
smiles = simple_smiles[idx % len(simple_smiles)] + ("O" * (idx // len(simple_smiles)))
|
|
||||||
return Compound.create(
|
|
||||||
package,
|
|
||||||
smiles,
|
|
||||||
f"Reviewed Compound {idx:03d}",
|
|
||||||
"Compound for pagination tests",
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_unreviewed_resource(cls, package, idx):
|
|
||||||
simple_smiles = ["C", "CC", "CCC", "CCCC", "CCCCC"]
|
|
||||||
smiles = simple_smiles[idx % len(simple_smiles)] + ("N" * (idx // len(simple_smiles)))
|
|
||||||
return Compound.create(
|
|
||||||
package,
|
|
||||||
smiles,
|
|
||||||
f"Draft Compound {idx:03d}",
|
|
||||||
"Compound for pagination tests",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class RulePaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|
||||||
"""Rule pagination tests using base class."""
|
|
||||||
|
|
||||||
resource_name = "rule"
|
|
||||||
resource_name_plural = "rules"
|
|
||||||
global_endpoint = "/api/v1/rules/"
|
|
||||||
package_endpoint_template = "/api/v1/package/{uuid}/rule/"
|
|
||||||
total_reviewed = 125
|
|
||||||
total_unreviewed = 35
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_reviewed_resource(cls, package, idx):
|
|
||||||
# Create unique SMIRKS by combining chain length and functional group variations
|
|
||||||
# This ensures each idx gets a truly unique SMIRKS pattern
|
|
||||||
carbon_chain = "C" * (idx + 1) # C, CC, CCC, ... (grows with idx)
|
|
||||||
smirks = f"[{carbon_chain}:1]>>[{carbon_chain}:1]O"
|
|
||||||
return SimpleAmbitRule.create(
|
|
||||||
package,
|
|
||||||
f"Reviewed Rule {idx:03d}",
|
|
||||||
f"Rule {idx} for pagination tests",
|
|
||||||
smirks,
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_unreviewed_resource(cls, package, idx):
|
|
||||||
# Create unique SMIRKS by varying the carbon chain length
|
|
||||||
carbon_chain = "C" * (idx + 1) # C, CC, CCC, ... (grows with idx)
|
|
||||||
smirks = f"[{carbon_chain}:1]>>[{carbon_chain}:1]N"
|
|
||||||
return SimpleAmbitRule.create(
|
|
||||||
package,
|
|
||||||
f"Draft Rule {idx:03d}",
|
|
||||||
f"Rule {idx} for pagination tests",
|
|
||||||
smirks,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class ReactionPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|
||||||
"""Reaction pagination tests using base class."""
|
|
||||||
|
|
||||||
resource_name = "reaction"
|
|
||||||
resource_name_plural = "reactions"
|
|
||||||
global_endpoint = "/api/v1/reactions/"
|
|
||||||
package_endpoint_template = "/api/v1/package/{uuid}/reaction/"
|
|
||||||
total_reviewed = 125
|
|
||||||
total_unreviewed = 35
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_reviewed_resource(cls, package, idx):
|
|
||||||
# Generate unique SMILES with growing chain lengths to avoid duplicates
|
|
||||||
# Each idx gets a unique chain length
|
|
||||||
educt_smiles = "C" * (idx + 1) # C, CC, CCC, ... (grows with idx)
|
|
||||||
product_smiles = educt_smiles + "O"
|
|
||||||
return Reaction.create(
|
|
||||||
package=package,
|
|
||||||
name=f"Reviewed Reaction {idx:03d}",
|
|
||||||
description="Reaction for pagination tests",
|
|
||||||
educts=[educt_smiles],
|
|
||||||
products=[product_smiles],
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_unreviewed_resource(cls, package, idx):
|
|
||||||
# Generate unique SMILES with growing chain lengths to avoid duplicates
|
|
||||||
# Each idx gets a unique chain length
|
|
||||||
educt_smiles = "C" * (idx + 1) # C, CC, CCC, ... (grows with idx)
|
|
||||||
product_smiles = educt_smiles + "N"
|
|
||||||
return Reaction.create(
|
|
||||||
package=package,
|
|
||||||
name=f"Draft Reaction {idx:03d}",
|
|
||||||
description="Reaction for pagination tests",
|
|
||||||
educts=[educt_smiles],
|
|
||||||
products=[product_smiles],
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class PathwayPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|
||||||
"""Pathway pagination tests using base class."""
|
|
||||||
|
|
||||||
resource_name = "pathway"
|
|
||||||
resource_name_plural = "pathways"
|
|
||||||
global_endpoint = "/api/v1/pathways/"
|
|
||||||
package_endpoint_template = "/api/v1/package/{uuid}/pathway/"
|
|
||||||
total_reviewed = 125
|
|
||||||
total_unreviewed = 35
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_reviewed_resource(cls, package, idx):
|
|
||||||
return Pathway.objects.create(
|
|
||||||
package=package,
|
|
||||||
name=f"Reviewed Pathway {idx:03d}",
|
|
||||||
description="Pathway for pagination tests",
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_unreviewed_resource(cls, package, idx):
|
|
||||||
return Pathway.objects.create(
|
|
||||||
package=package,
|
|
||||||
name=f"Draft Pathway {idx:03d}",
|
|
||||||
description="Pathway for pagination tests",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class ModelPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|
||||||
"""Model pagination tests using base class."""
|
|
||||||
|
|
||||||
resource_name = "model"
|
|
||||||
resource_name_plural = "models"
|
|
||||||
global_endpoint = "/api/v1/models/"
|
|
||||||
package_endpoint_template = "/api/v1/package/{uuid}/model/"
|
|
||||||
total_reviewed = 125
|
|
||||||
total_unreviewed = 35
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_reviewed_resource(cls, package, idx):
|
|
||||||
return EPModel.objects.create(
|
|
||||||
package=package,
|
|
||||||
name=f"Reviewed Model {idx:03d}",
|
|
||||||
description="Model for pagination tests",
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_unreviewed_resource(cls, package, idx):
|
|
||||||
return EPModel.objects.create(
|
|
||||||
package=package,
|
|
||||||
name=f"Draft Model {idx:03d}",
|
|
||||||
description="Model for pagination tests",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@tag("api", "end2end")
|
|
||||||
class ScenarioPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|
||||||
"""Scenario pagination tests using base class."""
|
|
||||||
|
|
||||||
resource_name = "scenario"
|
|
||||||
resource_name_plural = "scenarios"
|
|
||||||
global_endpoint = "/api/v1/scenarios/"
|
|
||||||
package_endpoint_template = "/api/v1/package/{uuid}/scenario/"
|
|
||||||
total_reviewed = 125
|
|
||||||
total_unreviewed = 35
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_reviewed_resource(cls, package, idx):
|
|
||||||
return Scenario.create(
|
|
||||||
package,
|
|
||||||
f"Reviewed Scenario {idx:03d}",
|
|
||||||
"Scenario for pagination tests",
|
|
||||||
"2025-01-01",
|
|
||||||
"lab",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def create_unreviewed_resource(cls, package, idx):
|
|
||||||
return Scenario.create(
|
|
||||||
package,
|
|
||||||
f"Draft Scenario {idx:03d}",
|
|
||||||
"Scenario for pagination tests",
|
|
||||||
"2025-01-01",
|
|
||||||
"field",
|
|
||||||
[],
|
|
||||||
)
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
from ninja.security import HttpBearer
|
|
||||||
from ninja.errors import HttpError
|
|
||||||
|
|
||||||
|
|
||||||
class BearerTokenAuth(HttpBearer):
|
|
||||||
def authenticate(self, request, token):
|
|
||||||
# FIXME: placeholder; implement it in O(1) time
|
|
||||||
raise HttpError(401, "Invalid or expired token")
|
|
||||||
@ -1,95 +0,0 @@
|
|||||||
from django.db.models import Model
|
|
||||||
from epdb.logic import PackageManager
|
|
||||||
from epdb.models import CompoundStructure, User, Package, Compound
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from .errors import EPAPINotFoundError, EPAPIPermissionDeniedError
|
|
||||||
|
|
||||||
|
|
||||||
def get_compound_or_error(user, compound_uuid: UUID):
|
|
||||||
"""
|
|
||||||
Get compound by UUID with permission check.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
compound = Compound.objects.get(uuid=compound_uuid)
|
|
||||||
package = compound.package
|
|
||||||
except Compound.DoesNotExist:
|
|
||||||
raise EPAPINotFoundError(f"Compound with UUID {compound_uuid} not found")
|
|
||||||
|
|
||||||
# FIXME: optimize package manager to exclusively work with UUIDs
|
|
||||||
if not user or user.is_anonymous or not PackageManager.readable(user, package):
|
|
||||||
raise EPAPIPermissionDeniedError("Insufficient permissions to access this compound.")
|
|
||||||
|
|
||||||
return compound
|
|
||||||
|
|
||||||
|
|
||||||
def get_package_or_error(user, package_uuid: UUID):
|
|
||||||
"""
|
|
||||||
Get package by UUID with permission check.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# FIXME: update package manager with custom exceptions to avoid manual checks here
|
|
||||||
try:
|
|
||||||
package = Package.objects.get(uuid=package_uuid)
|
|
||||||
except Package.DoesNotExist:
|
|
||||||
raise EPAPINotFoundError(f"Package with UUID {package_uuid} not found")
|
|
||||||
|
|
||||||
# FIXME: optimize package manager to exclusively work with UUIDs
|
|
||||||
if not user or user.is_anonymous or not PackageManager.readable(user, package):
|
|
||||||
raise EPAPIPermissionDeniedError("Insufficient permissions to access this package.")
|
|
||||||
|
|
||||||
return package
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_packages_qs(user: User | None):
|
|
||||||
"""Get all packages readable by the user."""
|
|
||||||
if not user or user.is_anonymous:
|
|
||||||
return PackageManager.get_reviewed_packages()
|
|
||||||
return PackageManager.get_all_readable_packages(user, include_reviewed=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_entities_qs(model_class: Model, user: User | None):
|
|
||||||
"""Build queryset for reviewed package entities."""
|
|
||||||
|
|
||||||
if not user or user.is_anonymous:
|
|
||||||
return model_class.objects.filter(package__reviewed=True).select_related("package")
|
|
||||||
|
|
||||||
qs = model_class.objects.filter(
|
|
||||||
package__in=PackageManager.get_all_readable_packages(user, include_reviewed=True)
|
|
||||||
).select_related("package")
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
def get_package_scoped_entities_qs(
|
|
||||||
model_class: Model, package_uuid: UUID, user: User | None = None
|
|
||||||
):
|
|
||||||
"""Build queryset for specific package entities."""
|
|
||||||
package = get_package_or_error(user, package_uuid)
|
|
||||||
qs = model_class.objects.filter(package=package).select_related("package")
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
def get_user_structures_qs(user: User | None):
|
|
||||||
"""Build queryset for structures accessible to the user (via compound->package)."""
|
|
||||||
|
|
||||||
if not user or user.is_anonymous:
|
|
||||||
return CompoundStructure.objects.filter(compound__package__reviewed=True).select_related(
|
|
||||||
"compound__package"
|
|
||||||
)
|
|
||||||
|
|
||||||
qs = CompoundStructure.objects.filter(
|
|
||||||
compound__package__in=PackageManager.get_all_readable_packages(user, include_reviewed=True)
|
|
||||||
).select_related("compound__package")
|
|
||||||
return qs
|
|
||||||
|
|
||||||
|
|
||||||
def get_package_compound_scoped_structure_qs(
|
|
||||||
package_uuid: UUID, compound_uuid: UUID, user: User | None = None
|
|
||||||
):
|
|
||||||
"""Build queryset for specific package compound structures."""
|
|
||||||
|
|
||||||
get_package_or_error(user, package_uuid)
|
|
||||||
compound = get_compound_or_error(user, compound_uuid)
|
|
||||||
|
|
||||||
qs = CompoundStructure.objects.filter(compound=compound).select_related("compound__package")
|
|
||||||
return qs
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
from django.conf import settings as s
|
|
||||||
from ninja import Router
|
|
||||||
from ninja_extra.pagination import paginate
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from epdb.models import Compound
|
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
|
||||||
from ..schemas import CompoundOutSchema, ReviewStatusFilter
|
|
||||||
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/compounds/", response=EnhancedPageNumberPagination.Output[CompoundOutSchema])
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_all_compounds(request):
|
|
||||||
"""
|
|
||||||
List all compounds from reviewed packages.
|
|
||||||
"""
|
|
||||||
return get_user_entities_qs(Compound, request.user).order_by("name").all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/package/{uuid:package_uuid}/compound/",
|
|
||||||
response=EnhancedPageNumberPagination.Output[CompoundOutSchema],
|
|
||||||
)
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_package_compounds(request, package_uuid: UUID):
|
|
||||||
"""
|
|
||||||
List all compounds for a specific package.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return get_package_scoped_entities_qs(Compound, package_uuid, user).order_by("name").all()
|
|
||||||
@ -1,41 +0,0 @@
|
|||||||
from django.conf import settings as s
|
|
||||||
from ninja import Router
|
|
||||||
from ninja_extra.pagination import paginate
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from epdb.models import EPModel
|
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
|
||||||
from ..schemas import ModelOutSchema, ReviewStatusFilter
|
|
||||||
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/models/", response=EnhancedPageNumberPagination.Output[ModelOutSchema])
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_all_models(request):
|
|
||||||
"""
|
|
||||||
List all models from reviewed packages.
|
|
||||||
"""
|
|
||||||
return get_user_entities_qs(EPModel, request.user).order_by("name").all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/package/{uuid:package_uuid}/model/",
|
|
||||||
response=EnhancedPageNumberPagination.Output[ModelOutSchema],
|
|
||||||
)
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_package_models(request, package_uuid: UUID):
|
|
||||||
"""
|
|
||||||
List all models for a specific package.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return get_package_scoped_entities_qs(EPModel, package_uuid, user).order_by("name").all()
|
|
||||||
@ -1,27 +0,0 @@
|
|||||||
from django.conf import settings as s
|
|
||||||
from ninja import Router
|
|
||||||
from ninja_extra.pagination import paginate
|
|
||||||
import logging
|
|
||||||
|
|
||||||
from ..dal import get_user_packages_qs
|
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
|
||||||
from ..schemas import PackageOutSchema, SelfReviewStatusFilter
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/packages/", response=EnhancedPageNumberPagination.Output[PackageOutSchema], auth=None)
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=SelfReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_all_packages(request):
|
|
||||||
"""
|
|
||||||
List packages accessible to the user.
|
|
||||||
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
qs = get_user_packages_qs(user)
|
|
||||||
return qs.order_by("name").all()
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
from django.conf import settings as s
|
|
||||||
from ninja import Router
|
|
||||||
from ninja_extra.pagination import paginate
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from epdb.models import Pathway
|
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
|
||||||
from ..schemas import PathwayOutSchema, ReviewStatusFilter
|
|
||||||
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/pathways/", response=EnhancedPageNumberPagination.Output[PathwayOutSchema])
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_all_pathways(request):
|
|
||||||
"""
|
|
||||||
List all pathways from reviewed packages.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return get_user_entities_qs(Pathway, user).order_by("name").all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/package/{uuid:package_uuid}/pathway/",
|
|
||||||
response=EnhancedPageNumberPagination.Output[PathwayOutSchema],
|
|
||||||
)
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_package_pathways(request, package_uuid: UUID):
|
|
||||||
"""
|
|
||||||
List all pathways for a specific package.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return get_package_scoped_entities_qs(Pathway, package_uuid, user).order_by("name").all()
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
from django.conf import settings as s
|
|
||||||
from ninja import Router
|
|
||||||
from ninja_extra.pagination import paginate
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from epdb.models import Reaction
|
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
|
||||||
from ..schemas import ReactionOutSchema, ReviewStatusFilter
|
|
||||||
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/reactions/", response=EnhancedPageNumberPagination.Output[ReactionOutSchema])
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_all_reactions(request):
|
|
||||||
"""
|
|
||||||
List all reactions from reviewed packages.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return get_user_entities_qs(Reaction, user).order_by("name").all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/package/{uuid:package_uuid}/reaction/",
|
|
||||||
response=EnhancedPageNumberPagination.Output[ReactionOutSchema],
|
|
||||||
)
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_package_reactions(request, package_uuid: UUID):
|
|
||||||
"""
|
|
||||||
List all reactions for a specific package.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return get_package_scoped_entities_qs(Reaction, package_uuid, user).order_by("name").all()
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
from django.conf import settings as s
|
|
||||||
from ninja import Router
|
|
||||||
from ninja_extra.pagination import paginate
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from epdb.models import Rule
|
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
|
||||||
from ..schemas import ReviewStatusFilter, RuleOutSchema
|
|
||||||
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/rules/", response=EnhancedPageNumberPagination.Output[RuleOutSchema])
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_all_rules(request):
|
|
||||||
"""
|
|
||||||
List all rules from reviewed packages.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return get_user_entities_qs(Rule, user).order_by("name").all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/package/{uuid:package_uuid}/rule/",
|
|
||||||
response=EnhancedPageNumberPagination.Output[RuleOutSchema],
|
|
||||||
)
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_package_rules(request, package_uuid: UUID):
|
|
||||||
"""
|
|
||||||
List all rules for a specific package.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return get_package_scoped_entities_qs(Rule, package_uuid, user).order_by("name").all()
|
|
||||||
@ -1,36 +0,0 @@
|
|||||||
from django.conf import settings as s
|
|
||||||
from ninja import Router
|
|
||||||
from ninja_extra.pagination import paginate
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from epdb.models import Scenario
|
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
|
||||||
from ..schemas import ReviewStatusFilter, ScenarioOutSchema
|
|
||||||
from ..dal import get_user_entities_qs, get_package_scoped_entities_qs
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/scenarios/", response=EnhancedPageNumberPagination.Output[ScenarioOutSchema])
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_all_scenarios(request):
|
|
||||||
user = request.user
|
|
||||||
return get_user_entities_qs(Scenario, user).order_by("name").all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/package/{uuid:package_uuid}/scenario/",
|
|
||||||
response=EnhancedPageNumberPagination.Output[ScenarioOutSchema],
|
|
||||||
)
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=ReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_package_scenarios(request, package_uuid: UUID):
|
|
||||||
user = request.user
|
|
||||||
return get_package_scoped_entities_qs(Scenario, package_uuid, user).order_by("name").all()
|
|
||||||
@ -1,50 +0,0 @@
|
|||||||
from django.conf import settings as s
|
|
||||||
from ninja import Router
|
|
||||||
from ninja_extra.pagination import paginate
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
from ..pagination import EnhancedPageNumberPagination
|
|
||||||
from ..schemas import CompoundStructureOutSchema, StructureReviewStatusFilter
|
|
||||||
from ..dal import (
|
|
||||||
get_user_structures_qs,
|
|
||||||
get_package_compound_scoped_structure_qs,
|
|
||||||
)
|
|
||||||
|
|
||||||
router = Router()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/structures/", response=EnhancedPageNumberPagination.Output[CompoundStructureOutSchema]
|
|
||||||
)
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=StructureReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_all_structures(request):
|
|
||||||
"""
|
|
||||||
List all structures from all packages.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return get_user_structures_qs(user).order_by("name").all()
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/",
|
|
||||||
response=EnhancedPageNumberPagination.Output[CompoundStructureOutSchema],
|
|
||||||
)
|
|
||||||
@paginate(
|
|
||||||
EnhancedPageNumberPagination,
|
|
||||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
|
||||||
filter_schema=StructureReviewStatusFilter,
|
|
||||||
)
|
|
||||||
def list_package_structures(request, package_uuid: UUID, compound_uuid: UUID):
|
|
||||||
"""
|
|
||||||
List all structures for a specific package and compound.
|
|
||||||
"""
|
|
||||||
user = request.user
|
|
||||||
return (
|
|
||||||
get_package_compound_scoped_structure_qs(package_uuid, compound_uuid, user)
|
|
||||||
.order_by("name")
|
|
||||||
.all()
|
|
||||||
)
|
|
||||||
@ -1,28 +0,0 @@
|
|||||||
from ninja.errors import HttpError
|
|
||||||
|
|
||||||
|
|
||||||
class EPAPIError(HttpError):
|
|
||||||
status_code: int = 500
|
|
||||||
|
|
||||||
def __init__(self, message: str) -> None:
|
|
||||||
super().__init__(status_code=self.status_code, message=message)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_exception(cls, exc: Exception):
|
|
||||||
return cls(message=str(exc))
|
|
||||||
|
|
||||||
|
|
||||||
class EPAPIUnauthorizedError(EPAPIError):
|
|
||||||
status_code = 401
|
|
||||||
|
|
||||||
|
|
||||||
class EPAPIPermissionDeniedError(EPAPIError):
|
|
||||||
status_code = 403
|
|
||||||
|
|
||||||
|
|
||||||
class EPAPINotFoundError(EPAPIError):
|
|
||||||
status_code = 404
|
|
||||||
|
|
||||||
|
|
||||||
class EPAPIValidationError(EPAPIError):
|
|
||||||
status_code = 422
|
|
||||||
@ -1,60 +0,0 @@
|
|||||||
import math
|
|
||||||
from typing import Any, Generic, List, TypeVar
|
|
||||||
|
|
||||||
from django.db.models import QuerySet
|
|
||||||
from ninja import Schema
|
|
||||||
from ninja.pagination import PageNumberPagination
|
|
||||||
|
|
||||||
T = TypeVar("T")
|
|
||||||
|
|
||||||
|
|
||||||
class EnhancedPageNumberPagination(PageNumberPagination):
|
|
||||||
class Output(Schema, Generic[T]):
|
|
||||||
items: List[T]
|
|
||||||
page: int
|
|
||||||
page_size: int
|
|
||||||
total_items: int
|
|
||||||
total_pages: int
|
|
||||||
|
|
||||||
def paginate_queryset(
|
|
||||||
self,
|
|
||||||
queryset: QuerySet,
|
|
||||||
pagination: PageNumberPagination.Input,
|
|
||||||
**params: Any,
|
|
||||||
) -> Any:
|
|
||||||
page_size = self._get_page_size(pagination.page_size)
|
|
||||||
offset = (pagination.page - 1) * page_size
|
|
||||||
total_items = self._items_count(queryset)
|
|
||||||
total_pages = math.ceil(total_items / page_size) if page_size > 0 else 0
|
|
||||||
|
|
||||||
return {
|
|
||||||
"items": queryset[offset : offset + page_size],
|
|
||||||
"page": pagination.page,
|
|
||||||
"page_size": page_size,
|
|
||||||
"total_items": total_items,
|
|
||||||
"total_pages": total_pages,
|
|
||||||
}
|
|
||||||
|
|
||||||
async def apaginate_queryset(
|
|
||||||
self,
|
|
||||||
queryset: QuerySet,
|
|
||||||
pagination: PageNumberPagination.Input,
|
|
||||||
**params: Any,
|
|
||||||
) -> Any:
|
|
||||||
page_size = self._get_page_size(pagination.page_size)
|
|
||||||
offset = (pagination.page - 1) * page_size
|
|
||||||
total_items = await self._aitems_count(queryset)
|
|
||||||
total_pages = math.ceil(total_items / page_size) if page_size > 0 else 0
|
|
||||||
|
|
||||||
if isinstance(queryset, QuerySet):
|
|
||||||
items = [obj async for obj in queryset[offset : offset + page_size]]
|
|
||||||
else:
|
|
||||||
items = queryset[offset : offset + page_size]
|
|
||||||
|
|
||||||
return {
|
|
||||||
"items": items,
|
|
||||||
"page": pagination.page,
|
|
||||||
"page_size": page_size,
|
|
||||||
"total_items": total_items,
|
|
||||||
"total_pages": total_pages,
|
|
||||||
}
|
|
||||||
@ -1,22 +0,0 @@
|
|||||||
from ninja import Router
|
|
||||||
from ninja.security import SessionAuth
|
|
||||||
from .auth import BearerTokenAuth
|
|
||||||
from .endpoints import packages, scenarios, compounds, rules, reactions, pathways, models, structure
|
|
||||||
|
|
||||||
# Main router with authentication
|
|
||||||
router = Router(
|
|
||||||
auth=[
|
|
||||||
SessionAuth(),
|
|
||||||
BearerTokenAuth(),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Include all endpoint routers
|
|
||||||
router.add_router("", packages.router)
|
|
||||||
router.add_router("", scenarios.router)
|
|
||||||
router.add_router("", compounds.router)
|
|
||||||
router.add_router("", rules.router)
|
|
||||||
router.add_router("", reactions.router)
|
|
||||||
router.add_router("", pathways.router)
|
|
||||||
router.add_router("", models.router)
|
|
||||||
router.add_router("", structure.router)
|
|
||||||
@ -1,104 +0,0 @@
|
|||||||
from ninja import FilterSchema, FilterLookup, Schema
|
|
||||||
from typing import Annotated, Optional
|
|
||||||
from uuid import UUID
|
|
||||||
|
|
||||||
|
|
||||||
# Filter schema for query parameters
|
|
||||||
class ReviewStatusFilter(FilterSchema):
|
|
||||||
"""Filter schema for review_status query parameter."""
|
|
||||||
|
|
||||||
review_status: Annotated[Optional[bool], FilterLookup("package__reviewed")] = None
|
|
||||||
|
|
||||||
|
|
||||||
class SelfReviewStatusFilter(FilterSchema):
|
|
||||||
"""Filter schema for review_status query parameter on self-reviewed entities."""
|
|
||||||
|
|
||||||
review_status: Annotated[Optional[bool], FilterLookup("reviewed")] = None
|
|
||||||
|
|
||||||
|
|
||||||
class StructureReviewStatusFilter(FilterSchema):
|
|
||||||
"""Filter schema for review_status on structures (via compound->package)."""
|
|
||||||
|
|
||||||
review_status: Annotated[Optional[bool], FilterLookup("compound__package__reviewed")] = None
|
|
||||||
|
|
||||||
|
|
||||||
# Base schema for all package-scoped entities
|
|
||||||
class PackageEntityOutSchema(Schema):
|
|
||||||
"""Base schema for entities belonging to a package."""
|
|
||||||
|
|
||||||
uuid: UUID
|
|
||||||
url: str = ""
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
review_status: str = ""
|
|
||||||
package: str = ""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_url(obj):
|
|
||||||
return obj.url
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_package(obj):
|
|
||||||
return obj.package.url
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_review_status(obj):
|
|
||||||
return "reviewed" if obj.package.reviewed else "unreviewed"
|
|
||||||
|
|
||||||
|
|
||||||
# All package-scoped entities inherit from base
|
|
||||||
class ScenarioOutSchema(PackageEntityOutSchema):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CompoundOutSchema(PackageEntityOutSchema):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class RuleOutSchema(PackageEntityOutSchema):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ReactionOutSchema(PackageEntityOutSchema):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class PathwayOutSchema(PackageEntityOutSchema):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class ModelOutSchema(PackageEntityOutSchema):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class CompoundStructureOutSchema(PackageEntityOutSchema):
|
|
||||||
compound: str = ""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_compound(obj):
|
|
||||||
return obj.compound.url
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_package(obj):
|
|
||||||
return obj.compound.package.url
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_review_status(obj):
|
|
||||||
return "reviewed" if obj.compound.package.reviewed else "unreviewed"
|
|
||||||
|
|
||||||
|
|
||||||
# Package is special (no package FK)
|
|
||||||
class PackageOutSchema(Schema):
|
|
||||||
uuid: UUID
|
|
||||||
url: str = ""
|
|
||||||
name: str
|
|
||||||
description: str
|
|
||||||
review_status: str = ""
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_url(obj):
|
|
||||||
return obj.url
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_review_status(obj):
|
|
||||||
return "reviewed" if obj.reviewed else "unreviewed"
|
|
||||||
@ -1,31 +1,29 @@
|
|||||||
from django.conf import settings as s
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Compound,
|
|
||||||
CompoundStructure,
|
|
||||||
Edge,
|
|
||||||
EnviFormer,
|
|
||||||
ExternalDatabase,
|
|
||||||
ExternalIdentifier,
|
|
||||||
Group,
|
|
||||||
GroupPackagePermission,
|
|
||||||
JobLog,
|
|
||||||
License,
|
|
||||||
MLRelativeReasoning,
|
|
||||||
Node,
|
|
||||||
ParallelRule,
|
|
||||||
Pathway,
|
|
||||||
Reaction,
|
|
||||||
Scenario,
|
|
||||||
Setting,
|
|
||||||
SimpleAmbitRule,
|
|
||||||
User,
|
User,
|
||||||
UserPackagePermission,
|
UserPackagePermission,
|
||||||
|
Group,
|
||||||
|
GroupPackagePermission,
|
||||||
|
Package,
|
||||||
|
MLRelativeReasoning,
|
||||||
|
EnviFormer,
|
||||||
|
Compound,
|
||||||
|
CompoundStructure,
|
||||||
|
SimpleAmbitRule,
|
||||||
|
ParallelRule,
|
||||||
|
Reaction,
|
||||||
|
Pathway,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
Scenario,
|
||||||
|
Setting,
|
||||||
|
ExternalDatabase,
|
||||||
|
ExternalIdentifier,
|
||||||
|
JobLog,
|
||||||
|
License,
|
||||||
)
|
)
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(admin.ModelAdmin):
|
class UserAdmin(admin.ModelAdmin):
|
||||||
list_display = ["username", "email", "is_active"]
|
list_display = ["username", "email", "is_active"]
|
||||||
|
|||||||
@ -1,9 +1,4 @@
|
|||||||
import logging
|
|
||||||
|
|
||||||
from django.apps import AppConfig
|
from django.apps import AppConfig
|
||||||
from django.conf import settings
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class EPDBConfig(AppConfig):
|
class EPDBConfig(AppConfig):
|
||||||
@ -12,6 +7,3 @@ class EPDBConfig(AppConfig):
|
|||||||
|
|
||||||
def ready(self):
|
def ready(self):
|
||||||
import epdb.signals # noqa: F401
|
import epdb.signals # noqa: F401
|
||||||
|
|
||||||
model_name = getattr(settings, "EPDB_PACKAGE_MODEL", "epdb.Package")
|
|
||||||
logger.info(f"Using Package model: {model_name}")
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ Context processors automatically make variables available to all templates.
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from .logic import PackageManager
|
from .logic import PackageManager
|
||||||
from django.conf import settings as s
|
from .models import Package
|
||||||
|
|
||||||
|
|
||||||
def package_context(request):
|
def package_context(request):
|
||||||
@ -20,7 +20,7 @@ def package_context(request):
|
|||||||
|
|
||||||
reviewed_package_qs = PackageManager.get_reviewed_packages()
|
reviewed_package_qs = PackageManager.get_reviewed_packages()
|
||||||
|
|
||||||
unreviewed_package_qs = s.GET_PACKAGE_MODEL().objects.none()
|
unreviewed_package_qs = Package.objects.none()
|
||||||
|
|
||||||
# Only get user-specific packages if user is authenticated
|
# Only get user-specific packages if user is authenticated
|
||||||
if current_user.is_authenticated:
|
if current_user.is_authenticated:
|
||||||
|
|||||||
@ -1,35 +1,27 @@
|
|||||||
from typing import Any, Dict, List, Optional
|
from typing import List, Dict, Optional, Any
|
||||||
|
|
||||||
import nh3
|
|
||||||
from django.conf import settings as s
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.http import HttpResponse
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
from ninja import Field, Form, Router, Schema, Query
|
from ninja import Router, Schema, Field, Form
|
||||||
from ninja.security import SessionAuth
|
|
||||||
|
|
||||||
from utilities.chem import FormatConverter
|
from utilities.chem import FormatConverter
|
||||||
from utilities.misc import PackageExporter
|
from .logic import PackageManager, UserManager, SettingManager
|
||||||
|
|
||||||
from .logic import GroupManager, PackageManager, SettingManager, UserManager, SearchManager
|
|
||||||
from .models import (
|
from .models import (
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
Package,
|
||||||
EPModel,
|
|
||||||
Node,
|
|
||||||
Pathway,
|
|
||||||
Reaction,
|
|
||||||
Rule,
|
|
||||||
Scenario,
|
|
||||||
SimpleAmbitRule,
|
|
||||||
User,
|
User,
|
||||||
UserPackagePermission,
|
UserPackagePermission,
|
||||||
ParallelRule,
|
Rule,
|
||||||
|
Reaction,
|
||||||
|
Scenario,
|
||||||
|
Pathway,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
SimpleAmbitRule,
|
||||||
)
|
)
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@ -37,7 +29,8 @@ def _anonymous_or_real(request):
|
|||||||
return get_user_model().objects.get(username="anonymous")
|
return get_user_model().objects.get(username="anonymous")
|
||||||
|
|
||||||
|
|
||||||
router = Router(auth=SessionAuth(csrf=False))
|
# router = Router(auth=SessionAuth())
|
||||||
|
router = Router()
|
||||||
|
|
||||||
|
|
||||||
class Error(Schema):
|
class Error(Schema):
|
||||||
@ -125,16 +118,13 @@ class SimpleEdge(SimpleObject):
|
|||||||
identifier: str = "edge"
|
identifier: str = "edge"
|
||||||
|
|
||||||
|
|
||||||
class SimpleModel(SimpleObject):
|
|
||||||
identifier: str = "relative-reasoning"
|
|
||||||
|
|
||||||
|
|
||||||
################
|
################
|
||||||
# Login/Logout #
|
# Login/Logout #
|
||||||
################
|
################
|
||||||
@router.post("/", response={200: SimpleUser, 403: Error}, auth=None)
|
@router.post("/", response={200: SimpleUser, 403: Error})
|
||||||
def login(request, loginusername: Form[str], loginpassword: Form[str]):
|
def login(request, loginusername: Form[str], loginpassword: Form[str]):
|
||||||
from django.contrib.auth import authenticate, login
|
from django.contrib.auth import authenticate
|
||||||
|
from django.contrib.auth import login
|
||||||
|
|
||||||
email = User.objects.get(username=loginusername).email
|
email = User.objects.get(username=loginusername).email
|
||||||
user = authenticate(username=email, password=loginpassword)
|
user = authenticate(username=email, password=loginpassword)
|
||||||
@ -177,13 +167,9 @@ class UserSchema(Schema):
|
|||||||
return SettingManager.get_all_settings(obj)
|
return SettingManager.get_all_settings(obj)
|
||||||
|
|
||||||
|
|
||||||
class Me(Schema):
|
|
||||||
whoami: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/user", response={200: UserWrapper, 403: Error})
|
@router.get("/user", response={200: UserWrapper, 403: Error})
|
||||||
def get_users(request, me: Query[Me]):
|
def get_users(request, whoami: str = None):
|
||||||
if me.whoami:
|
if whoami:
|
||||||
return {"user": [request.user]}
|
return {"user": [request.user]}
|
||||||
else:
|
else:
|
||||||
return {"user": User.objects.all()}
|
return {"user": User.objects.all()}
|
||||||
@ -200,61 +186,6 @@ def get_user(request, user_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Search(Schema):
|
|
||||||
packages: List[str] = Field(alias="packages[]")
|
|
||||||
search: str
|
|
||||||
method: str
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/search", response={200: Any, 403: Error})
|
|
||||||
def search(request, search: Query[Search]):
|
|
||||||
try:
|
|
||||||
packs = []
|
|
||||||
for package in search.packages:
|
|
||||||
packs.append(PackageManager.get_package_by_url(request.user, package))
|
|
||||||
|
|
||||||
method = None
|
|
||||||
|
|
||||||
if search.method == "text":
|
|
||||||
method = "text"
|
|
||||||
elif search.method == "inchikey":
|
|
||||||
method = "inchikey"
|
|
||||||
elif search.method == "defaultSmiles":
|
|
||||||
method = "default"
|
|
||||||
elif search.method == "canonicalSmiles":
|
|
||||||
method = "canonical"
|
|
||||||
elif search.method == "exactSmiles":
|
|
||||||
method = "exact"
|
|
||||||
|
|
||||||
if method is None:
|
|
||||||
raise ValueError(f"Search method {search.method} is not supported!")
|
|
||||||
|
|
||||||
search_res = SearchManager.search(packs, search.search, method)
|
|
||||||
res = {}
|
|
||||||
if "Compounds" in search_res:
|
|
||||||
res["compound"] = search_res["Compounds"]
|
|
||||||
|
|
||||||
if "Compound Structures" in search_res:
|
|
||||||
res["structure"] = search_res["Compound Structures"]
|
|
||||||
|
|
||||||
if "Reaction" in search_res:
|
|
||||||
res["reaction"] = search_res["Reaction"]
|
|
||||||
|
|
||||||
if "Pathway" in search_res:
|
|
||||||
res["pathway"] = search_res["Pathway"]
|
|
||||||
|
|
||||||
if "Rules" in search_res:
|
|
||||||
res["rule"] = search_res["Rules"]
|
|
||||||
|
|
||||||
for key in res:
|
|
||||||
for v in res[key]:
|
|
||||||
v["id"] = v["url"].replace("simple-ambit-rule", "simple-rule")
|
|
||||||
|
|
||||||
return res
|
|
||||||
except ValueError as e:
|
|
||||||
return 403, {"message": f"Search failed due to {e}"}
|
|
||||||
|
|
||||||
|
|
||||||
###########
|
###########
|
||||||
# Package #
|
# Package #
|
||||||
###########
|
###########
|
||||||
@ -320,110 +251,67 @@ def get_packages(request):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class GetPackage(Schema):
|
@router.get("/package/{uuid:package_uuid}", response={200: PackageSchema, 403: Error})
|
||||||
exportAsJson: str | None = None
|
def get_package(request, package_uuid):
|
||||||
|
|
||||||
|
|
||||||
@router.get("/package/{uuid:package_uuid}", response={200: PackageSchema | Any, 403: Error})
|
|
||||||
def get_package(request, package_uuid, gp: Query[GetPackage]):
|
|
||||||
try:
|
try:
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
return PackageManager.get_package_by_id(request.user, package_uuid)
|
||||||
|
|
||||||
if gp.exportAsJson and gp.exportAsJson.strip() == "true":
|
|
||||||
return PackageExporter(p).do_export()
|
|
||||||
|
|
||||||
return p
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return 403, {
|
return 403, {
|
||||||
"message": f"Getting Package with id {package_uuid} failed due to insufficient rights!"
|
"message": f"Getting Package with id {package_uuid} failed due to insufficient rights!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CreatePackage(Schema):
|
|
||||||
packageName: str
|
|
||||||
packageDescription: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package")
|
@router.post("/package")
|
||||||
def create_packages(
|
def create_packages(
|
||||||
request,
|
request, packageName: Form[str], packageDescription: Optional[str] = Form(None)
|
||||||
p: Form[CreatePackage],
|
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
if p.packageName.strip() == "":
|
if packageName.strip() == "":
|
||||||
raise ValueError("Package name cannot be empty!")
|
raise ValueError("Package name cannot be empty!")
|
||||||
|
|
||||||
new_pacakge = PackageManager.create_package(
|
new_pacakge = PackageManager.create_package(request.user, packageName, packageDescription)
|
||||||
request.user, p.packageName, p.packageDescription
|
|
||||||
)
|
|
||||||
return redirect(new_pacakge.url)
|
return redirect(new_pacakge.url)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return 400, {"message": str(e)}
|
return 400, {"message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
class UpdatePackage(Schema):
|
|
||||||
packageDescription: str | None = None
|
|
||||||
hiddenMethod: str | None = None
|
|
||||||
permissions: str | None = None
|
|
||||||
ppsURI: str | None = None
|
|
||||||
read: str | None = None
|
|
||||||
write: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package/{uuid:package_uuid}", response={200: PackageSchema | Any, 400: Error})
|
@router.post("/package/{uuid:package_uuid}", response={200: PackageSchema | Any, 400: Error})
|
||||||
def update_package(request, package_uuid, pack: Form[UpdatePackage]):
|
def update_package(
|
||||||
|
request,
|
||||||
|
package_uuid,
|
||||||
|
packageDescription: Optional[str] = Form(None),
|
||||||
|
hiddenMethod: Optional[str] = Form(None),
|
||||||
|
exportAsJson: Optional[str] = Form(None),
|
||||||
|
permissions: Optional[str] = Form(None),
|
||||||
|
ppsURI: Optional[str] = Form(None),
|
||||||
|
read: Optional[str] = Form(None),
|
||||||
|
write: Optional[str] = Form(None),
|
||||||
|
):
|
||||||
try:
|
try:
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
||||||
|
|
||||||
if pack.hiddenMethod:
|
if hiddenMethod:
|
||||||
if pack.hiddenMethod == "DELETE":
|
if hiddenMethod == "DELETE":
|
||||||
p.delete()
|
p.delete()
|
||||||
|
|
||||||
elif pack.packageDescription is not None:
|
elif packageDescription and packageDescription.strip() != "":
|
||||||
description = nh3.clean(pack.packageDescription, tags=s.ALLOWED_HTML_TAGS).strip()
|
p.description = packageDescription
|
||||||
|
|
||||||
if description:
|
|
||||||
p.description = description
|
|
||||||
p.save()
|
p.save()
|
||||||
return HttpResponse(status=200)
|
return
|
||||||
else:
|
elif exportAsJson == "true":
|
||||||
raise ValueError("Package description cannot be empty!")
|
pack_json = PackageManager.export_package(
|
||||||
elif all([pack.permissions, pack.ppsURI, pack.read]):
|
p, include_models=False, include_external_identifiers=False
|
||||||
if "group" in pack.ppsURI:
|
)
|
||||||
grantee = GroupManager.get_group_lp(pack.ppsURI)
|
return pack_json
|
||||||
else:
|
elif all([permissions, ppsURI, read]):
|
||||||
grantee = UserManager.get_user_lp(pack.ppsURI)
|
PackageManager.update_permissions
|
||||||
|
elif all([permissions, ppsURI, write]):
|
||||||
|
pass
|
||||||
|
|
||||||
PackageManager.grant_read(request.user, p, grantee)
|
|
||||||
return HttpResponse(status=200)
|
|
||||||
elif all([pack.permissions, pack.ppsURI, pack.write]):
|
|
||||||
if "group" in pack.ppsURI:
|
|
||||||
grantee = GroupManager.get_group_lp(pack.ppsURI)
|
|
||||||
else:
|
|
||||||
grantee = UserManager.get_user_lp(pack.ppsURI)
|
|
||||||
|
|
||||||
PackageManager.grant_write(request.user, p, grantee)
|
|
||||||
return HttpResponse(status=200)
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return 400, {"message": str(e)}
|
return 400, {"message": str(e)}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}")
|
|
||||||
def delete_package(request, package_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.administrable(request.user, p):
|
|
||||||
p.delete()
|
|
||||||
return redirect(f"{s.SERVER_URL}/package")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this Package!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Package with id {package_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
################################
|
################################
|
||||||
# Compound / CompoundStructure #
|
# Compound / CompoundStructure #
|
||||||
################################
|
################################
|
||||||
@ -621,83 +509,6 @@ def get_package_compound_structure(request, package_uuid, compound_uuid, structu
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CreateCompound(Schema):
|
|
||||||
compoundSmiles: str
|
|
||||||
compoundName: str | None = None
|
|
||||||
compoundDescription: str | None = None
|
|
||||||
inchi: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package/{uuid:package_uuid}/compound")
|
|
||||||
def create_package_compound(
|
|
||||||
request,
|
|
||||||
package_uuid,
|
|
||||||
c: Form[CreateCompound],
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
# inchi is not used atm
|
|
||||||
c = Compound.create(
|
|
||||||
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
|
|
||||||
)
|
|
||||||
return redirect(c.url)
|
|
||||||
except ValueError as e:
|
|
||||||
return 400, {"message": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}")
|
|
||||||
def delete_compound(request, package_uuid, compound_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
c = Compound.objects.get(package=p, uuid=compound_uuid)
|
|
||||||
c.delete()
|
|
||||||
return redirect(f"{p.url}/compound")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this Compound!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Compound with id {compound_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}"
|
|
||||||
)
|
|
||||||
def delete_compound_structure(request, package_uuid, compound_uuid, structure_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
c = Compound.objects.get(package=p, uuid=compound_uuid)
|
|
||||||
cs = CompoundStructure.objects.get(compound=c, uuid=structure_uuid)
|
|
||||||
|
|
||||||
# Check if we have to delete the compound as no structure is left
|
|
||||||
if len(cs.compound.structures.all()) == 1:
|
|
||||||
# This will delete the structure as well
|
|
||||||
c.delete()
|
|
||||||
return redirect(p.url + "/compound")
|
|
||||||
else:
|
|
||||||
if cs.normalized_structure:
|
|
||||||
c.delete()
|
|
||||||
return redirect(p.url + "/compound")
|
|
||||||
else:
|
|
||||||
if c.default_structure == cs:
|
|
||||||
cs.delete()
|
|
||||||
c.default_structure = c.structures.all().first()
|
|
||||||
return redirect(c.url + "/structure")
|
|
||||||
else:
|
|
||||||
cs.delete()
|
|
||||||
return redirect(c.url + "/structure")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this CompoundStructure!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting CompoundStructure with id {compound_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#########
|
#########
|
||||||
# Rules #
|
# Rules #
|
||||||
#########
|
#########
|
||||||
@ -861,73 +672,6 @@ def _get_package_rule(request, package_uuid, rule_uuid):
|
|||||||
|
|
||||||
|
|
||||||
# POST
|
# POST
|
||||||
class CreateSimpleRule(Schema):
|
|
||||||
smirks: str
|
|
||||||
name: str | None = None
|
|
||||||
description: str | None = None
|
|
||||||
reactantFilterSmarts: str | None = None
|
|
||||||
productFilterSmarts: str | None = None
|
|
||||||
immediate: str | None = None
|
|
||||||
rdkitrule: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package/{uuid:package_uuid}/simple-rule")
|
|
||||||
def create_package_simple_rule(
|
|
||||||
request,
|
|
||||||
package_uuid,
|
|
||||||
r: Form[CreateSimpleRule],
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if r.rdkitrule and r.rdkitrule.strip() == "true":
|
|
||||||
raise ValueError("Not yet implemented!")
|
|
||||||
else:
|
|
||||||
sr = SimpleAmbitRule.create(
|
|
||||||
p, r.name, r.description, r.smirks, r.reactantFilterSmarts, r.productFilterSmarts
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(sr.url)
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
return 400, {"message": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
class CreateParallelRule(Schema):
|
|
||||||
simpleRules: str
|
|
||||||
name: str | None = None
|
|
||||||
description: str | None = None
|
|
||||||
reactantFilterSmarts: str | None = None
|
|
||||||
productFilterSmarts: str | None = None
|
|
||||||
immediate: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package/{uuid:package_uuid}/parallel-rule")
|
|
||||||
def create_package_parallel_rule(
|
|
||||||
request,
|
|
||||||
package_uuid,
|
|
||||||
r: Form[CreateParallelRule],
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
srs = SimpleRule.objects.filter(package=p, url__in=r.simpleRules)
|
|
||||||
|
|
||||||
if srs.count() != len(r.simpleRules):
|
|
||||||
raise ValueError(
|
|
||||||
f"Not all SimpleRules could be found in Package with id {package_uuid}!"
|
|
||||||
)
|
|
||||||
|
|
||||||
sr = ParallelRule.create(
|
|
||||||
p, list(srs), r.name, r.description, r.reactantFilterSmarts, r.productFilterSmarts
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(sr.url)
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
return 400, {"message": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
@router.post(
|
||||||
"/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error}
|
"/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error}
|
||||||
)
|
)
|
||||||
@ -977,41 +721,6 @@ def _post_package_rule(request, package_uuid, rule_uuid, compound: Form[str]):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}")
|
|
||||||
def delete_rule(request, package_uuid, rule_uuid):
|
|
||||||
return _delete_rule(request, package_uuid, rule_uuid)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/package/{uuid:package_uuid}/simple-rule/{uuid:rule_uuid}",
|
|
||||||
)
|
|
||||||
def delete_simple_rule(request, package_uuid, rule_uuid):
|
|
||||||
return _delete_rule(request, package_uuid, rule_uuid)
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete(
|
|
||||||
"/package/{uuid:package_uuid}/parallel-rule/{uuid:rule_uuid}",
|
|
||||||
)
|
|
||||||
def delete_parallel_rule(request, package_uuid, rule_uuid):
|
|
||||||
return _delete_rule(request, package_uuid, rule_uuid)
|
|
||||||
|
|
||||||
|
|
||||||
def _delete_rule(request, package_uuid, rule_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
r = Rule.objects.get(package=p, uuid=rule_uuid)
|
|
||||||
r.delete()
|
|
||||||
return redirect(f"{p.url}/rule")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this Rule!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Rule with id {rule_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
############
|
############
|
||||||
# Reaction #
|
# Reaction #
|
||||||
############
|
############
|
||||||
@ -1100,82 +809,6 @@ def get_package_reaction(request, package_uuid, reaction_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CreateReaction(Schema):
|
|
||||||
reactionName: str | None = None
|
|
||||||
reactionDescription: str | None = None
|
|
||||||
smirks: str | None = None
|
|
||||||
educt: str | None = None
|
|
||||||
product: str | None = None
|
|
||||||
rule: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package/{uuid:package_uuid}/reaction")
|
|
||||||
def create_package_reaction(
|
|
||||||
request,
|
|
||||||
package_uuid,
|
|
||||||
r: Form[CreateReaction],
|
|
||||||
):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if r.smirks is None and (r.educt is None or r.product is None):
|
|
||||||
raise ValueError("Either SMIRKS or educt/product must be provided")
|
|
||||||
|
|
||||||
if r.smirks is not None and (r.educt is not None and r.product is not None):
|
|
||||||
raise ValueError("SMIRKS and educt/product provided!")
|
|
||||||
|
|
||||||
rule = None
|
|
||||||
if r.rule:
|
|
||||||
try:
|
|
||||||
rule = Rule.objects.get(package=p, url=r.rule)
|
|
||||||
except Rule.DoesNotExist:
|
|
||||||
raise ValueError(f"Rule with id {r.rule} does not exist!")
|
|
||||||
|
|
||||||
if r.educt is not None:
|
|
||||||
try:
|
|
||||||
educt_cs = CompoundStructure.objects.get(compound__package=p, url=r.educt)
|
|
||||||
except CompoundStructure.DoesNotExist:
|
|
||||||
raise ValueError(f"Compound with id {r.educt} does not exist!")
|
|
||||||
|
|
||||||
try:
|
|
||||||
product_cs = CompoundStructure.objects.get(compound__package=p, url=r.product)
|
|
||||||
except CompoundStructure.DoesNotExist:
|
|
||||||
raise ValueError(f"Compound with id {r.product} does not exist!")
|
|
||||||
|
|
||||||
new_r = Reaction.create(
|
|
||||||
p, r.reactionName, r.reactionDescription, [educt_cs], [product_cs], rule
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
educts = r.smirks.split(">>")[0].split("\\.")
|
|
||||||
products = r.smirks.split(">>")[1].split("\\.")
|
|
||||||
|
|
||||||
new_r = Reaction.create(
|
|
||||||
p, r.reactionName, r.reactionDescription, educts, products, rule
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(new_r.url)
|
|
||||||
|
|
||||||
except ValueError as e:
|
|
||||||
return 400, {"message": str(e)}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/reaction/{uuid:reaction_uuid}")
|
|
||||||
def delete_reaction(request, package_uuid, reaction_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
r = Reaction.objects.get(package=p, uuid=reaction_uuid)
|
|
||||||
r.delete()
|
|
||||||
return redirect(f"{p.url}/reaction")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this Reaction!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Reaction with id {reaction_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
############
|
############
|
||||||
# Scenario #
|
# Scenario #
|
||||||
############
|
############
|
||||||
@ -1190,7 +823,7 @@ class ScenarioSchema(Schema):
|
|||||||
description: str = Field(None, alias="description")
|
description: str = Field(None, alias="description")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
identifier: str = "scenario"
|
identifier: str = "scenario"
|
||||||
linkedTo: List[Dict[str, str]] = Field([], alias="linked_to")
|
linkedTo: List[Dict[str, str]] = Field({}, alias="linked_to")
|
||||||
name: str = Field(None, alias="name")
|
name: str = Field(None, alias="name")
|
||||||
pathways: List["SimplePathway"] = Field([], alias="related_pathways")
|
pathways: List["SimplePathway"] = Field([], alias="related_pathways")
|
||||||
relatedScenarios: List[Dict[str, str]] = Field([], alias="related_scenarios")
|
relatedScenarios: List[Dict[str, str]] = Field([], alias="related_scenarios")
|
||||||
@ -1241,38 +874,6 @@ def get_package_scenario(request, package_uuid, scenario_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/scenario")
|
|
||||||
def delete_scenarios(request, package_uuid, scenario_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
scens = Scenario.objects.filter(package=p)
|
|
||||||
scens.delete()
|
|
||||||
return redirect(f"{p.url}/scenario")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete Scenarios!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {"message": "Deleting Scenarios failed due to insufficient rights!"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/scenario/{uuid:scenario_uuid}")
|
|
||||||
def delete_scenario(request, package_uuid, scenario_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
scen = Scenario.objects.get(package=p, uuid=scenario_uuid)
|
|
||||||
scen.delete()
|
|
||||||
return redirect(f"{p.url}/scenario")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this Scenario!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Scenario with id {scenario_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
###########
|
###########
|
||||||
# Pathway #
|
# Pathway #
|
||||||
###########
|
###########
|
||||||
@ -1412,67 +1013,46 @@ def get_package_pathway(request, package_uuid, pathway_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CreatePathway(Schema):
|
|
||||||
smilesinput: str
|
|
||||||
name: str | None = None
|
|
||||||
description: str | None = None
|
|
||||||
rootOnly: str | None = None
|
|
||||||
selectedSetting: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/package/{uuid:package_uuid}/pathway")
|
@router.post("/package/{uuid:package_uuid}/pathway")
|
||||||
def create_pathway(
|
def create_pathway(
|
||||||
request,
|
request,
|
||||||
package_uuid,
|
package_uuid,
|
||||||
pw: Form[CreatePathway],
|
smilesinput: Form[str],
|
||||||
|
name: Optional[str] = Form(None),
|
||||||
|
description: Optional[str] = Form(None),
|
||||||
|
rootOnly: Optional[str] = Form(None),
|
||||||
|
selectedSetting: Optional[str] = Form(None),
|
||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
||||||
|
|
||||||
stand_smiles = FormatConverter.standardize(pw.smilesinput.strip())
|
stand_smiles = FormatConverter.standardize(smilesinput.strip())
|
||||||
|
|
||||||
new_pw = Pathway.create(p, stand_smiles, name=pw.name, description=pw.description)
|
pw = Pathway.create(p, stand_smiles, name=name, description=description)
|
||||||
|
|
||||||
pw_mode = "predict"
|
pw_mode = "predict"
|
||||||
if pw.rootOnly and pw.rootOnly.strip() == "true":
|
if rootOnly and rootOnly == "true":
|
||||||
pw_mode = "build"
|
pw_mode = "build"
|
||||||
|
|
||||||
new_pw.kv.update({"mode": pw_mode})
|
pw.kv.update({"mode": pw_mode})
|
||||||
new_pw.save()
|
pw.save()
|
||||||
|
|
||||||
if pw_mode == "predict":
|
if pw_mode == "predict":
|
||||||
setting = request.user.prediction_settings()
|
setting = request.user.prediction_settings()
|
||||||
|
|
||||||
if pw.selectedSetting:
|
if selectedSetting:
|
||||||
setting = SettingManager.get_setting_by_url(request.user, pw.selectedSetting)
|
setting = SettingManager.get_setting_by_url(request.user, selectedSetting)
|
||||||
|
|
||||||
new_pw.setting = setting
|
pw.setting = setting
|
||||||
new_pw.save()
|
pw.save()
|
||||||
|
|
||||||
from .tasks import dispatch, predict
|
from .tasks import predict
|
||||||
|
|
||||||
dispatch(request.user, predict, new_pw.pk, setting.pk, limit=None)
|
predict.delay(pw.pk, setting.pk, limit=-1)
|
||||||
|
|
||||||
return redirect(new_pw.url)
|
return redirect(pw.url)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return 400, {"message": str(e)}
|
print(e)
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}")
|
|
||||||
def delete_pathway(request, package_uuid, pathway_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
|
||||||
pw.delete()
|
|
||||||
return redirect(f"{p.url}/pathway")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this pathway!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Pathway with id {pathway_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
########
|
########
|
||||||
@ -1563,52 +1143,6 @@ def get_package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CreateNode(Schema):
|
|
||||||
nodeAsSmiles: str
|
|
||||||
nodeName: str | None = None
|
|
||||||
nodeReason: str | None = None
|
|
||||||
nodeDepth: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node",
|
|
||||||
response={200: str | Any, 403: Error},
|
|
||||||
)
|
|
||||||
def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
|
||||||
|
|
||||||
if n.nodeDepth is not None and n.nodeDepth.strip() != "":
|
|
||||||
node_depth = int(n.nodeDepth)
|
|
||||||
else:
|
|
||||||
node_depth = -1
|
|
||||||
|
|
||||||
n = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
|
|
||||||
|
|
||||||
return redirect(n.url)
|
|
||||||
except ValueError:
|
|
||||||
return 403, {"message": "Adding node failed!"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/node/{uuid:node_uuid}")
|
|
||||||
def delete_node(request, package_uuid, pathway_uuid, node_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
|
||||||
n = Node.objects.get(pathway=pw, uuid=node_uuid)
|
|
||||||
n.delete()
|
|
||||||
return redirect(f"{pw.url}/node")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this Node!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Node with id {node_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
########
|
########
|
||||||
# Edge #
|
# Edge #
|
||||||
########
|
########
|
||||||
@ -1672,200 +1206,6 @@ def get_package_pathway_edge(request, package_uuid, pathway_uuid, edge_uuid):
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class CreateEdge(Schema):
|
|
||||||
edgeAsSmirks: str | None = None
|
|
||||||
educts: str | None = None # Node URIs comma sep
|
|
||||||
products: str | None = None # Node URIs comma sep
|
|
||||||
multistep: str | None = None
|
|
||||||
edgeReason: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.post(
|
|
||||||
"/package/{uuid:package_uuid}/üathway/{uuid:pathway_uuid}/edge",
|
|
||||||
response={200: str | Any, 403: Error},
|
|
||||||
)
|
|
||||||
def add_pathway_edge(request, package_uuid, pathway_uuid, e: Form[CreateEdge]):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
|
||||||
|
|
||||||
if e.edgeAsSmirks is None and (e.educts is None or e.products is None):
|
|
||||||
raise ValueError("Either SMIRKS or educt/product must be provided")
|
|
||||||
|
|
||||||
if e.edgeAsSmirks is not None and (e.educts is not None and e.products is not None):
|
|
||||||
raise ValueError("SMIRKS and educt/product provided!")
|
|
||||||
|
|
||||||
educts = []
|
|
||||||
products = []
|
|
||||||
|
|
||||||
if e.edgeAsSmirks:
|
|
||||||
for ed in e.edgeAsSmirks.split(">>")[0].split("\\."):
|
|
||||||
educts.append(Node.objects.get(pathway=pw, default_node_label__smiles=ed))
|
|
||||||
|
|
||||||
for pr in e.edgeAsSmirks.split(">>")[1].split("\\."):
|
|
||||||
products.append(Node.objects.get(pathway=pw, default_node_label__smiles=pr))
|
|
||||||
else:
|
|
||||||
for ed in e.educts.split(","):
|
|
||||||
educts.append(Node.objects.get(pathway=pw, url=ed.strip()))
|
|
||||||
|
|
||||||
for pr in e.products.split(","):
|
|
||||||
products.append(Node.objects.get(pathway=pw, url=pr.strip()))
|
|
||||||
|
|
||||||
new_e = Edge.create(
|
|
||||||
pathway=pw,
|
|
||||||
start_nodes=educts,
|
|
||||||
end_nodes=products,
|
|
||||||
rule=None,
|
|
||||||
name=e.name,
|
|
||||||
description=e.edgeReason,
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(new_e.url)
|
|
||||||
except ValueError:
|
|
||||||
return 403, {"message": "Adding node failed!"}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}/edge/{uuid:edge_uuid}")
|
|
||||||
def delete_edge(request, package_uuid, pathway_uuid, edge_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
pw = Pathway.objects.get(package=p, uuid=pathway_uuid)
|
|
||||||
e = Edge.objects.get(pathway=pw, uuid=edge_uuid)
|
|
||||||
e.delete()
|
|
||||||
return redirect(f"{pw.url}/edge")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this Edge!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Edge with id {edge_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
#########
|
|
||||||
# Model #
|
|
||||||
#########
|
|
||||||
class ModelWrapper(Schema):
|
|
||||||
relative_reasoning: List["SimpleModel"] = Field(..., alias="relative-reasoning")
|
|
||||||
|
|
||||||
|
|
||||||
class ModelSchema(Schema):
|
|
||||||
aliases: List[str] = Field([], alias="aliases")
|
|
||||||
description: str = Field(None, alias="description")
|
|
||||||
evalPackages: List["SimplePackage"] = Field([])
|
|
||||||
id: str = Field(None, alias="url")
|
|
||||||
identifier: str = "relative-reasoning"
|
|
||||||
# "info" : {
|
|
||||||
# "Accuracy (Single-Gen)" : "0.5932962678936605" ,
|
|
||||||
# "Area under PR-Curve (Single-Gen)" : "0.5654653182134282" ,
|
|
||||||
# "Area under ROC-Curve (Single-Gen)" : "0.8178302405034772" ,
|
|
||||||
# "Precision (Single-Gen)" : "0.6978730822873083" ,
|
|
||||||
# "Probability Threshold" : "0.5" ,
|
|
||||||
# "Recall/Sensitivity (Single-Gen)" : "0.4484149210261006"
|
|
||||||
# } ,
|
|
||||||
name: str = Field(None, alias="name")
|
|
||||||
pathwayPackages: List["SimplePackage"] = Field([])
|
|
||||||
reviewStatus: str = Field(None, alias="review_status")
|
|
||||||
rulePackages: List["SimplePackage"] = Field([])
|
|
||||||
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
|
||||||
status: str
|
|
||||||
statusMessage: str
|
|
||||||
threshold: str
|
|
||||||
type: str
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/model", response={200: ModelWrapper, 403: Error})
|
|
||||||
def get_models(request):
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/package/{uuid:package_uuid}/model", response={200: ModelWrapper, 403: Error})
|
|
||||||
def get_package_models(request, package_uuid, model_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
return EPModel.objects.filter(package=p)
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Getting Reaction with id {model_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class Classify(Schema):
|
|
||||||
smiles: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
@router.get(
|
|
||||||
"/package/{uuid:package_uuid}/model/{uuid:model_uuid}",
|
|
||||||
response={200: ModelSchema | Any, 403: Error, 400: Error},
|
|
||||||
)
|
|
||||||
def get_model(request, package_uuid, model_uuid, c: Query[Classify]):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
mod = EPModel.objects.get(package=p, uuid=model_uuid)
|
|
||||||
|
|
||||||
if c.smiles:
|
|
||||||
if c.smiles == "":
|
|
||||||
return 400, {"message": "Received empty SMILES"}
|
|
||||||
|
|
||||||
try:
|
|
||||||
stand_smiles = FormatConverter.standardize(c.smiles)
|
|
||||||
except ValueError:
|
|
||||||
return 400, {"message": f'"{c.smiles}" is not a valid SMILES'}
|
|
||||||
|
|
||||||
from epdb.tasks import dispatch_eager, predict_simple
|
|
||||||
|
|
||||||
_, pred_res = dispatch_eager(request.user, predict_simple, mod.pk, stand_smiles)
|
|
||||||
|
|
||||||
result = []
|
|
||||||
|
|
||||||
for pr in pred_res:
|
|
||||||
if len(pr) > 0:
|
|
||||||
products = []
|
|
||||||
for prod_set in pr.product_sets:
|
|
||||||
products.append(tuple([x for x in prod_set]))
|
|
||||||
|
|
||||||
res = {
|
|
||||||
"probability": pr.probability,
|
|
||||||
"products": list(set(products)),
|
|
||||||
}
|
|
||||||
|
|
||||||
if pr.rule:
|
|
||||||
res["id"] = pr.rule.url
|
|
||||||
res["identifier"] = pr.rule.get_rule_identifier()
|
|
||||||
res["name"] = pr.rule.name
|
|
||||||
res["reviewStatus"] = (
|
|
||||||
"reviewed" if pr.rule.package.reviewed else "unreviewed"
|
|
||||||
)
|
|
||||||
|
|
||||||
result.append(res)
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
return mod
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Getting Reaction with id {model_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/package/{uuid:package_uuid}/model/{uuid:model_uuid}")
|
|
||||||
def delete_model(request, package_uuid, model_uuid):
|
|
||||||
try:
|
|
||||||
p = PackageManager.get_package_by_id(request.user, package_uuid)
|
|
||||||
|
|
||||||
if PackageManager.writable(request.user, p):
|
|
||||||
m = EPModel.objects.get(package=p, uuid=model_uuid)
|
|
||||||
m.delete()
|
|
||||||
return redirect(f"{p.url}/model")
|
|
||||||
else:
|
|
||||||
raise ValueError("You do not have the rights to delete this Model!")
|
|
||||||
except ValueError:
|
|
||||||
return 403, {
|
|
||||||
"message": f"Deleting Model with id {model_uuid} failed due to insufficient rights!"
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
###########
|
###########
|
||||||
# Setting #
|
# Setting #
|
||||||
###########
|
###########
|
||||||
|
|||||||
285
epdb/logic.py
285
epdb/logic.py
@ -1,41 +1,39 @@
|
|||||||
import json
|
|
||||||
import logging
|
|
||||||
import re
|
import re
|
||||||
from typing import Any, Dict, List, Optional, Set, Union, Tuple
|
import logging
|
||||||
|
import json
|
||||||
|
from typing import Union, List, Optional, Set, Dict, Any
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
import nh3
|
import nh3
|
||||||
from django.conf import settings as s
|
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.conf import settings as s
|
||||||
from pydantic import ValidationError
|
from pydantic import ValidationError
|
||||||
|
|
||||||
from epdb.models import (
|
from epdb.models import (
|
||||||
Compound,
|
|
||||||
CompoundStructure,
|
|
||||||
Edge,
|
|
||||||
EnzymeLink,
|
|
||||||
EPModel,
|
|
||||||
ExpansionSchemeChoice,
|
|
||||||
Group,
|
|
||||||
GroupPackagePermission,
|
|
||||||
Node,
|
|
||||||
Pathway,
|
|
||||||
Permission,
|
|
||||||
Reaction,
|
|
||||||
Rule,
|
|
||||||
Setting,
|
|
||||||
User,
|
User,
|
||||||
|
Package,
|
||||||
UserPackagePermission,
|
UserPackagePermission,
|
||||||
|
GroupPackagePermission,
|
||||||
|
Permission,
|
||||||
|
Group,
|
||||||
|
Setting,
|
||||||
|
EPModel,
|
||||||
UserSettingPermission,
|
UserSettingPermission,
|
||||||
|
Rule,
|
||||||
|
Pathway,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
Compound,
|
||||||
|
Reaction,
|
||||||
|
CompoundStructure,
|
||||||
|
EnzymeLink,
|
||||||
)
|
)
|
||||||
from utilities.chem import FormatConverter
|
from utilities.chem import FormatConverter
|
||||||
from utilities.misc import PackageExporter, PackageImporter
|
from utilities.misc import PackageImporter, PackageExporter
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
|
||||||
|
|
||||||
|
|
||||||
class EPDBURLParser:
|
class EPDBURLParser:
|
||||||
UUID_PATTERN = r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
|
UUID_PATTERN = r"[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}"
|
||||||
@ -444,7 +442,6 @@ class PackageManager(object):
|
|||||||
if PackageManager.readable(user, p):
|
if PackageManager.readable(user, p):
|
||||||
return p
|
return p
|
||||||
else:
|
else:
|
||||||
# FIXME: use custom exception to be translatable to 403 in API
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"Insufficient permissions to access Package with ID {}".format(package_id)
|
"Insufficient permissions to access Package with ID {}".format(package_id)
|
||||||
)
|
)
|
||||||
@ -581,39 +578,30 @@ class PackageManager(object):
|
|||||||
else:
|
else:
|
||||||
_ = perm_cls.objects.update_or_create(defaults={"permission": new_perm}, **data)
|
_ = perm_cls.objects.update_or_create(defaults={"permission": new_perm}, **data)
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def grant_read(caller: User, package: Package, grantee: Union[User, Group]):
|
|
||||||
PackageManager.update_permissions(caller, package, grantee, Permission.READ[0])
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def grant_write(caller: User, package: Package, grantee: Union[User, Group]):
|
|
||||||
PackageManager.update_permissions(caller, package, grantee, Permission.WRITE[0])
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def import_legacy_package(
|
def import_legacy_package(
|
||||||
data: dict, owner: User, keep_ids=False, add_import_timestamp=True, trust_reviewed=False
|
data: dict, owner: User, keep_ids=False, add_import_timestamp=True, trust_reviewed=False
|
||||||
):
|
):
|
||||||
from collections import defaultdict
|
|
||||||
from datetime import datetime
|
|
||||||
from uuid import UUID, uuid4
|
from uuid import UUID, uuid4
|
||||||
|
from datetime import datetime
|
||||||
from envipy_additional_information import AdditionalInformationConverter
|
from collections import defaultdict
|
||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
|
Package,
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
SimpleRule,
|
||||||
Node,
|
SimpleAmbitRule,
|
||||||
ParallelRule,
|
ParallelRule,
|
||||||
Pathway,
|
|
||||||
Reaction,
|
|
||||||
Scenario,
|
|
||||||
SequentialRule,
|
SequentialRule,
|
||||||
SequentialRuleOrdering,
|
SequentialRuleOrdering,
|
||||||
SimpleAmbitRule,
|
Reaction,
|
||||||
SimpleRule,
|
Pathway,
|
||||||
|
Node,
|
||||||
|
Edge,
|
||||||
|
Scenario,
|
||||||
)
|
)
|
||||||
|
from envipy_additional_information import AdditionalInformationConverter
|
||||||
|
|
||||||
pack = Package()
|
pack = Package()
|
||||||
pack.uuid = UUID(data["id"].split("/")[-1]) if keep_ids else uuid4()
|
pack.uuid = UUID(data["id"].split("/")[-1]) if keep_ids else uuid4()
|
||||||
@ -1118,7 +1106,6 @@ class SettingManager(object):
|
|||||||
rule_packages: List[Package] = None,
|
rule_packages: List[Package] = None,
|
||||||
model: EPModel = None,
|
model: EPModel = None,
|
||||||
model_threshold: float = None,
|
model_threshold: float = None,
|
||||||
expansion_scheme: ExpansionSchemeChoice = ExpansionSchemeChoice.BFS,
|
|
||||||
):
|
):
|
||||||
new_s = Setting()
|
new_s = Setting()
|
||||||
# Clean for potential XSS
|
# Clean for potential XSS
|
||||||
@ -1401,9 +1388,6 @@ class SEdge(object):
|
|||||||
self.rule = rule
|
self.rule = rule
|
||||||
self.probability = probability
|
self.probability = probability
|
||||||
|
|
||||||
def product_smiles(self):
|
|
||||||
return [p.smiles for p in self.products]
|
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
full_hash = 0
|
full_hash = 0
|
||||||
|
|
||||||
@ -1489,7 +1473,6 @@ class SPathway(object):
|
|||||||
self.smiles_to_node: Dict[str, SNode] = dict(**{n.smiles: n for n in self.root_nodes})
|
self.smiles_to_node: Dict[str, SNode] = dict(**{n.smiles: n for n in self.root_nodes})
|
||||||
self.edges: Set["SEdge"] = set()
|
self.edges: Set["SEdge"] = set()
|
||||||
self.done = False
|
self.done = False
|
||||||
self.empty_due_to_threshold = False
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_pathway(pw: "Pathway", persist: bool = True):
|
def from_pathway(pw: "Pathway", persist: bool = True):
|
||||||
@ -1554,32 +1537,22 @@ class SPathway(object):
|
|||||||
|
|
||||||
return sorted(res, key=lambda x: hash(x))
|
return sorted(res, key=lambda x: hash(x))
|
||||||
|
|
||||||
def _expand(self, substrates: List[SNode]) -> Tuple[List[SNode], List[SEdge]]:
|
def predict_step(self, from_depth: int = None, from_node: "Node" = None):
|
||||||
"""
|
substrates: List[SNode] = []
|
||||||
Expands the given substrates by generating new nodes and edges based on prediction settings.
|
|
||||||
|
|
||||||
This method processes a list of substrates and expands them into new nodes and edges using defined
|
if from_depth is not None:
|
||||||
rules and settings. It evaluates each substrate to determine its applicability domain, persists
|
substrates = self._get_nodes_for_depth(from_depth)
|
||||||
domain assessments, and generates candidates for further processing. Newly created nodes and edges
|
elif from_node is not None:
|
||||||
are returned, and any applicable information is stored or updated internally during the process.
|
for k, v in self.snode_persist_lookup.items():
|
||||||
|
if from_node == v:
|
||||||
Parameters:
|
substrates = [k]
|
||||||
substrates (List[SNode]): A list of substrate nodes to be expanded.
|
break
|
||||||
|
else:
|
||||||
Returns:
|
raise ValueError("Neither from_depth nor from_node_url specified")
|
||||||
Tuple[List[SNode], List[SEdge]]:
|
|
||||||
A tuple containing:
|
|
||||||
- A list of new nodes generated during the expansion.
|
|
||||||
- A list of new edges representing connections between nodes based on candidate reactions.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If a node does not have an ID when it should have been saved already.
|
|
||||||
"""
|
|
||||||
new_nodes: List[SNode] = []
|
|
||||||
new_edges: List[SEdge] = []
|
|
||||||
|
|
||||||
|
new_tp = False
|
||||||
|
if substrates:
|
||||||
for sub in substrates:
|
for sub in substrates:
|
||||||
# For App Domain we have to ensure that each Node is evaluated
|
|
||||||
if sub.app_domain_assessment is None:
|
if sub.app_domain_assessment is None:
|
||||||
if self.prediction_setting.model:
|
if self.prediction_setting.model:
|
||||||
if self.prediction_setting.model.app_domain:
|
if self.prediction_setting.model.app_domain:
|
||||||
@ -1590,9 +1563,9 @@ class SPathway(object):
|
|||||||
if self.persist is not None:
|
if self.persist is not None:
|
||||||
n = self.snode_persist_lookup[sub]
|
n = self.snode_persist_lookup[sub]
|
||||||
|
|
||||||
if n.id is None:
|
assert n.id is not None, (
|
||||||
raise ValueError(f"Node {n} has no ID... aborting!")
|
"Node has no id! Should have been saved already... aborting!"
|
||||||
|
)
|
||||||
node_data = n.simple_json()
|
node_data = n.simple_json()
|
||||||
node_data["image"] = f"{n.url}?image=svg"
|
node_data["image"] = f"{n.url}?image=svg"
|
||||||
app_domain_assessment["assessment"]["node"] = node_data
|
app_domain_assessment["assessment"]["node"] = node_data
|
||||||
@ -1602,25 +1575,11 @@ class SPathway(object):
|
|||||||
|
|
||||||
sub.app_domain_assessment = app_domain_assessment
|
sub.app_domain_assessment = app_domain_assessment
|
||||||
|
|
||||||
expansion_result = self.prediction_setting.expand(self, sub)
|
candidates = self.prediction_setting.expand(self, sub)
|
||||||
|
|
||||||
# We don't have any substrate, but technically we have at least one rule that triggered.
|
|
||||||
# If our substrate is a root node a.k.a. depth == 0 store that info in SPathway
|
|
||||||
if (
|
|
||||||
len(expansion_result["transformations"]) == 0
|
|
||||||
and expansion_result["rule_triggered"]
|
|
||||||
and sub.depth == 0
|
|
||||||
):
|
|
||||||
self.empty_due_to_threshold = True
|
|
||||||
|
|
||||||
# Emit directly
|
|
||||||
if self.persist is not None:
|
|
||||||
self.persist.kv["empty_due_to_threshold"] = True
|
|
||||||
self.persist.save()
|
|
||||||
|
|
||||||
# candidates is a List of PredictionResult. The length of the List is equal to the number of rules
|
# candidates is a List of PredictionResult. The length of the List is equal to the number of rules
|
||||||
for cand_set in expansion_result["transformations"]:
|
for cand_set in candidates:
|
||||||
if cand_set:
|
if cand_set:
|
||||||
|
new_tp = True
|
||||||
# cand_set is a PredictionResult object that can consist of multiple candidate reactions
|
# cand_set is a PredictionResult object that can consist of multiple candidate reactions
|
||||||
for cand in cand_set:
|
for cand in cand_set:
|
||||||
cand_nodes = []
|
cand_nodes = []
|
||||||
@ -1634,9 +1593,10 @@ class SPathway(object):
|
|||||||
app_domain_assessment = (
|
app_domain_assessment = (
|
||||||
self.prediction_setting.model.app_domain.assess(c)
|
self.prediction_setting.model.app_domain.assess(c)
|
||||||
)
|
)
|
||||||
snode = SNode(c, sub.depth + 1, app_domain_assessment)
|
|
||||||
self.smiles_to_node[c] = snode
|
self.smiles_to_node[c] = SNode(
|
||||||
new_nodes.append(snode)
|
c, sub.depth + 1, app_domain_assessment
|
||||||
|
)
|
||||||
|
|
||||||
node = self.smiles_to_node[c]
|
node = self.smiles_to_node[c]
|
||||||
cand_nodes.append(node)
|
cand_nodes.append(node)
|
||||||
@ -1648,132 +1608,6 @@ class SPathway(object):
|
|||||||
probability=cand_set.probability,
|
probability=cand_set.probability,
|
||||||
)
|
)
|
||||||
self.edges.add(edge)
|
self.edges.add(edge)
|
||||||
new_edges.append(edge)
|
|
||||||
|
|
||||||
return new_nodes, new_edges
|
|
||||||
|
|
||||||
def predict(self):
|
|
||||||
"""
|
|
||||||
Predicts outcomes based on a graph traversal algorithm using the specified expansion schema.
|
|
||||||
|
|
||||||
This method iteratively explores the nodes of a graph starting from the root nodes, propagating
|
|
||||||
probabilities through edges, and updating the probabilities of the connected nodes. The traversal
|
|
||||||
can follow one of three predefined expansion schemas: Depth-First Search (DFS), Breadth-First Search
|
|
||||||
(BFS), or a Greedy approach based on node probabilities. The methodology ensures that all reachable
|
|
||||||
nodes are processed systematically according to the specified schema.
|
|
||||||
|
|
||||||
Errors will be raised if the expansion schema is undefined or invalid. Additionally, this method
|
|
||||||
supports persisting changes by writing back data to the database when configured to do so.
|
|
||||||
|
|
||||||
Attributes
|
|
||||||
----------
|
|
||||||
done : bool
|
|
||||||
A flag indicating whether the prediction process is completed.
|
|
||||||
persist : Any
|
|
||||||
An optional object that manages persistence operations for saving modifications.
|
|
||||||
root_nodes : List[SNode]
|
|
||||||
A collection of initial nodes in the graph from which traversal begins.
|
|
||||||
prediction_setting : Any
|
|
||||||
Configuration object specifying settings for graph traversal, such as the choice of
|
|
||||||
expansion schema.
|
|
||||||
|
|
||||||
Raises
|
|
||||||
------
|
|
||||||
ValueError
|
|
||||||
If an invalid or unknown expansion schema is provided in `prediction_setting`.
|
|
||||||
"""
|
|
||||||
# populate initial queue
|
|
||||||
queue = list(self.root_nodes)
|
|
||||||
processed = set()
|
|
||||||
|
|
||||||
# initial nodes have prob 1.0
|
|
||||||
node_probs: Dict[SNode, float] = {}
|
|
||||||
node_probs.update({n: 1.0 for n in queue})
|
|
||||||
|
|
||||||
while queue:
|
|
||||||
current = queue.pop(0)
|
|
||||||
|
|
||||||
if current in processed:
|
|
||||||
continue
|
|
||||||
|
|
||||||
processed.add(current)
|
|
||||||
|
|
||||||
new_nodes, new_edges = self._expand([current])
|
|
||||||
|
|
||||||
if new_nodes or new_edges:
|
|
||||||
# Check if we need to write back data to the database
|
|
||||||
if self.persist:
|
|
||||||
self._sync_to_pathway()
|
|
||||||
# call save to update the internal modified field
|
|
||||||
self.persist.save()
|
|
||||||
|
|
||||||
if new_nodes:
|
|
||||||
for edge in new_edges:
|
|
||||||
# All edge have `current` as educt
|
|
||||||
# Use `current` and adjust probs
|
|
||||||
current_prob = node_probs[current]
|
|
||||||
|
|
||||||
for prod in edge.products:
|
|
||||||
# Either is a new product or a product and we found a path with a higher prob
|
|
||||||
if (
|
|
||||||
prod not in node_probs
|
|
||||||
or current_prob * edge.probability > node_probs[prod]
|
|
||||||
):
|
|
||||||
node_probs[prod] = current_prob * edge.probability
|
|
||||||
|
|
||||||
# Update Queue to proceed
|
|
||||||
if self.prediction_setting.expansion_scheme == "DFS":
|
|
||||||
for n in new_nodes:
|
|
||||||
if n not in processed:
|
|
||||||
# We want to follow this path -> prepend queue
|
|
||||||
queue.insert(0, n)
|
|
||||||
elif self.prediction_setting.expansion_scheme == "BFS":
|
|
||||||
for n in new_nodes:
|
|
||||||
if n not in processed:
|
|
||||||
# Add at the end, everything queued before will be processed
|
|
||||||
# before new_nodese
|
|
||||||
queue.append(n)
|
|
||||||
elif self.prediction_setting.expansion_scheme == "GREEDY":
|
|
||||||
# Simply add them, as we will re-order the queue later
|
|
||||||
for n in new_nodes:
|
|
||||||
if n not in processed:
|
|
||||||
queue.append(n)
|
|
||||||
|
|
||||||
node_and_probs = []
|
|
||||||
for queued_val in queue:
|
|
||||||
node_and_probs.append((queued_val, node_probs[queued_val]))
|
|
||||||
|
|
||||||
# re-order the queue and only pick smiles
|
|
||||||
queue = [
|
|
||||||
n[0] for n in sorted(node_and_probs, key=lambda x: x[1], reverse=True)
|
|
||||||
]
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Unknown expansion schema: {self.prediction_setting.expansion_scheme}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Queue exhausted, we're done
|
|
||||||
self.done = True
|
|
||||||
|
|
||||||
def predict_step(self, from_depth: int = None, from_node: "Node" = None):
|
|
||||||
substrates: List[SNode] = []
|
|
||||||
|
|
||||||
if from_depth is not None:
|
|
||||||
substrates = self._get_nodes_for_depth(from_depth)
|
|
||||||
elif from_node is not None:
|
|
||||||
for k, v in self.snode_persist_lookup.items():
|
|
||||||
if from_node == v:
|
|
||||||
substrates = [k]
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Node {from_node} not found in SPathway!")
|
|
||||||
else:
|
|
||||||
raise ValueError("Neither from_depth nor from_node_url specified")
|
|
||||||
|
|
||||||
new_tp = False
|
|
||||||
if substrates:
|
|
||||||
new_nodes, _ = self._expand(substrates)
|
|
||||||
new_tp = len(new_nodes) > 0
|
|
||||||
|
|
||||||
# In case no substrates are found, we're done.
|
# In case no substrates are found, we're done.
|
||||||
# For "predict from node" we're always done
|
# For "predict from node" we're always done
|
||||||
@ -1786,14 +1620,6 @@ class SPathway(object):
|
|||||||
# call save to update the internal modified field
|
# call save to update the internal modified field
|
||||||
self.persist.save()
|
self.persist.save()
|
||||||
|
|
||||||
def get_edge_for_educt_smiles(self, smiles: str) -> List[SEdge]:
|
|
||||||
res = []
|
|
||||||
for e in self.edges:
|
|
||||||
for n in e.educts:
|
|
||||||
if n.smiles == smiles:
|
|
||||||
res.append(e)
|
|
||||||
return res
|
|
||||||
|
|
||||||
def _sync_to_pathway(self) -> None:
|
def _sync_to_pathway(self) -> None:
|
||||||
logger.info("Updating Pathway with SPathway")
|
logger.info("Updating Pathway with SPathway")
|
||||||
|
|
||||||
@ -1857,6 +1683,11 @@ class SPathway(object):
|
|||||||
"to": to_indices,
|
"to": to_indices,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# if edge.rule:
|
||||||
|
# e['rule'] = {
|
||||||
|
# 'name': edge.rule.name,
|
||||||
|
# 'id': edge.rule.url,
|
||||||
|
# }
|
||||||
edges.append(e)
|
edges.append(e)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -2,9 +2,7 @@ from django.conf import settings as s
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from epdb.models import EnviFormer, MLRelativeReasoning
|
from epdb.models import MLRelativeReasoning, EnviFormer, Package
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
@ -77,13 +75,11 @@ class Command(BaseCommand):
|
|||||||
return packages
|
return packages
|
||||||
|
|
||||||
# Iteratively create models in options["model_names"]
|
# Iteratively create models in options["model_names"]
|
||||||
print(
|
print(f"Creating models: {options['model_names']}\n"
|
||||||
f"Creating models: {options['model_names']}\n"
|
|
||||||
f"Data packages: {options['data_packages']}\n"
|
f"Data packages: {options['data_packages']}\n"
|
||||||
f"Rule Packages (only for MLRR): {options['rule_packages']}\n"
|
f"Rule Packages (only for MLRR): {options['rule_packages']}\n"
|
||||||
f"Eval Packages: {options['eval_packages']}\n"
|
f"Eval Packages: {options['eval_packages']}\n"
|
||||||
f"Threshold: {options['threshold']:.2f}"
|
f"Threshold: {options['threshold']:.2f}")
|
||||||
)
|
|
||||||
data_packages = decode_packages(options["data_packages"])
|
data_packages = decode_packages(options["data_packages"])
|
||||||
eval_packages = decode_packages(options["eval_packages"])
|
eval_packages = decode_packages(options["eval_packages"])
|
||||||
rule_packages = decode_packages(options["rule_packages"])
|
rule_packages = decode_packages(options["rule_packages"])
|
||||||
@ -93,7 +89,8 @@ class Command(BaseCommand):
|
|||||||
model = EnviFormer.create(
|
model = EnviFormer.create(
|
||||||
pack,
|
pack,
|
||||||
data_packages=data_packages,
|
data_packages=data_packages,
|
||||||
threshold=options["threshold"],
|
eval_packages=eval_packages,
|
||||||
|
threshold=options['threshold'],
|
||||||
name=f"EnviFormer - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
|
name=f"EnviFormer - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
|
||||||
description=f"EnviFormer transformer trained on {options['data_packages']} "
|
description=f"EnviFormer transformer trained on {options['data_packages']} "
|
||||||
f"evaluated on {options['eval_packages']}.",
|
f"evaluated on {options['eval_packages']}.",
|
||||||
@ -103,7 +100,8 @@ class Command(BaseCommand):
|
|||||||
package=pack,
|
package=pack,
|
||||||
rule_packages=rule_packages,
|
rule_packages=rule_packages,
|
||||||
data_packages=data_packages,
|
data_packages=data_packages,
|
||||||
threshold=options["threshold"],
|
eval_packages=eval_packages,
|
||||||
|
threshold=options['threshold'],
|
||||||
name=f"ECC - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
|
name=f"ECC - {', '.join(options['data_packages'])} - T{options['threshold']:.2f}",
|
||||||
description=f"ML Relative Reasoning trained on {options['data_packages']} with rules from "
|
description=f"ML Relative Reasoning trained on {options['data_packages']} with rules from "
|
||||||
f"{options['rule_packages']} and evaluated on {options['eval_packages']}.",
|
f"{options['rule_packages']} and evaluated on {options['eval_packages']}.",
|
||||||
|
|||||||
@ -8,9 +8,7 @@ from django.conf import settings as s
|
|||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
|
||||||
from epdb.models import EnviFormer
|
from epdb.models import EnviFormer, Package
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
from django.apps import apps
|
from django.apps import apps
|
||||||
from django.conf import settings as s
|
|
||||||
from django.core.management.base import BaseCommand
|
from django.core.management.base import BaseCommand
|
||||||
from django.db.models import F, JSONField, TextField, Value
|
|
||||||
from django.db.models.functions import Cast, Replace
|
from django.db.models import F, Value, TextField, JSONField
|
||||||
|
from django.db.models.functions import Replace, Cast
|
||||||
|
|
||||||
from epdb.models import EnviPathModel
|
from epdb.models import EnviPathModel
|
||||||
|
|
||||||
@ -23,12 +23,10 @@ class Command(BaseCommand):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
def handle(self, *args, **options):
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
|
||||||
Package.objects.update(url=Replace(F("url"), Value(options["old"]), Value(options["new"])))
|
|
||||||
|
|
||||||
MODELS = [
|
MODELS = [
|
||||||
"User",
|
"User",
|
||||||
"Group",
|
"Group",
|
||||||
|
"Package",
|
||||||
"Compound",
|
"Compound",
|
||||||
"CompoundStructure",
|
"CompoundStructure",
|
||||||
"Pathway",
|
"Pathway",
|
||||||
@ -49,6 +47,7 @@ class Command(BaseCommand):
|
|||||||
]
|
]
|
||||||
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}")
|
||||||
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"]))
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-12-02 13:09
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0011_auto_20251111_1413"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="node",
|
|
||||||
name="stereo_removed",
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="pathway",
|
|
||||||
name="predicted",
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,25 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-12-14 11:30
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0012_node_stereo_removed_pathway_predicted"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name="setting",
|
|
||||||
name="expansion_schema",
|
|
||||||
field=models.CharField(
|
|
||||||
choices=[
|
|
||||||
("BFS", "Breadth First Search"),
|
|
||||||
("DFS", "Depth First Search"),
|
|
||||||
("GREEDY", "Greedy"),
|
|
||||||
],
|
|
||||||
default="BFS",
|
|
||||||
max_length=20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@ -1,17 +0,0 @@
|
|||||||
# Generated by Django 5.2.7 on 2025-12-14 16:02
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
dependencies = [
|
|
||||||
("epdb", "0013_setting_expansion_schema"),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RenameField(
|
|
||||||
model_name="setting",
|
|
||||||
old_name="expansion_schema",
|
|
||||||
new_name="expansion_scheme",
|
|
||||||
),
|
|
||||||
]
|
|
||||||
520
epdb/models.py
520
epdb/models.py
@ -2,41 +2,40 @@ import abc
|
|||||||
import hashlib
|
import hashlib
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import math
|
|
||||||
import os
|
import os
|
||||||
import secrets
|
import secrets
|
||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Any, Dict, List, Optional, Set, Tuple, Union
|
from typing import Union, List, Optional, Dict, Tuple, Set, Any
|
||||||
from uuid import uuid4
|
from uuid import uuid4
|
||||||
|
import math
|
||||||
import joblib
|
import joblib
|
||||||
import nh3
|
import nh3
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth.models import AbstractUser
|
from django.contrib.auth.models import AbstractUser
|
||||||
from django.contrib.contenttypes.fields import GenericForeignKey, GenericRelation
|
from django.contrib.contenttypes.fields import GenericRelation, GenericForeignKey
|
||||||
from django.contrib.contenttypes.models import ContentType
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import Count, JSONField, Q, QuerySet
|
from django.db.models import JSONField, Count, Q, QuerySet
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.functional import cached_property
|
from django.utils.functional import cached_property
|
||||||
from envipy_additional_information import EnviPyModel, HalfLife
|
from envipy_additional_information import EnviPyModel
|
||||||
from model_utils.models import TimeStampedModel
|
from model_utils.models import TimeStampedModel
|
||||||
from polymorphic.models import PolymorphicModel
|
from polymorphic.models import PolymorphicModel
|
||||||
from sklearn.metrics import jaccard_score, precision_score, recall_score
|
from sklearn.metrics import precision_score, recall_score, jaccard_score
|
||||||
from sklearn.model_selection import ShuffleSplit
|
from sklearn.model_selection import ShuffleSplit
|
||||||
|
|
||||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
from utilities.chem import FormatConverter, ProductSet, PredictionResult, IndigoUtils
|
||||||
from utilities.ml import (
|
from utilities.ml import (
|
||||||
ApplicabilityDomainPCA,
|
|
||||||
Dataset,
|
|
||||||
EnsembleClassifierChain,
|
|
||||||
EnviFormerDataset,
|
|
||||||
RelativeReasoning,
|
|
||||||
RuleBasedDataset,
|
RuleBasedDataset,
|
||||||
|
ApplicabilityDomainPCA,
|
||||||
|
EnsembleClassifierChain,
|
||||||
|
RelativeReasoning,
|
||||||
|
EnviFormerDataset,
|
||||||
|
Dataset,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -45,6 +44,8 @@ logger = logging.getLogger(__name__)
|
|||||||
##########################
|
##########################
|
||||||
# User/Groups/Permission #
|
# User/Groups/Permission #
|
||||||
##########################
|
##########################
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
email = models.EmailField(unique=True)
|
email = models.EmailField(unique=True)
|
||||||
uuid = models.UUIDField(
|
uuid = models.UUIDField(
|
||||||
@ -52,10 +53,7 @@ class User(AbstractUser):
|
|||||||
)
|
)
|
||||||
url = models.TextField(blank=False, null=True, verbose_name="URL", unique=True)
|
url = models.TextField(blank=False, null=True, verbose_name="URL", unique=True)
|
||||||
default_package = models.ForeignKey(
|
default_package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL,
|
"epdb.Package", verbose_name="Default Package", null=True, on_delete=models.SET_NULL
|
||||||
verbose_name="Default Package",
|
|
||||||
null=True,
|
|
||||||
on_delete=models.SET_NULL,
|
|
||||||
)
|
)
|
||||||
default_group = models.ForeignKey(
|
default_group = models.ForeignKey(
|
||||||
"Group",
|
"Group",
|
||||||
@ -245,7 +243,7 @@ class UserPackagePermission(Permission):
|
|||||||
)
|
)
|
||||||
user = models.ForeignKey("User", verbose_name="Permission to", on_delete=models.CASCADE)
|
user = models.ForeignKey("User", verbose_name="Permission to", on_delete=models.CASCADE)
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Permission on", on_delete=models.CASCADE
|
"epdb.Package", verbose_name="Permission on", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -261,7 +259,7 @@ class GroupPackagePermission(Permission):
|
|||||||
)
|
)
|
||||||
group = models.ForeignKey("Group", verbose_name="Permission to", on_delete=models.CASCADE)
|
group = models.ForeignKey("Group", verbose_name="Permission to", on_delete=models.CASCADE)
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Permission on", on_delete=models.CASCADE
|
"epdb.Package", verbose_name="Permission on", on_delete=models.CASCADE
|
||||||
)
|
)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
@ -730,13 +728,10 @@ class Package(EnviPathModel):
|
|||||||
rules = sorted(rules, key=lambda x: x.url)
|
rules = sorted(rules, key=lambda x: x.url)
|
||||||
return rules
|
return rules
|
||||||
|
|
||||||
class Meta:
|
|
||||||
swappable = "EPDB_PACKAGE_MODEL"
|
|
||||||
|
|
||||||
|
|
||||||
class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin):
|
class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin):
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
default_structure = models.ForeignKey(
|
default_structure = models.ForeignKey(
|
||||||
"CompoundStructure",
|
"CompoundStructure",
|
||||||
@ -754,30 +749,6 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def normalized_structure(self) -> "CompoundStructure":
|
def normalized_structure(self) -> "CompoundStructure":
|
||||||
if not CompoundStructure.objects.filter(compound=self, normalized_structure=True).exists():
|
|
||||||
num_structs = self.structures.count()
|
|
||||||
stand_smiles = set()
|
|
||||||
for structure in self.structures.all():
|
|
||||||
stand_smiles.add(FormatConverter.standardize(structure.smiles))
|
|
||||||
|
|
||||||
if len(stand_smiles) != 1:
|
|
||||||
logger.debug(
|
|
||||||
f"#Structures: {num_structs} - #Standardized SMILES: {len(stand_smiles)}"
|
|
||||||
)
|
|
||||||
logger.debug(f"Couldn't infer normalized structure for {self.name} - {self.url}")
|
|
||||||
raise ValueError(
|
|
||||||
f"Couldn't find nor infer normalized structure for {self.name} ({self.url})"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
cs = CompoundStructure.create(
|
|
||||||
self,
|
|
||||||
stand_smiles.pop(),
|
|
||||||
name="Normalized structure of {}".format(self.name),
|
|
||||||
description="{} (in its normalized form)".format(self.description),
|
|
||||||
normalized_structure=True,
|
|
||||||
)
|
|
||||||
return cs
|
|
||||||
|
|
||||||
return CompoundStructure.objects.get(compound=self, normalized_structure=True)
|
return CompoundStructure.objects.get(compound=self, normalized_structure=True)
|
||||||
|
|
||||||
def _url(self):
|
def _url(self):
|
||||||
@ -795,7 +766,9 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def related_pathways(self):
|
def related_pathways(self):
|
||||||
pathways = self.related_nodes.values_list("pathway", flat=True)
|
pathways = Node.objects.filter(node_labels__in=[self.default_structure]).values_list(
|
||||||
|
"pathway", flat=True
|
||||||
|
)
|
||||||
return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by("name")
|
return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by("name")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -805,16 +778,10 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
|||||||
| Reaction.objects.filter(package=self.package, products__in=[self.default_structure])
|
| Reaction.objects.filter(package=self.package, products__in=[self.default_structure])
|
||||||
).order_by("name")
|
).order_by("name")
|
||||||
|
|
||||||
@property
|
|
||||||
def related_nodes(self):
|
|
||||||
return Node.objects.filter(
|
|
||||||
node_labels__in=[self.default_structure], pathway__package=self.package
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
package: "Package", smiles: str, name: str = None, description: str = None, *args, **kwargs
|
package: Package, smiles: str, name: str = None, description: str = None, *args, **kwargs
|
||||||
) -> "Compound":
|
) -> "Compound":
|
||||||
if smiles is None or smiles.strip() == "":
|
if smiles is None or smiles.strip() == "":
|
||||||
raise ValueError("SMILES is required")
|
raise ValueError("SMILES is required")
|
||||||
@ -929,79 +896,15 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
|||||||
if self in mapping:
|
if self in mapping:
|
||||||
return mapping[self]
|
return mapping[self]
|
||||||
|
|
||||||
default_structure_smiles = self.default_structure.smiles
|
|
||||||
normalized_structure_smiles = self.normalized_structure.smiles
|
|
||||||
|
|
||||||
existing_compound = None
|
|
||||||
existing_normalized_compound = None
|
|
||||||
|
|
||||||
# Dedup check - Check if we find a direct match for a given SMILES
|
|
||||||
if CompoundStructure.objects.filter(
|
|
||||||
smiles=default_structure_smiles, compound__package=target
|
|
||||||
).exists():
|
|
||||||
existing_compound = CompoundStructure.objects.get(
|
|
||||||
smiles=default_structure_smiles, compound__package=target
|
|
||||||
).compound
|
|
||||||
|
|
||||||
# Check if we can find the standardized one
|
|
||||||
if CompoundStructure.objects.filter(
|
|
||||||
smiles=normalized_structure_smiles, compound__package=target
|
|
||||||
).exists():
|
|
||||||
existing_normalized_compound = CompoundStructure.objects.get(
|
|
||||||
smiles=normalized_structure_smiles, compound__package=target
|
|
||||||
).compound
|
|
||||||
|
|
||||||
if any([existing_compound, existing_normalized_compound]):
|
|
||||||
if existing_normalized_compound and existing_compound:
|
|
||||||
# We only have to set the mapping
|
|
||||||
mapping[self] = existing_compound
|
|
||||||
for structure in self.structures.all():
|
|
||||||
if structure not in mapping:
|
|
||||||
mapping[structure] = existing_compound.structures.get(
|
|
||||||
smiles=structure.smiles
|
|
||||||
)
|
|
||||||
|
|
||||||
return existing_compound
|
|
||||||
|
|
||||||
elif existing_normalized_compound:
|
|
||||||
mapping[self] = existing_normalized_compound
|
|
||||||
|
|
||||||
# Merge the structure into the existing compound
|
|
||||||
for structure in self.structures.all():
|
|
||||||
if existing_normalized_compound.structures.filter(
|
|
||||||
smiles=structure.smiles
|
|
||||||
).exists():
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Create a new Structure
|
|
||||||
cs = CompoundStructure.create(
|
|
||||||
existing_normalized_compound,
|
|
||||||
structure.smiles,
|
|
||||||
name=structure.name,
|
|
||||||
description=structure.description,
|
|
||||||
normalized_structure=structure.normalized_structure,
|
|
||||||
)
|
|
||||||
|
|
||||||
mapping[structure] = cs
|
|
||||||
|
|
||||||
return existing_normalized_compound
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise ValueError(
|
|
||||||
f"Found a CompoundStructure for {default_structure_smiles} but not for {normalized_structure_smiles} in target package {target.name}"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Here we can safely use Compound.objects.create as we won't end up in a duplicate
|
|
||||||
new_compound = Compound.objects.create(
|
new_compound = Compound.objects.create(
|
||||||
package=target,
|
package=target,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
description=self.description,
|
description=self.description,
|
||||||
kv=self.kv.copy() if self.kv else {},
|
kv=self.kv.copy() if self.kv else {},
|
||||||
)
|
)
|
||||||
|
|
||||||
mapping[self] = new_compound
|
mapping[self] = new_compound
|
||||||
|
|
||||||
# Copy underlying structures
|
# Copy compound structures
|
||||||
for structure in self.structures.all():
|
for structure in self.structures.all():
|
||||||
if structure not in mapping:
|
if structure not in mapping:
|
||||||
new_structure = CompoundStructure.objects.create(
|
new_structure = CompoundStructure.objects.create(
|
||||||
@ -1046,17 +949,6 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
|||||||
|
|
||||||
return new_compound
|
return new_compound
|
||||||
|
|
||||||
def half_lifes(self):
|
|
||||||
hls: Dict[Scenario, List[HalfLife]] = defaultdict(list)
|
|
||||||
|
|
||||||
for n in self.related_nodes:
|
|
||||||
for scen in n.scenarios.all().order_by("name"):
|
|
||||||
for ai in scen.get_additional_information():
|
|
||||||
if isinstance(ai, HalfLife):
|
|
||||||
hls[scen].append(ai)
|
|
||||||
|
|
||||||
return dict(hls)
|
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
unique_together = [("uuid", "package")]
|
unique_together = [("uuid", "package")]
|
||||||
|
|
||||||
@ -1169,7 +1061,7 @@ class EnzymeLink(EnviPathModel, KEGGIdentifierMixin):
|
|||||||
|
|
||||||
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
|
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
# # https://github.com/django-polymorphic/django-polymorphic/issues/229
|
# # https://github.com/django-polymorphic/django-polymorphic/issues/229
|
||||||
@ -1182,10 +1074,6 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
def apply(self, *args, **kwargs):
|
def apply(self, *args, **kwargs):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
|
||||||
def get_rule_identifier(self) -> str:
|
|
||||||
pass
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def cls_for_type(rule_type: str):
|
def cls_for_type(rule_type: str):
|
||||||
if rule_type == "SimpleAmbitRule":
|
if rule_type == "SimpleAmbitRule":
|
||||||
@ -1215,44 +1103,34 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
rule_type = type(self)
|
rule_type = type(self)
|
||||||
|
|
||||||
if rule_type == SimpleAmbitRule:
|
if rule_type == SimpleAmbitRule:
|
||||||
new_rule = SimpleAmbitRule.create(
|
new_rule = SimpleAmbitRule.objects.create(
|
||||||
package=target,
|
package=target,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
description=self.description,
|
description=self.description,
|
||||||
smirks=self.smirks,
|
smirks=self.smirks,
|
||||||
reactant_filter_smarts=self.reactant_filter_smarts,
|
reactant_filter_smarts=self.reactant_filter_smarts,
|
||||||
product_filter_smarts=self.product_filter_smarts,
|
product_filter_smarts=self.product_filter_smarts,
|
||||||
|
kv=self.kv.copy() if self.kv else {},
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.kv:
|
|
||||||
new_rule.kv.update(**self.kv)
|
|
||||||
new_rule.save()
|
|
||||||
|
|
||||||
elif rule_type == SimpleRDKitRule:
|
elif rule_type == SimpleRDKitRule:
|
||||||
new_rule = SimpleRDKitRule.create(
|
new_rule = SimpleRDKitRule.objects.create(
|
||||||
package=target,
|
package=target,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
description=self.description,
|
description=self.description,
|
||||||
reaction_smarts=self.reaction_smarts,
|
reaction_smarts=self.reaction_smarts,
|
||||||
|
kv=self.kv.copy() if self.kv else {},
|
||||||
)
|
)
|
||||||
|
|
||||||
if self.kv:
|
|
||||||
new_rule.kv.update(**self.kv)
|
|
||||||
new_rule.save()
|
|
||||||
|
|
||||||
elif rule_type == ParallelRule:
|
elif rule_type == ParallelRule:
|
||||||
new_srs = []
|
new_rule = ParallelRule.objects.create(
|
||||||
for simple_rule in self.simple_rules.all():
|
|
||||||
copied_simple_rule = simple_rule.copy(target, mapping)
|
|
||||||
new_srs.append(copied_simple_rule)
|
|
||||||
|
|
||||||
new_rule = ParallelRule.create(
|
|
||||||
package=target,
|
package=target,
|
||||||
simple_rules=new_srs,
|
|
||||||
name=self.name,
|
name=self.name,
|
||||||
description=self.description,
|
description=self.description,
|
||||||
|
kv=self.kv.copy() if self.kv else {},
|
||||||
)
|
)
|
||||||
|
# Copy simple rules relationships
|
||||||
|
for simple_rule in self.simple_rules.all():
|
||||||
|
copied_simple_rule = simple_rule.copy(target, mapping)
|
||||||
|
new_rule.simple_rules.add(copied_simple_rule)
|
||||||
elif rule_type == SequentialRule:
|
elif rule_type == SequentialRule:
|
||||||
raise ValueError("SequentialRule copy not implemented!")
|
raise ValueError("SequentialRule copy not implemented!")
|
||||||
else:
|
else:
|
||||||
@ -1289,7 +1167,7 @@ class SimpleAmbitRule(SimpleRule):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
package: "Package",
|
package: Package,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
smirks: str = None,
|
smirks: str = None,
|
||||||
@ -1350,16 +1228,8 @@ class SimpleAmbitRule(SimpleRule):
|
|||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/simple-ambit-rule/{}".format(self.package.url, self.uuid)
|
return "{}/simple-ambit-rule/{}".format(self.package.url, self.uuid)
|
||||||
|
|
||||||
def get_rule_identifier(self) -> str:
|
|
||||||
return "simple-rule"
|
|
||||||
|
|
||||||
def apply(self, smiles):
|
def apply(self, smiles):
|
||||||
return FormatConverter.apply(
|
return FormatConverter.apply(smiles, self.smirks)
|
||||||
smiles,
|
|
||||||
self.smirks,
|
|
||||||
reactant_filter_smarts=self.reactant_filter_smarts,
|
|
||||||
product_filter_smarts=self.product_filter_smarts,
|
|
||||||
)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reactants_smarts(self):
|
def reactants_smarts(self):
|
||||||
@ -1371,7 +1241,7 @@ class SimpleAmbitRule(SimpleRule):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def related_reactions(self):
|
def related_reactions(self):
|
||||||
qs = s.GET_PACKAGE_MODEL().objects.filter(reviewed=True)
|
qs = Package.objects.filter(reviewed=True)
|
||||||
return self.reaction_rule.filter(package__in=qs).order_by("name")
|
return self.reaction_rule.filter(package__in=qs).order_by("name")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -1403,9 +1273,6 @@ class ParallelRule(Rule):
|
|||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/parallel-rule/{}".format(self.package.url, self.uuid)
|
return "{}/parallel-rule/{}".format(self.package.url, self.uuid)
|
||||||
|
|
||||||
def get_rule_identifier(self) -> str:
|
|
||||||
return "parallel-rule"
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def srs(self) -> QuerySet:
|
def srs(self) -> QuerySet:
|
||||||
return self.simple_rules.all()
|
return self.simple_rules.all()
|
||||||
@ -1437,71 +1304,6 @@ class ParallelRule(Rule):
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
@transaction.atomic
|
|
||||||
def create(
|
|
||||||
package: "Package",
|
|
||||||
simple_rules: List["SimpleRule"],
|
|
||||||
name: str = None,
|
|
||||||
description: str = None,
|
|
||||||
reactant_filter_smarts: str = None,
|
|
||||||
product_filter_smarts: str = None,
|
|
||||||
):
|
|
||||||
if len(simple_rules) == 0:
|
|
||||||
raise ValueError("At least one simple rule is required!")
|
|
||||||
|
|
||||||
for sr in simple_rules:
|
|
||||||
if sr.package != package:
|
|
||||||
raise ValueError(
|
|
||||||
f"Simple rule {sr.uuid} does not belong to package {package.uuid}!"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Deduplication check
|
|
||||||
query = ParallelRule.objects.annotate(
|
|
||||||
srs_count=Count("simple_rules", filter=Q(simple_rules__in=simple_rules), distinct=True)
|
|
||||||
)
|
|
||||||
|
|
||||||
existing_rule_qs = query.filter(
|
|
||||||
srs_count=len(simple_rules),
|
|
||||||
)
|
|
||||||
|
|
||||||
if existing_rule_qs.exists():
|
|
||||||
if existing_rule_qs.count() > 1:
|
|
||||||
logger.error(f"Found more than one reaction for given input! {existing_rule_qs}")
|
|
||||||
return existing_rule_qs.first()
|
|
||||||
|
|
||||||
r = ParallelRule()
|
|
||||||
r.package = package
|
|
||||||
|
|
||||||
if name is not None:
|
|
||||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
|
||||||
|
|
||||||
if name is None or name == "":
|
|
||||||
name = f"Rule {Rule.objects.filter(package=package).count() + 1}"
|
|
||||||
|
|
||||||
r.name = name
|
|
||||||
if description is not None and description.strip() != "":
|
|
||||||
r.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
|
||||||
|
|
||||||
if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != "":
|
|
||||||
if not FormatConverter.is_valid_smarts(reactant_filter_smarts.strip()):
|
|
||||||
raise ValueError(f'Reactant Filter SMARTS "{reactant_filter_smarts}" is invalid!')
|
|
||||||
else:
|
|
||||||
r.reactant_filter_smarts = reactant_filter_smarts.strip()
|
|
||||||
|
|
||||||
if product_filter_smarts is not None and product_filter_smarts.strip() != "":
|
|
||||||
if not FormatConverter.is_valid_smarts(product_filter_smarts.strip()):
|
|
||||||
raise ValueError(f'Product Filter SMARTS "{product_filter_smarts}" is invalid!')
|
|
||||||
else:
|
|
||||||
r.product_filter_smarts = product_filter_smarts.strip()
|
|
||||||
|
|
||||||
r.save()
|
|
||||||
|
|
||||||
for sr in simple_rules:
|
|
||||||
r.simple_rules.add(sr)
|
|
||||||
|
|
||||||
return r
|
|
||||||
|
|
||||||
|
|
||||||
class SequentialRule(Rule):
|
class SequentialRule(Rule):
|
||||||
simple_rules = models.ManyToManyField(
|
simple_rules = models.ManyToManyField(
|
||||||
@ -1511,9 +1313,6 @@ class SequentialRule(Rule):
|
|||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/sequential-rule/{}".format(self.compound.url, self.uuid)
|
return "{}/sequential-rule/{}".format(self.compound.url, self.uuid)
|
||||||
|
|
||||||
def get_rule_identifier(self) -> str:
|
|
||||||
return "sequential-rule"
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def srs(self):
|
def srs(self):
|
||||||
return self.simple_rules.all()
|
return self.simple_rules.all()
|
||||||
@ -1534,7 +1333,7 @@ class SequentialRuleOrdering(models.Model):
|
|||||||
|
|
||||||
class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin):
|
class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin):
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
educts = models.ManyToManyField(
|
educts = models.ManyToManyField(
|
||||||
"epdb.CompoundStructure", verbose_name="Educts", related_name="reaction_educts"
|
"epdb.CompoundStructure", verbose_name="Educts", related_name="reaction_educts"
|
||||||
@ -1556,7 +1355,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
package: "Package",
|
package: Package,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
educts: Union[List[str], List[CompoundStructure]] = None,
|
educts: Union[List[str], List[CompoundStructure]] = None,
|
||||||
@ -1651,44 +1450,31 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
|
|||||||
if self in mapping:
|
if self in mapping:
|
||||||
return mapping[self]
|
return mapping[self]
|
||||||
|
|
||||||
copied_reaction_educts = []
|
# Create new reaction
|
||||||
copied_reaction_products = []
|
new_reaction = Reaction.objects.create(
|
||||||
copied_reaction_rules = []
|
package=target,
|
||||||
|
name=self.name,
|
||||||
|
description=self.description,
|
||||||
|
multi_step=self.multi_step,
|
||||||
|
medline_references=self.medline_references,
|
||||||
|
kv=self.kv.copy() if self.kv else {},
|
||||||
|
)
|
||||||
|
mapping[self] = new_reaction
|
||||||
|
|
||||||
# Copy educts (reactant compounds)
|
# Copy educts (reactant compounds)
|
||||||
for educt in self.educts.all():
|
for educt in self.educts.all():
|
||||||
copied_educt = educt.copy(target, mapping)
|
copied_educt = educt.copy(target, mapping)
|
||||||
copied_reaction_educts.append(copied_educt)
|
new_reaction.educts.add(copied_educt)
|
||||||
|
|
||||||
# Copy products
|
# Copy products
|
||||||
for product in self.products.all():
|
for product in self.products.all():
|
||||||
copied_product = product.copy(target, mapping)
|
copied_product = product.copy(target, mapping)
|
||||||
copied_reaction_products.append(copied_product)
|
new_reaction.products.add(copied_product)
|
||||||
|
|
||||||
# Copy rules
|
# Copy rules
|
||||||
for rule in self.rules.all():
|
for rule in self.rules.all():
|
||||||
copied_rule = rule.copy(target, mapping)
|
copied_rule = rule.copy(target, mapping)
|
||||||
copied_reaction_rules.append(copied_rule)
|
new_reaction.rules.add(copied_rule)
|
||||||
|
|
||||||
new_reaction = Reaction.create(
|
|
||||||
package=target,
|
|
||||||
name=self.name,
|
|
||||||
description=self.description,
|
|
||||||
educts=copied_reaction_educts,
|
|
||||||
products=copied_reaction_products,
|
|
||||||
rules=copied_reaction_rules,
|
|
||||||
multi_step=self.multi_step,
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.medline_references:
|
|
||||||
new_reaction.medline_references = self.medline_references
|
|
||||||
new_reaction.save()
|
|
||||||
|
|
||||||
if self.kv:
|
|
||||||
new_reaction.kv = self.kv
|
|
||||||
new_reaction.save()
|
|
||||||
|
|
||||||
mapping[self] = new_reaction
|
|
||||||
|
|
||||||
# Copy external identifiers
|
# Copy external identifiers
|
||||||
for ext_id in self.external_identifiers.all():
|
for ext_id in self.external_identifiers.all():
|
||||||
@ -1728,12 +1514,11 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
|
|||||||
|
|
||||||
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
setting = models.ForeignKey(
|
setting = models.ForeignKey(
|
||||||
"epdb.Setting", verbose_name="Setting", on_delete=models.CASCADE, null=True, blank=True
|
"epdb.Setting", verbose_name="Setting", on_delete=models.CASCADE, null=True, blank=True
|
||||||
)
|
)
|
||||||
predicted = models.BooleanField(default=False, null=False)
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def root_nodes(self):
|
def root_nodes(self):
|
||||||
@ -1759,16 +1544,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
# potentially prefetched edge_set
|
# potentially prefetched edge_set
|
||||||
return self.edge_set.all()
|
return self.edge_set.all()
|
||||||
|
|
||||||
@property
|
|
||||||
def setting_with_overrides(self):
|
|
||||||
mem_copy = Setting.objects.get(pk=self.setting.pk)
|
|
||||||
|
|
||||||
if "setting_overrides" in self.kv:
|
|
||||||
for k, v in self.kv["setting_overrides"].items():
|
|
||||||
setattr(mem_copy, k, f"{v} (this is an override for this particular pathway)")
|
|
||||||
|
|
||||||
return mem_copy
|
|
||||||
|
|
||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/pathway/{}".format(self.package.url, self.uuid)
|
return "{}/pathway/{}".format(self.package.url, self.uuid)
|
||||||
|
|
||||||
@ -1795,9 +1570,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
def failed(self):
|
def failed(self):
|
||||||
return self.status() == "failed"
|
return self.status() == "failed"
|
||||||
|
|
||||||
def empty_due_to_threshold(self):
|
|
||||||
return self.kv.get("empty_due_to_threshold", False)
|
|
||||||
|
|
||||||
def d3_json(self):
|
def d3_json(self):
|
||||||
# Ideally it would be something like this but
|
# Ideally it would be something like this but
|
||||||
# to reduce crossing in edges do a DFS
|
# to reduce crossing in edges do a DFS
|
||||||
@ -1819,9 +1591,11 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
while len(queue):
|
while len(queue):
|
||||||
current = queue.pop()
|
current = queue.pop()
|
||||||
processed.add(current)
|
processed.add(current)
|
||||||
|
|
||||||
nodes.append(current.d3_json())
|
nodes.append(current.d3_json())
|
||||||
|
|
||||||
for e in self.edges.filter(start_nodes=current).distinct():
|
for e in self.edges:
|
||||||
|
if current in e.start_nodes.all():
|
||||||
for prod in e.end_nodes.all():
|
for prod in e.end_nodes.all():
|
||||||
if prod not in queue and prod not in processed:
|
if prod not in queue and prod not in processed:
|
||||||
queue.append(prod)
|
queue.append(prod)
|
||||||
@ -1905,18 +1679,15 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
"status": self.status(),
|
"status": self.status(),
|
||||||
}
|
}
|
||||||
|
|
||||||
return res
|
return json.dumps(res)
|
||||||
|
|
||||||
def to_csv(self, include_header=True, include_pathway_url=False) -> str:
|
def to_csv(self) -> str:
|
||||||
import csv
|
import csv
|
||||||
import io
|
import io
|
||||||
|
|
||||||
header = []
|
rows = []
|
||||||
|
rows.append(
|
||||||
if include_pathway_url:
|
[
|
||||||
header += ["Pathway URL"]
|
|
||||||
|
|
||||||
header += [
|
|
||||||
"SMILES",
|
"SMILES",
|
||||||
"name",
|
"name",
|
||||||
"depth",
|
"depth",
|
||||||
@ -1925,20 +1696,10 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
"rule_ids",
|
"rule_ids",
|
||||||
"parent_smiles",
|
"parent_smiles",
|
||||||
]
|
]
|
||||||
|
)
|
||||||
rows = []
|
|
||||||
|
|
||||||
if include_header:
|
|
||||||
rows.append(header)
|
|
||||||
|
|
||||||
for n in self.nodes.order_by("depth"):
|
for n in self.nodes.order_by("depth"):
|
||||||
cs = n.default_node_label
|
cs = n.default_node_label
|
||||||
row = []
|
row = [cs.smiles, cs.name, n.depth]
|
||||||
|
|
||||||
if include_pathway_url:
|
|
||||||
row.append(n.pathway.url)
|
|
||||||
|
|
||||||
row += [cs.smiles, cs.name, n.depth]
|
|
||||||
|
|
||||||
edges = self.edges.filter(end_nodes__in=[n])
|
edges = self.edges.filter(end_nodes__in=[n])
|
||||||
if len(edges):
|
if len(edges):
|
||||||
@ -1969,7 +1730,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
smiles: str,
|
smiles: str,
|
||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
predicted: bool = False,
|
|
||||||
):
|
):
|
||||||
pw = Pathway()
|
pw = Pathway()
|
||||||
pw.package = package
|
pw.package = package
|
||||||
@ -1982,7 +1742,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
pw.name = name
|
pw.name = name
|
||||||
if description is not None and description.strip() != "":
|
if description is not None and description.strip() != "":
|
||||||
pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
pw.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
pw.predicted = predicted
|
|
||||||
|
|
||||||
pw.save()
|
pw.save()
|
||||||
try:
|
try:
|
||||||
@ -2002,8 +1761,6 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
return mapping[self]
|
return mapping[self]
|
||||||
|
|
||||||
# Start copying the pathway
|
# Start copying the pathway
|
||||||
# Its safe to use .objects.create here as Pathways itself aren't
|
|
||||||
# deduplicated
|
|
||||||
new_pathway = Pathway.objects.create(
|
new_pathway = Pathway.objects.create(
|
||||||
package=target,
|
package=target,
|
||||||
name=self.name,
|
name=self.name,
|
||||||
@ -2115,7 +1872,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
)
|
)
|
||||||
out_edges = models.ManyToManyField("epdb.Edge", verbose_name="Outgoing Edges")
|
out_edges = models.ManyToManyField("epdb.Edge", verbose_name="Outgoing Edges")
|
||||||
depth = models.IntegerField(verbose_name="Node depth", null=False, blank=False)
|
depth = models.IntegerField(verbose_name="Node depth", null=False, blank=False)
|
||||||
stereo_removed = models.BooleanField(default=False, null=False)
|
|
||||||
|
|
||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/node/{}".format(self.pathway.url, self.uuid)
|
return "{}/node/{}".format(self.pathway.url, self.uuid)
|
||||||
@ -2125,7 +1881,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
"depth": self.depth,
|
"depth": self.depth,
|
||||||
"stereo_removed": self.stereo_removed,
|
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
"node_label_id": self.default_node_label.url,
|
"node_label_id": self.default_node_label.url,
|
||||||
"image": f"{self.url}?image=svg",
|
"image": f"{self.url}?image=svg",
|
||||||
@ -2141,7 +1896,6 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
else None,
|
else None,
|
||||||
"uncovered_functional_groups": False,
|
"uncovered_functional_groups": False,
|
||||||
},
|
},
|
||||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -2152,17 +1906,12 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
name: Optional[str] = None,
|
name: Optional[str] = None,
|
||||||
description: Optional[str] = None,
|
description: Optional[str] = None,
|
||||||
):
|
):
|
||||||
stereo_removed = False
|
|
||||||
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
|
||||||
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
|
||||||
stereo_removed = True
|
|
||||||
c = Compound.create(pathway.package, smiles, name=name, description=description)
|
c = Compound.create(pathway.package, smiles, name=name, description=description)
|
||||||
|
|
||||||
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
|
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
|
||||||
return Node.objects.get(pathway=pathway, default_node_label=c.default_structure)
|
return Node.objects.get(pathway=pathway, default_node_label=c.default_structure)
|
||||||
|
|
||||||
n = Node()
|
n = Node()
|
||||||
n.stereo_removed = stereo_removed
|
|
||||||
n.pathway = pathway
|
n.pathway = pathway
|
||||||
n.depth = depth
|
n.depth = depth
|
||||||
|
|
||||||
@ -2327,7 +2076,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
|
|||||||
|
|
||||||
class EPModel(PolymorphicModel, EnviPathModel):
|
class EPModel(PolymorphicModel, EnviPathModel):
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
|
|
||||||
def _url(self):
|
def _url(self):
|
||||||
@ -2336,17 +2085,17 @@ class EPModel(PolymorphicModel, EnviPathModel):
|
|||||||
|
|
||||||
class PackageBasedModel(EPModel):
|
class PackageBasedModel(EPModel):
|
||||||
rule_packages = models.ManyToManyField(
|
rule_packages = models.ManyToManyField(
|
||||||
s.EPDB_PACKAGE_MODEL,
|
"Package",
|
||||||
verbose_name="Rule Packages",
|
verbose_name="Rule Packages",
|
||||||
related_name="%(app_label)s_%(class)s_rule_packages",
|
related_name="%(app_label)s_%(class)s_rule_packages",
|
||||||
)
|
)
|
||||||
data_packages = models.ManyToManyField(
|
data_packages = models.ManyToManyField(
|
||||||
s.EPDB_PACKAGE_MODEL,
|
"Package",
|
||||||
verbose_name="Data Packages",
|
verbose_name="Data Packages",
|
||||||
related_name="%(app_label)s_%(class)s_data_packages",
|
related_name="%(app_label)s_%(class)s_data_packages",
|
||||||
)
|
)
|
||||||
eval_packages = models.ManyToManyField(
|
eval_packages = models.ManyToManyField(
|
||||||
s.EPDB_PACKAGE_MODEL,
|
"Package",
|
||||||
verbose_name="Evaluation Packages",
|
verbose_name="Evaluation Packages",
|
||||||
related_name="%(app_label)s_%(class)s_eval_packages",
|
related_name="%(app_label)s_%(class)s_eval_packages",
|
||||||
)
|
)
|
||||||
@ -2403,29 +2152,6 @@ class PackageBasedModel(EPModel):
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@property
|
|
||||||
def mg_pr_curve(self):
|
|
||||||
if self.model_status != self.FINISHED:
|
|
||||||
raise ValueError(f"Expected {self.FINISHED} but model is in status {self.model_status}")
|
|
||||||
|
|
||||||
if not self.multigen_eval:
|
|
||||||
raise ValueError("MG PR Curve is only available for multigen models")
|
|
||||||
|
|
||||||
res = []
|
|
||||||
|
|
||||||
thresholds = self.eval_results["multigen_average_precision_per_threshold"].keys()
|
|
||||||
|
|
||||||
for t in thresholds:
|
|
||||||
res.append(
|
|
||||||
{
|
|
||||||
"precision": self.eval_results["multigen_average_precision_per_threshold"][t],
|
|
||||||
"recall": self.eval_results["multigen_average_recall_per_threshold"][t],
|
|
||||||
"threshold": float(t),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def applicable_rules(self) -> List["Rule"]:
|
def applicable_rules(self) -> List["Rule"]:
|
||||||
"""
|
"""
|
||||||
@ -2487,13 +2213,6 @@ class PackageBasedModel(EPModel):
|
|||||||
return Dataset.load(ds_path)
|
return Dataset.load(ds_path)
|
||||||
|
|
||||||
def retrain(self):
|
def retrain(self):
|
||||||
# Reset eval fields
|
|
||||||
self.eval_results = {}
|
|
||||||
self.eval_packages.clear()
|
|
||||||
self.model_status = False
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
# Do actual retrain
|
|
||||||
self.build_dataset()
|
self.build_dataset()
|
||||||
self.build_model()
|
self.build_model()
|
||||||
|
|
||||||
@ -2531,7 +2250,7 @@ class PackageBasedModel(EPModel):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
|
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
|
||||||
if self.model_status not in [self.BUILT_NOT_EVALUATED, self.FINISHED]:
|
if self.model_status != self.BUILT_NOT_EVALUATED:
|
||||||
raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
|
raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
|
||||||
|
|
||||||
if multigen:
|
if multigen:
|
||||||
@ -2539,12 +2258,9 @@ class PackageBasedModel(EPModel):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
if eval_packages is not None:
|
if eval_packages is not None:
|
||||||
self.eval_packages.clear()
|
|
||||||
for p in eval_packages:
|
for p in eval_packages:
|
||||||
self.eval_packages.add(p)
|
self.eval_packages.add(p)
|
||||||
|
|
||||||
self.eval_results = {}
|
|
||||||
|
|
||||||
self.model_status = self.EVALUATING
|
self.model_status = self.EVALUATING
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -2598,14 +2314,9 @@ class PackageBasedModel(EPModel):
|
|||||||
recall = {f"{t:.2f}": [] for t in thresholds}
|
recall = {f"{t:.2f}": [] for t in thresholds}
|
||||||
|
|
||||||
# Note: only one root compound supported at this time
|
# Note: only one root compound supported at this time
|
||||||
root_compounds = []
|
root_compounds = [
|
||||||
for pw in pathways:
|
[p.default_node_label.smiles for p in p.root_nodes][0] for p in pathways
|
||||||
if pw.root_nodes:
|
]
|
||||||
root_compounds.append(pw.root_nodes[0].default_node_label)
|
|
||||||
else:
|
|
||||||
logger.info(
|
|
||||||
f"Skipping MG Eval of Pathway {pw.name} ({pw.uuid}) as it has no root compounds!"
|
|
||||||
)
|
|
||||||
|
|
||||||
# As we need a Model Instance in our setting, get a fresh copy from db, overwrite the serialized mode and
|
# As we need a Model Instance in our setting, get a fresh copy from db, overwrite the serialized mode and
|
||||||
# pass it to the setting used in prediction
|
# pass it to the setting used in prediction
|
||||||
@ -2629,7 +2340,7 @@ class PackageBasedModel(EPModel):
|
|||||||
for i, root in enumerate(root_compounds):
|
for i, root in enumerate(root_compounds):
|
||||||
logger.debug(f"Evaluating pathway {i + 1} of {len(root_compounds)}...")
|
logger.debug(f"Evaluating pathway {i + 1} of {len(root_compounds)}...")
|
||||||
|
|
||||||
spw = SPathway(root_nodes=root.smiles, prediction_setting=s)
|
spw = SPathway(root_nodes=root, prediction_setting=s)
|
||||||
level = 0
|
level = 0
|
||||||
|
|
||||||
while not spw.done:
|
while not spw.done:
|
||||||
@ -3412,7 +3123,7 @@ class EnviFormer(PackageBasedModel):
|
|||||||
return args
|
return args
|
||||||
|
|
||||||
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
|
def evaluate_model(self, multigen: bool, eval_packages: List["Package"] = None, **kwargs):
|
||||||
if self.model_status not in [self.BUILT_NOT_EVALUATED, self.FINISHED]:
|
if self.model_status != self.BUILT_NOT_EVALUATED:
|
||||||
raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
|
raise ValueError(f"Can't evaluate a model in state {self.model_status}!")
|
||||||
|
|
||||||
if multigen:
|
if multigen:
|
||||||
@ -3420,12 +3131,9 @@ class EnviFormer(PackageBasedModel):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
if eval_packages is not None:
|
if eval_packages is not None:
|
||||||
self.eval_packages.clear()
|
|
||||||
for p in eval_packages:
|
for p in eval_packages:
|
||||||
self.eval_packages.add(p)
|
self.eval_packages.add(p)
|
||||||
|
|
||||||
self.eval_results = {}
|
|
||||||
|
|
||||||
self.model_status = self.EVALUATING
|
self.model_status = self.EVALUATING
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -3692,7 +3400,7 @@ class PluginModel(EPModel):
|
|||||||
|
|
||||||
class Scenario(EnviPathModel):
|
class Scenario(EnviPathModel):
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
s.EPDB_PACKAGE_MODEL, verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
"epdb.Package", verbose_name="Package", on_delete=models.CASCADE, db_index=True
|
||||||
)
|
)
|
||||||
scenario_date = models.CharField(max_length=256, null=False, blank=False, default="No date")
|
scenario_date = models.CharField(max_length=256, null=False, blank=False, default="No date")
|
||||||
scenario_type = models.CharField(
|
scenario_type = models.CharField(
|
||||||
@ -3835,12 +3543,6 @@ class UserSettingPermission(Permission):
|
|||||||
return f"User: {self.user} has Permission: {self.permission} on Setting: {self.setting}"
|
return f"User: {self.user} has Permission: {self.permission} on Setting: {self.setting}"
|
||||||
|
|
||||||
|
|
||||||
class ExpansionSchemeChoice(models.TextChoices):
|
|
||||||
BFS = "BFS", "Breadth First Search"
|
|
||||||
DFS = "DFS", "Depth First Search"
|
|
||||||
GREEDY = "GREEDY", "Greedy"
|
|
||||||
|
|
||||||
|
|
||||||
class Setting(EnviPathModel):
|
class Setting(EnviPathModel):
|
||||||
public = models.BooleanField(null=False, blank=False, default=False)
|
public = models.BooleanField(null=False, blank=False, default=False)
|
||||||
global_default = models.BooleanField(null=False, blank=False, default=False)
|
global_default = models.BooleanField(null=False, blank=False, default=False)
|
||||||
@ -3853,7 +3555,7 @@ class Setting(EnviPathModel):
|
|||||||
)
|
)
|
||||||
|
|
||||||
rule_packages = models.ManyToManyField(
|
rule_packages = models.ManyToManyField(
|
||||||
s.EPDB_PACKAGE_MODEL,
|
"Package",
|
||||||
verbose_name="Setting Rule Packages",
|
verbose_name="Setting Rule Packages",
|
||||||
related_name="setting_rule_packages",
|
related_name="setting_rule_packages",
|
||||||
blank=True,
|
blank=True,
|
||||||
@ -3865,12 +3567,6 @@ class Setting(EnviPathModel):
|
|||||||
null=True, blank=True, verbose_name="Setting Model Threshold", default=0.25
|
null=True, blank=True, verbose_name="Setting Model Threshold", default=0.25
|
||||||
)
|
)
|
||||||
|
|
||||||
expansion_scheme = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
choices=ExpansionSchemeChoice.choices,
|
|
||||||
default=ExpansionSchemeChoice.BFS,
|
|
||||||
)
|
|
||||||
|
|
||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/setting/{}".format(s.SERVER_URL, self.uuid)
|
return "{}/setting/{}".format(s.SERVER_URL, self.uuid)
|
||||||
|
|
||||||
@ -3905,48 +3601,33 @@ class Setting(EnviPathModel):
|
|||||||
rules = sorted(rules, key=lambda x: x.url)
|
rules = sorted(rules, key=lambda x: x.url)
|
||||||
return rules
|
return rules
|
||||||
|
|
||||||
def expand(self, pathway, current_node) -> Dict[str, Any]:
|
def expand(self, pathway, current_node):
|
||||||
res: Dict[str, Any] = defaultdict(list)
|
|
||||||
|
|
||||||
"""Decision Method whether to expand on a certain Node or not"""
|
"""Decision Method whether to expand on a certain Node or not"""
|
||||||
if pathway.num_nodes() >= self.max_nodes:
|
if pathway.num_nodes() >= self.max_nodes:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Pathway has {pathway.num_nodes()} Nodes which exceeds the limit of {self.max_nodes}"
|
f"Pathway has {pathway.num_nodes()} which exceeds the limit of {self.max_nodes}"
|
||||||
)
|
)
|
||||||
res["expansion_skipped"] = True
|
return []
|
||||||
return res
|
|
||||||
|
|
||||||
if pathway.depth() >= self.max_depth:
|
if pathway.depth() >= self.max_depth:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Pathway has reached depth {pathway.depth()} which exceeds the limit of {self.max_depth}"
|
f"Pathway has reached depth {pathway.depth()} which exceeds the limit of {self.max_depth}"
|
||||||
)
|
)
|
||||||
res["expansion_skipped"] = True
|
return []
|
||||||
return res
|
|
||||||
|
|
||||||
|
transformations = []
|
||||||
if self.model is not None:
|
if self.model is not None:
|
||||||
pred_results = self.model.predict(current_node.smiles)
|
pred_results = self.model.predict(current_node.smiles)
|
||||||
|
|
||||||
# Store whether there are results that may be removed as they are below
|
|
||||||
# the given threshold
|
|
||||||
if len(pred_results):
|
|
||||||
res["rule_triggered"] = True
|
|
||||||
|
|
||||||
for pred_result in pred_results:
|
for pred_result in pred_results:
|
||||||
if (
|
if pred_result.probability >= self.model_threshold:
|
||||||
len(pred_result.product_sets)
|
transformations.append(pred_result)
|
||||||
and pred_result.probability >= self.model_threshold
|
|
||||||
):
|
|
||||||
res["transformations"].append(pred_result)
|
|
||||||
else:
|
else:
|
||||||
for rule in self.applicable_rules:
|
for rule in self.applicable_rules:
|
||||||
tmp_products = rule.apply(current_node.smiles)
|
tmp_products = rule.apply(current_node.smiles)
|
||||||
if tmp_products:
|
if tmp_products:
|
||||||
res["transformations"].append(PredictionResult(tmp_products, 1.0, rule))
|
transformations.append(PredictionResult(tmp_products, 1.0, rule))
|
||||||
|
|
||||||
if len(res["transformations"]):
|
return transformations
|
||||||
res["rule_triggered"] = True
|
|
||||||
|
|
||||||
return res
|
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def make_global_default(self):
|
def make_global_default(self):
|
||||||
@ -3979,6 +3660,10 @@ class JobLog(TimeStampedModel):
|
|||||||
done_at = models.DateTimeField(null=True, blank=True, default=None)
|
done_at = models.DateTimeField(null=True, blank=True, default=None)
|
||||||
task_result = models.TextField(null=True, blank=True, default=None)
|
task_result = models.TextField(null=True, blank=True, default=None)
|
||||||
|
|
||||||
|
def check_for_update(self):
|
||||||
|
async_res = self.get_result()
|
||||||
|
new_status = async_res.state
|
||||||
|
|
||||||
TERMINAL_STATES = [
|
TERMINAL_STATES = [
|
||||||
"SUCCESS",
|
"SUCCESS",
|
||||||
"FAILURE",
|
"FAILURE",
|
||||||
@ -3986,22 +3671,12 @@ class JobLog(TimeStampedModel):
|
|||||||
"IGNORED",
|
"IGNORED",
|
||||||
]
|
]
|
||||||
|
|
||||||
def is_in_terminal_state(self):
|
if new_status != self.status and new_status in TERMINAL_STATES:
|
||||||
return self.status in self.TERMINAL_STATES
|
|
||||||
|
|
||||||
def check_for_update(self):
|
|
||||||
if self.is_in_terminal_state():
|
|
||||||
return
|
|
||||||
|
|
||||||
async_res = self.get_result()
|
|
||||||
new_status = async_res.state
|
|
||||||
|
|
||||||
if new_status != self.status and new_status in self.TERMINAL_STATES:
|
|
||||||
self.status = new_status
|
self.status = new_status
|
||||||
self.done_at = async_res.date_done
|
self.done_at = async_res.date_done
|
||||||
|
|
||||||
if new_status == "SUCCESS":
|
if new_status == "SUCCESS":
|
||||||
self.task_result = str(async_res.result) if async_res.result else None
|
self.task_result = async_res.result
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -4012,18 +3687,3 @@ class JobLog(TimeStampedModel):
|
|||||||
from celery.result import AsyncResult
|
from celery.result import AsyncResult
|
||||||
|
|
||||||
return AsyncResult(str(self.task_id))
|
return AsyncResult(str(self.task_id))
|
||||||
|
|
||||||
def parsed_result(self):
|
|
||||||
if not self.is_in_terminal_state() or self.task_result is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
import ast
|
|
||||||
|
|
||||||
if self.job_name == "engineer_pathways":
|
|
||||||
return ast.literal_eval(self.task_result)
|
|
||||||
return self.task_result
|
|
||||||
|
|
||||||
def is_result_downloadable(self):
|
|
||||||
downloadable = ["batch_predict"]
|
|
||||||
|
|
||||||
return self.job_name in downloadable
|
|
||||||
|
|||||||
168
epdb/tasks.py
168
epdb/tasks.py
@ -6,18 +6,14 @@ from uuid import uuid4
|
|||||||
|
|
||||||
from celery import shared_task
|
from celery import shared_task
|
||||||
from celery.utils.functional import LRUCache
|
from celery.utils.functional import LRUCache
|
||||||
from django.conf import settings as s
|
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from epdb.logic import SPathway
|
from epdb.logic import SPathway
|
||||||
from epdb.models import Edge, EPModel, JobLog, Node, Pathway, Rule, Setting, User
|
from epdb.models import Edge, EPModel, JobLog, Node, Package, Pathway, Rule, Setting, User
|
||||||
from utilities.chem import FormatConverter
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
ML_CACHE = LRUCache(3) # Cache the three most recent ML models to reduce load times.
|
ML_CACHE = LRUCache(3) # Cache the three most recent ML models to reduce load times.
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
|
||||||
|
|
||||||
|
|
||||||
def get_ml_model(model_pk: int):
|
def get_ml_model(model_pk: int):
|
||||||
if model_pk not in ML_CACHE:
|
if model_pk not in ML_CACHE:
|
||||||
@ -37,7 +33,7 @@ def dispatch_eager(user: "User", job: Callable, *args, **kwargs):
|
|||||||
log.task_result = str(x) if x else None
|
log.task_result = str(x) if x else None
|
||||||
log.save()
|
log.save()
|
||||||
|
|
||||||
return log, x
|
return x
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
raise e
|
raise e
|
||||||
@ -53,7 +49,7 @@ def dispatch(user: "User", job: Callable, *args, **kwargs):
|
|||||||
log.status = "INITIAL"
|
log.status = "INITIAL"
|
||||||
log.save()
|
log.save()
|
||||||
|
|
||||||
return log
|
return x.result
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.exception(e)
|
logger.exception(e)
|
||||||
raise e
|
raise e
|
||||||
@ -140,25 +136,14 @@ def predict(
|
|||||||
pred_setting_pk: int,
|
pred_setting_pk: int,
|
||||||
limit: Optional[int] = None,
|
limit: Optional[int] = None,
|
||||||
node_pk: Optional[int] = None,
|
node_pk: Optional[int] = None,
|
||||||
setting_overrides: Optional[dict] = None,
|
|
||||||
) -> Pathway:
|
) -> 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)
|
||||||
|
|
||||||
if setting_overrides:
|
|
||||||
for k, v in setting_overrides.items():
|
|
||||||
setattr(setting, k, v)
|
|
||||||
|
|
||||||
# If the setting has a model add/restore it from the cache
|
# If the setting has a model add/restore it from the cache
|
||||||
if setting.model is not None:
|
if setting.model is not None:
|
||||||
setting.model = get_ml_model(setting.model.pk)
|
setting.model = get_ml_model(setting.model.pk)
|
||||||
|
|
||||||
kv = {"status": "running"}
|
pw.kv.update(**{"status": "running"})
|
||||||
|
|
||||||
if setting_overrides:
|
|
||||||
kv["setting_overrides"] = setting_overrides
|
|
||||||
|
|
||||||
pw.kv.update(**kv)
|
|
||||||
pw.save()
|
pw.save()
|
||||||
|
|
||||||
if JobLog.objects.filter(task_id=self.request.id).exists():
|
if JobLog.objects.filter(task_id=self.request.id).exists():
|
||||||
@ -183,12 +168,10 @@ def predict(
|
|||||||
spw = SPathway.from_pathway(pw)
|
spw = SPathway.from_pathway(pw)
|
||||||
spw.predict_step(from_node=n)
|
spw.predict_step(from_node=n)
|
||||||
else:
|
else:
|
||||||
spw = SPathway(prediction_setting=setting, persist=pw)
|
raise ValueError("Neither limit nor node_pk given!")
|
||||||
spw.predict()
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
pw.kv.update({"status": "failed"})
|
pw.kv.update({"status": "failed"})
|
||||||
pw.kv.update(**{"error": str(e)})
|
|
||||||
pw.save()
|
pw.save()
|
||||||
|
|
||||||
if JobLog.objects.filter(task_id=self.request.id).exists():
|
if JobLog.objects.filter(task_id=self.request.id).exists():
|
||||||
@ -298,144 +281,3 @@ def identify_missing_rules(
|
|||||||
buffer.seek(0)
|
buffer.seek(0)
|
||||||
|
|
||||||
return buffer.getvalue()
|
return buffer.getvalue()
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True, queue="background")
|
|
||||||
def engineer_pathways(self, pw_pks: List[int], setting_pk: int, target_package_pk: int):
|
|
||||||
from utilities.misc import PathwayUtils
|
|
||||||
|
|
||||||
setting = Setting.objects.get(pk=setting_pk)
|
|
||||||
# Temporarily set model_threshold to 0.0 to keep all tps
|
|
||||||
setting.model_threshold = 0.0
|
|
||||||
|
|
||||||
target = Package.objects.get(pk=target_package_pk)
|
|
||||||
|
|
||||||
intermediate_pathways = []
|
|
||||||
predicted_pathways = []
|
|
||||||
|
|
||||||
for pw in Pathway.objects.filter(pk__in=pw_pks):
|
|
||||||
pu = PathwayUtils(pw)
|
|
||||||
|
|
||||||
eng_pw, node_to_snode_mapping, intermediates = pu.engineer(setting)
|
|
||||||
|
|
||||||
# If we've found intermediates, do the following
|
|
||||||
# - Get a copy of the original pathway and add intermediates
|
|
||||||
# - Store the predicted pathway for further investigation
|
|
||||||
if len(intermediates):
|
|
||||||
copy_mapping = {}
|
|
||||||
copied_pw = pw.copy(target, copy_mapping)
|
|
||||||
copied_pw.name = f"{copied_pw.name} (Engineered)"
|
|
||||||
copied_pw.description = f"The original Pathway can be found here: {pw.url}"
|
|
||||||
copied_pw.save()
|
|
||||||
|
|
||||||
for inter in intermediates:
|
|
||||||
start = copy_mapping[inter[0]]
|
|
||||||
end = copy_mapping[inter[1]]
|
|
||||||
start_snode = inter[2]
|
|
||||||
end_snode = inter[3]
|
|
||||||
for idx, intermediate_edge in enumerate(inter[4]):
|
|
||||||
smiles_to_node = {}
|
|
||||||
|
|
||||||
snodes_to_create = list(
|
|
||||||
set(intermediate_edge.educts + intermediate_edge.products)
|
|
||||||
)
|
|
||||||
|
|
||||||
for snode in snodes_to_create:
|
|
||||||
if snode == start_snode or snode == end_snode:
|
|
||||||
smiles_to_node[snode.smiles] = start if snode == start_snode else end
|
|
||||||
continue
|
|
||||||
|
|
||||||
if snode.smiles not in smiles_to_node:
|
|
||||||
n = Node.create(copied_pw, smiles=snode.smiles, depth=snode.depth)
|
|
||||||
# Used in viz to highlight intermediates
|
|
||||||
n.kv.update({"is_engineered_intermediate": True})
|
|
||||||
n.save()
|
|
||||||
smiles_to_node[snode.smiles] = n
|
|
||||||
|
|
||||||
Edge.create(
|
|
||||||
copied_pw,
|
|
||||||
[smiles_to_node[educt.smiles] for educt in intermediate_edge.educts],
|
|
||||||
[smiles_to_node[product.smiles] for product in intermediate_edge.products],
|
|
||||||
rule=intermediate_edge.rule,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Persist the predicted pathway
|
|
||||||
pred_pw = pu.spathway_to_pathway(target, eng_pw, name=f"{pw.name} (Predicted)")
|
|
||||||
|
|
||||||
intermediate_pathways.append(copied_pw.url)
|
|
||||||
predicted_pathways.append(pred_pw.url)
|
|
||||||
|
|
||||||
return intermediate_pathways, predicted_pathways
|
|
||||||
|
|
||||||
|
|
||||||
@shared_task(bind=True, queue="background")
|
|
||||||
def batch_predict(
|
|
||||||
self,
|
|
||||||
substrates: List[str] | List[List[str]],
|
|
||||||
prediction_setting_pk: int,
|
|
||||||
target_package_pk: int,
|
|
||||||
num_tps: int = 50,
|
|
||||||
):
|
|
||||||
target_package = Package.objects.get(pk=target_package_pk)
|
|
||||||
prediction_setting = Setting.objects.get(pk=prediction_setting_pk)
|
|
||||||
|
|
||||||
if len(substrates) == 0:
|
|
||||||
raise ValueError("No substrates given!")
|
|
||||||
|
|
||||||
is_pair = isinstance(substrates[0], list)
|
|
||||||
|
|
||||||
substrate_and_names = []
|
|
||||||
if not is_pair:
|
|
||||||
for sub in substrates:
|
|
||||||
substrate_and_names.append([sub, None])
|
|
||||||
else:
|
|
||||||
substrate_and_names = substrates
|
|
||||||
|
|
||||||
# Check prerequisite that we can standardize all substrates
|
|
||||||
standardized_substrates_and_smiles = []
|
|
||||||
for substrate in substrate_and_names:
|
|
||||||
try:
|
|
||||||
stand_smiles = FormatConverter.standardize(substrate[0])
|
|
||||||
standardized_substrates_and_smiles.append([stand_smiles, substrate[1]])
|
|
||||||
except ValueError:
|
|
||||||
raise ValueError(
|
|
||||||
f'Pathway prediction failed as standardization of SMILES "{substrate}" failed!'
|
|
||||||
)
|
|
||||||
|
|
||||||
pathways = []
|
|
||||||
|
|
||||||
for pair in standardized_substrates_and_smiles:
|
|
||||||
pw = Pathway.create(
|
|
||||||
target_package,
|
|
||||||
pair[0],
|
|
||||||
name=pair[1],
|
|
||||||
predicted=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# set mode and setting
|
|
||||||
pw.setting = prediction_setting
|
|
||||||
pw.kv.update({"mode": "predict"})
|
|
||||||
pw.save()
|
|
||||||
|
|
||||||
predict(
|
|
||||||
pw.pk,
|
|
||||||
prediction_setting.pk,
|
|
||||||
limit=None,
|
|
||||||
setting_overrides={
|
|
||||||
"max_nodes": num_tps,
|
|
||||||
"max_depth": num_tps,
|
|
||||||
"model_threshold": 0.001,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
pathways.append(pw)
|
|
||||||
|
|
||||||
buffer = io.StringIO()
|
|
||||||
|
|
||||||
for idx, pw in enumerate(pathways):
|
|
||||||
# Carry out header only for the first pathway
|
|
||||||
buffer.write(pw.to_csv(include_header=idx == 0, include_pathway_url=True))
|
|
||||||
|
|
||||||
buffer.seek(0)
|
|
||||||
|
|
||||||
return buffer.getvalue()
|
|
||||||
|
|||||||
@ -49,7 +49,6 @@ urlpatterns = [
|
|||||||
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"),
|
||||||
re_path(r"^predict$", v.predict_pathway, name="predict_pathway"),
|
re_path(r"^predict$", v.predict_pathway, name="predict_pathway"),
|
||||||
re_path(r"^batch-predict$", v.batch_predict_pathway, name="batch_predict_pathway"),
|
|
||||||
# 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
|
||||||
@ -197,8 +196,7 @@ urlpatterns = [
|
|||||||
re_path(r"^indigo/dearomatize$", v.dearomatize, name="indigo_dearomatize"),
|
re_path(r"^indigo/dearomatize$", v.dearomatize, name="indigo_dearomatize"),
|
||||||
re_path(r"^indigo/layout$", v.layout, name="indigo_layout"),
|
re_path(r"^indigo/layout$", v.layout, name="indigo_layout"),
|
||||||
re_path(r"^depict$", v.depict, name="depict"),
|
re_path(r"^depict$", v.depict, name="depict"),
|
||||||
path("jobs", v.jobs, name="jobs"),
|
re_path(r"^jobs", v.jobs, name="jobs"),
|
||||||
path("jobs/<uuid:job_uuid>", v.job, name="job detail"),
|
|
||||||
# OAuth Stuff
|
# OAuth Stuff
|
||||||
path("o/userinfo/", v.userinfo, name="oauth_userinfo"),
|
path("o/userinfo/", v.userinfo, name="oauth_userinfo"),
|
||||||
# Static Pages
|
# Static Pages
|
||||||
|
|||||||
602
epdb/views.py
602
epdb/views.py
@ -1,63 +1,58 @@
|
|||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from typing import Any, Dict, List
|
from typing import List, Dict, Any
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
import nh3
|
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.contrib.auth import get_user_model
|
from django.contrib.auth import get_user_model
|
||||||
from django.core.exceptions import BadRequest, PermissionDenied
|
from django.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed, JsonResponse
|
from django.shortcuts import render, redirect
|
||||||
from django.shortcuts import redirect, render
|
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
from django.views.decorators.csrf import csrf_exempt
|
||||||
from envipy_additional_information import NAME_MAPPING
|
from envipy_additional_information import NAME_MAPPING
|
||||||
from oauth2_provider.decorators import protected_resource
|
from oauth2_provider.decorators import protected_resource
|
||||||
|
import nh3
|
||||||
|
|
||||||
from utilities.chem import FormatConverter, IndigoUtils
|
from utilities.chem import FormatConverter, IndigoUtils
|
||||||
from utilities.decorators import package_permission_required
|
from utilities.decorators import package_permission_required
|
||||||
from utilities.misc import HTMLGenerator
|
from utilities.misc import HTMLGenerator
|
||||||
|
|
||||||
from .logic import (
|
from .logic import (
|
||||||
EPDBURLParser,
|
|
||||||
GroupManager,
|
GroupManager,
|
||||||
PackageManager,
|
PackageManager,
|
||||||
SearchManager,
|
|
||||||
SettingManager,
|
|
||||||
UserManager,
|
UserManager,
|
||||||
|
SettingManager,
|
||||||
|
SearchManager,
|
||||||
|
EPDBURLParser,
|
||||||
)
|
)
|
||||||
from .models import (
|
from .models import (
|
||||||
APIToken,
|
Package,
|
||||||
Compound,
|
|
||||||
CompoundStructure,
|
|
||||||
Edge,
|
|
||||||
EnviFormer,
|
|
||||||
EnzymeLink,
|
|
||||||
EPModel,
|
|
||||||
ExternalDatabase,
|
|
||||||
ExternalIdentifier,
|
|
||||||
Group,
|
|
||||||
GroupPackagePermission,
|
GroupPackagePermission,
|
||||||
JobLog,
|
Group,
|
||||||
License,
|
CompoundStructure,
|
||||||
MLRelativeReasoning,
|
Compound,
|
||||||
Node,
|
|
||||||
Pathway,
|
|
||||||
Permission,
|
|
||||||
Reaction,
|
Reaction,
|
||||||
Rule,
|
Rule,
|
||||||
|
Pathway,
|
||||||
|
Node,
|
||||||
|
EPModel,
|
||||||
|
EnviFormer,
|
||||||
|
MLRelativeReasoning,
|
||||||
RuleBasedRelativeReasoning,
|
RuleBasedRelativeReasoning,
|
||||||
Scenario,
|
Scenario,
|
||||||
SimpleAmbitRule,
|
SimpleAmbitRule,
|
||||||
User,
|
APIToken,
|
||||||
UserPackagePermission,
|
UserPackagePermission,
|
||||||
ExpansionSchemeChoice,
|
Permission,
|
||||||
|
License,
|
||||||
|
User,
|
||||||
|
Edge,
|
||||||
|
ExternalDatabase,
|
||||||
|
ExternalIdentifier,
|
||||||
|
EnzymeLink,
|
||||||
|
JobLog,
|
||||||
)
|
)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
|
||||||
|
|
||||||
|
|
||||||
def log_post_params(request):
|
def log_post_params(request):
|
||||||
if s.DEBUG:
|
if s.DEBUG:
|
||||||
@ -65,26 +60,6 @@ def log_post_params(request):
|
|||||||
logger.debug(f"{k}\t{v}")
|
logger.debug(f"{k}\t{v}")
|
||||||
|
|
||||||
|
|
||||||
def get_error_handler_context(request, for_user=None) -> Dict[str, Any]:
|
|
||||||
current_user = _anonymous_or_real(request)
|
|
||||||
|
|
||||||
if for_user:
|
|
||||||
current_user = for_user
|
|
||||||
|
|
||||||
ctx = {
|
|
||||||
"title": "enviPath",
|
|
||||||
"meta": {
|
|
||||||
"site_id": s.MATOMO_SITE_ID,
|
|
||||||
"version": "0.0.1",
|
|
||||||
"server_url": s.SERVER_URL,
|
|
||||||
"user": current_user,
|
|
||||||
"enabled_features": s.FLAGS,
|
|
||||||
"debug": s.DEBUG,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
return ctx
|
|
||||||
|
|
||||||
|
|
||||||
def error(request, message: str, detail: str, code: int = 400):
|
def error(request, message: str, detail: str, code: int = 400):
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
error_context = {
|
error_context = {
|
||||||
@ -99,48 +74,6 @@ def error(request, message: str, detail: str, code: int = 400):
|
|||||||
return render(request, "errors/error.html", context, status=code)
|
return render(request, "errors/error.html", context, status=code)
|
||||||
|
|
||||||
|
|
||||||
def handler400(request, exception):
|
|
||||||
"""Custom 400 Bad Request error handler"""
|
|
||||||
context = get_error_handler_context(request)
|
|
||||||
context["public_mode"] = True
|
|
||||||
return render(request, "errors/400_bad_request.html", context, status=400)
|
|
||||||
|
|
||||||
|
|
||||||
def handler403(request, exception):
|
|
||||||
"""Custom 403 Forbidden error handler"""
|
|
||||||
context = get_error_handler_context(request)
|
|
||||||
context["public_mode"] = True
|
|
||||||
return render(request, "errors/403_access_denied.html", context, status=403)
|
|
||||||
|
|
||||||
|
|
||||||
def handler404(request, exception):
|
|
||||||
"""Custom 404 Not Found error handler"""
|
|
||||||
context = get_error_handler_context(request)
|
|
||||||
context["public_mode"] = True
|
|
||||||
return render(request, "errors/404_not_found.html", context, status=404)
|
|
||||||
|
|
||||||
|
|
||||||
def handler500(request):
|
|
||||||
"""Custom 500 Internal Server Error handler"""
|
|
||||||
context = get_error_handler_context(request)
|
|
||||||
|
|
||||||
error_context = {}
|
|
||||||
error_context["error_message"] = "Internal Server Error"
|
|
||||||
error_context["error_detail"] = "An unexpected error occurred. Please try again later."
|
|
||||||
|
|
||||||
if request.headers.get("Accept") == "application/json":
|
|
||||||
return JsonResponse(error_context, status=500)
|
|
||||||
|
|
||||||
context["public_mode"] = True
|
|
||||||
context["error_code"] = 500
|
|
||||||
context["error_description"] = (
|
|
||||||
"We encountered an unexpected error while processing your request. Our team has been notified and is working to resolve the issue."
|
|
||||||
)
|
|
||||||
context.update(**error_context)
|
|
||||||
|
|
||||||
return render(request, "errors/error.html", context, status=500)
|
|
||||||
|
|
||||||
|
|
||||||
def login(request):
|
def login(request):
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
|
|
||||||
@ -150,7 +83,8 @@ def login(request):
|
|||||||
return render(request, "static/login.html", context)
|
return render(request, "static/login.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
from django.contrib.auth import authenticate, login
|
from django.contrib.auth import authenticate
|
||||||
|
from django.contrib.auth import login
|
||||||
|
|
||||||
username = request.POST.get("username").strip()
|
username = request.POST.get("username").strip()
|
||||||
if username != request.POST.get("username"):
|
if username != request.POST.get("username"):
|
||||||
@ -257,8 +191,8 @@ def register(request):
|
|||||||
|
|
||||||
|
|
||||||
def editable(request, user):
|
def editable(request, user):
|
||||||
# if user.is_superuser:
|
if user.is_superuser:
|
||||||
# return True
|
return True
|
||||||
|
|
||||||
url = request.build_absolute_uri(request.path)
|
url = request.build_absolute_uri(request.path)
|
||||||
if PackageManager.is_package_url(url):
|
if PackageManager.is_package_url(url):
|
||||||
@ -322,7 +256,7 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def _anonymous_or_real(request):
|
def _anonymous_or_real(request):
|
||||||
if request.user and (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")
|
||||||
|
|
||||||
@ -440,18 +374,6 @@ def predict_pathway(request):
|
|||||||
return render(request, "predict_pathway.html", context)
|
return render(request, "predict_pathway.html", context)
|
||||||
|
|
||||||
|
|
||||||
def batch_predict_pathway(request):
|
|
||||||
"""Top-level predict pathway view using user's default package."""
|
|
||||||
if request.method != "GET":
|
|
||||||
return HttpResponseNotAllowed(["GET"])
|
|
||||||
|
|
||||||
context = get_base_context(request)
|
|
||||||
context["title"] = "enviPath - Batch Predict Pathway"
|
|
||||||
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
|
||||||
|
|
||||||
return render(request, "batch_predict_pathway.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
@package_permission_required()
|
@package_permission_required()
|
||||||
def package_predict_pathway(request, package_uuid):
|
def package_predict_pathway(request, package_uuid):
|
||||||
"""Package-specific predict pathway view."""
|
"""Package-specific predict pathway view."""
|
||||||
@ -474,15 +396,20 @@ def packages(request):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
context["title"] = "enviPath - Packages"
|
context["title"] = "enviPath - Packages"
|
||||||
|
|
||||||
|
context["object_type"] = "package"
|
||||||
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
||||||
|
context["meta"]["can_edit"] = True
|
||||||
|
|
||||||
# Context for paginated template
|
reviewed_package_qs = Package.objects.filter(reviewed=True).order_by("created")
|
||||||
context["entity_type"] = "package"
|
unreviewed_package_qs = PackageManager.get_all_readable_packages(current_user).order_by(
|
||||||
context["api_endpoint"] = "/api/v1/packages/"
|
"name"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
)
|
||||||
context["list_title"] = "packages"
|
|
||||||
|
|
||||||
return render(request, "collections/packages_paginated.html", context)
|
context["reviewed_objects"] = reviewed_package_qs
|
||||||
|
context["unreviewed_objects"] = unreviewed_package_qs
|
||||||
|
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
if hidden := request.POST.get("hidden", None):
|
if hidden := request.POST.get("hidden", None):
|
||||||
@ -528,16 +455,29 @@ def compounds(request):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
context["title"] = "enviPath - Compounds"
|
context["title"] = "enviPath - Compounds"
|
||||||
|
|
||||||
|
context["object_type"] = "compound"
|
||||||
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
||||||
|
|
||||||
# Context for paginated template
|
reviewed_compound_qs = Compound.objects.none()
|
||||||
context["entity_type"] = "compound"
|
|
||||||
context["api_endpoint"] = "/api/v1/compounds/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_mode"] = "tabbed"
|
|
||||||
context["list_title"] = "compounds"
|
|
||||||
|
|
||||||
return render(request, "collections/compounds_paginated.html", context)
|
for p in PackageManager.get_reviewed_packages():
|
||||||
|
reviewed_compound_qs |= Compound.objects.filter(package=p)
|
||||||
|
|
||||||
|
reviewed_compound_qs = reviewed_compound_qs.order_by("name")
|
||||||
|
|
||||||
|
if request.GET.get("all"):
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{"name": pw.name, "url": pw.url, "reviewed": True}
|
||||||
|
for pw in reviewed_compound_qs
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context["reviewed_objects"] = reviewed_compound_qs
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
# delegate to default package
|
# delegate to default package
|
||||||
@ -553,19 +493,32 @@ def rules(request):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
context["title"] = "enviPath - Rules"
|
context["title"] = "enviPath - Rules"
|
||||||
|
|
||||||
|
context["object_type"] = "rule"
|
||||||
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
||||||
context["breadcrumbs"] = [
|
context["breadcrumbs"] = [
|
||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Rule": s.SERVER_URL + "/rule"},
|
{"Rule": s.SERVER_URL + "/rule"},
|
||||||
]
|
]
|
||||||
|
reviewed_rule_qs = Rule.objects.none()
|
||||||
|
|
||||||
# Context for paginated template
|
for p in PackageManager.get_reviewed_packages():
|
||||||
context["entity_type"] = "rule"
|
reviewed_rule_qs |= Rule.objects.filter(package=p)
|
||||||
context["api_endpoint"] = "/api/v1/rules/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "rules"
|
|
||||||
|
|
||||||
return render(request, "collections/rules_paginated.html", context)
|
reviewed_rule_qs = reviewed_rule_qs.order_by("name")
|
||||||
|
|
||||||
|
if request.GET.get("all"):
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{"name": pw.name, "url": pw.url, "reviewed": True}
|
||||||
|
for pw in reviewed_rule_qs
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context["reviewed_objects"] = reviewed_rule_qs
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
# delegate to default package
|
# delegate to default package
|
||||||
@ -581,19 +534,32 @@ def reactions(request):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
context["title"] = "enviPath - Reactions"
|
context["title"] = "enviPath - Reactions"
|
||||||
|
|
||||||
|
context["object_type"] = "reaction"
|
||||||
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
||||||
context["breadcrumbs"] = [
|
context["breadcrumbs"] = [
|
||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Reaction": s.SERVER_URL + "/reaction"},
|
{"Reaction": s.SERVER_URL + "/reaction"},
|
||||||
]
|
]
|
||||||
|
reviewed_reaction_qs = Reaction.objects.none()
|
||||||
|
|
||||||
# Context for paginated template
|
for p in PackageManager.get_reviewed_packages():
|
||||||
context["entity_type"] = "reaction"
|
reviewed_reaction_qs |= Reaction.objects.filter(package=p).order_by("name")
|
||||||
context["api_endpoint"] = "/api/v1/reactions/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "reactions"
|
|
||||||
|
|
||||||
return render(request, "collections/reactions_paginated.html", context)
|
reviewed_reaction_qs = reviewed_reaction_qs.order_by("name")
|
||||||
|
|
||||||
|
if request.GET.get("all"):
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{"name": pw.name, "url": pw.url, "reviewed": True}
|
||||||
|
for pw in reviewed_reaction_qs
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context["reviewed_objects"] = reviewed_reaction_qs
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
# delegate to default package
|
# delegate to default package
|
||||||
@ -609,19 +575,33 @@ def pathways(request):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
context["title"] = "enviPath - Pathways"
|
context["title"] = "enviPath - Pathways"
|
||||||
|
|
||||||
|
context["object_type"] = "pathway"
|
||||||
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
||||||
context["breadcrumbs"] = [
|
context["breadcrumbs"] = [
|
||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Pathway": s.SERVER_URL + "/pathway"},
|
{"Pathway": s.SERVER_URL + "/pathway"},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Context for paginated template
|
reviewed_pathway_qs = Pathway.objects.none()
|
||||||
context["entity_type"] = "pathway"
|
|
||||||
context["api_endpoint"] = "/api/v1/pathways/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "pathways"
|
|
||||||
|
|
||||||
return render(request, "collections/pathways_paginated.html", context)
|
for p in PackageManager.get_reviewed_packages():
|
||||||
|
reviewed_pathway_qs |= Pathway.objects.filter(package=p).order_by("name")
|
||||||
|
|
||||||
|
reviewed_pathway_qs = reviewed_pathway_qs.order_by("name")
|
||||||
|
|
||||||
|
if request.GET.get("all"):
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{"name": pw.name, "url": pw.url, "reviewed": True}
|
||||||
|
for pw in reviewed_pathway_qs
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context["reviewed_objects"] = reviewed_pathway_qs
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
# delegate to default package
|
# delegate to default package
|
||||||
@ -645,13 +625,25 @@ def scenarios(request):
|
|||||||
{"Scenario": s.SERVER_URL + "/scenario"},
|
{"Scenario": s.SERVER_URL + "/scenario"},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Context for paginated template
|
reviewed_scenario_qs = Scenario.objects.none()
|
||||||
context["entity_type"] = "scenario"
|
|
||||||
context["api_endpoint"] = "/api/v1/scenarios/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "scenarios"
|
|
||||||
|
|
||||||
return render(request, "collections/scenarios_paginated.html", context)
|
for p in PackageManager.get_reviewed_packages():
|
||||||
|
reviewed_scenario_qs |= Scenario.objects.filter(package=p).order_by("name")
|
||||||
|
|
||||||
|
reviewed_scenario_qs = reviewed_scenario_qs.order_by("name")
|
||||||
|
|
||||||
|
if request.GET.get("all"):
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{"name": s.name, "url": s.url, "reviewed": True}
|
||||||
|
for s in reviewed_scenario_qs
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context["reviewed_objects"] = reviewed_scenario_qs
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
# delegate to default package
|
# delegate to default package
|
||||||
@ -666,28 +658,42 @@ def models(request):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
context["title"] = "enviPath - Models"
|
context["title"] = "enviPath - Models"
|
||||||
|
|
||||||
|
context["object_type"] = "model"
|
||||||
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
context["meta"]["current_package"] = context["meta"]["user"].default_package
|
||||||
context["breadcrumbs"] = [
|
context["breadcrumbs"] = [
|
||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Model": s.SERVER_URL + "/model"},
|
{"Model": s.SERVER_URL + "/model"},
|
||||||
]
|
]
|
||||||
|
|
||||||
# Keep model_types for potential modal/action use
|
|
||||||
context["model_types"] = {
|
context["model_types"] = {
|
||||||
"ML Relative Reasoning": "ml-relative-reasoning",
|
"ML Relative Reasoning": "ml-relative-reasoning",
|
||||||
"Rule Based Relative Reasoning": "rule-based-relative-reasoning",
|
"Rule Based Relative Reasoning": "rule-based-relative-reasoning",
|
||||||
"EnviFormer": "enviformer",
|
"EnviFormer": "enviformer",
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
context["model_types"][v.display()] = k
|
context["model_types"][v.display()] = k
|
||||||
|
|
||||||
# Context for paginated template
|
reviewed_model_qs = EPModel.objects.none()
|
||||||
context["entity_type"] = "model"
|
|
||||||
context["api_endpoint"] = "/api/v1/models/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "models"
|
|
||||||
|
|
||||||
return render(request, "collections/models_paginated.html", context)
|
for p in PackageManager.get_reviewed_packages():
|
||||||
|
reviewed_model_qs |= EPModel.objects.filter(package=p).order_by("name")
|
||||||
|
|
||||||
|
reviewed_model_qs = reviewed_model_qs.order_by("name")
|
||||||
|
|
||||||
|
if request.GET.get("all"):
|
||||||
|
return JsonResponse(
|
||||||
|
{
|
||||||
|
"objects": [
|
||||||
|
{"name": pw.name, "url": pw.url, "reviewed": True}
|
||||||
|
for pw in reviewed_model_qs
|
||||||
|
]
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
context["reviewed_objects"] = reviewed_model_qs
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
current_user = _anonymous_or_real(request)
|
current_user = _anonymous_or_real(request)
|
||||||
@ -764,10 +770,6 @@ def package_models(request, package_uuid):
|
|||||||
context["meta"]["current_package"] = current_package
|
context["meta"]["current_package"] = current_package
|
||||||
context["object_type"] = "model"
|
context["object_type"] = "model"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "model")
|
context["breadcrumbs"] = breadcrumbs(current_package, "model")
|
||||||
context["entity_type"] = "model"
|
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/model/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "models"
|
|
||||||
|
|
||||||
reviewed_model_qs = EPModel.objects.none()
|
reviewed_model_qs = EPModel.objects.none()
|
||||||
unreviewed_model_qs = EPModel.objects.none()
|
unreviewed_model_qs = EPModel.objects.none()
|
||||||
@ -789,6 +791,9 @@ def package_models(request, package_uuid):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
context["reviewed_objects"] = reviewed_model_qs
|
||||||
|
context["unreviewed_objects"] = unreviewed_model_qs
|
||||||
|
|
||||||
context["model_types"] = {
|
context["model_types"] = {
|
||||||
"ML Relative Reasoning": "mlrr",
|
"ML Relative Reasoning": "mlrr",
|
||||||
"Rule Based Relative Reasoning": "rbrr",
|
"Rule Based Relative Reasoning": "rbrr",
|
||||||
@ -801,7 +806,7 @@ def package_models(request, package_uuid):
|
|||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
context["model_types"][v.display()] = k
|
context["model_types"][v.display()] = k
|
||||||
|
|
||||||
return render(request, "collections/models_paginated.html", context)
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
log_post_params(request)
|
log_post_params(request)
|
||||||
@ -867,7 +872,7 @@ def package_models(request, package_uuid):
|
|||||||
request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
|
request, "Invalid model type.", f'Model type "{model_type}" is not supported."'
|
||||||
)
|
)
|
||||||
|
|
||||||
from .tasks import build_model, dispatch
|
from .tasks import dispatch, build_model
|
||||||
|
|
||||||
dispatch(current_user, build_model, mod.pk)
|
dispatch(current_user, build_model, mod.pk)
|
||||||
|
|
||||||
@ -892,20 +897,19 @@ def package_model(request, package_uuid, model_uuid):
|
|||||||
# Check if smiles is non empty and valid
|
# Check if smiles is non empty and valid
|
||||||
if smiles == "":
|
if smiles == "":
|
||||||
return JsonResponse({"error": "Received empty SMILES"}, status=400)
|
return JsonResponse({"error": "Received empty SMILES"}, status=400)
|
||||||
stereo = FormatConverter.has_stereo(smiles)
|
|
||||||
try:
|
try:
|
||||||
stand_smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
stand_smiles = FormatConverter.standardize(smiles)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return JsonResponse({"error": f'"{smiles}" is not a valid SMILES'}, status=400)
|
return JsonResponse({"error": f'"{smiles}" is not a valid SMILES'}, status=400)
|
||||||
|
|
||||||
if classify:
|
if classify:
|
||||||
from epdb.tasks import dispatch_eager, predict_simple
|
from epdb.tasks import dispatch_eager, predict_simple
|
||||||
|
|
||||||
_, pred_res = dispatch_eager(
|
res = dispatch_eager(current_user, predict_simple, current_model.pk, stand_smiles)
|
||||||
current_user, predict_simple, current_model.pk, stand_smiles
|
|
||||||
)
|
|
||||||
|
|
||||||
res = {"pred": [], "stereo": stereo}
|
pred_res = current_model.predict(stand_smiles)
|
||||||
|
res = []
|
||||||
|
|
||||||
for pr in pred_res:
|
for pr in pred_res:
|
||||||
if len(pr) > 0:
|
if len(pr) > 0:
|
||||||
@ -914,7 +918,7 @@ def package_model(request, package_uuid, model_uuid):
|
|||||||
logger.debug(f"Checking {prod_set}")
|
logger.debug(f"Checking {prod_set}")
|
||||||
products.append(tuple([x for x in prod_set]))
|
products.append(tuple([x for x in prod_set]))
|
||||||
|
|
||||||
res["pred"].append(
|
res.append(
|
||||||
{
|
{
|
||||||
"products": list(set(products)),
|
"products": list(set(products)),
|
||||||
"probability": pr.probability,
|
"probability": pr.probability,
|
||||||
@ -1064,7 +1068,9 @@ def package(request, package_uuid):
|
|||||||
return redirect(s.SERVER_URL + "/package")
|
return redirect(s.SERVER_URL + "/package")
|
||||||
elif hidden == "publish-package":
|
elif hidden == "publish-package":
|
||||||
for g in Group.objects.filter(public=True):
|
for g in Group.objects.filter(public=True):
|
||||||
PackageManager.grant_read(current_user, current_package, g)
|
PackageManager.update_permissions(
|
||||||
|
current_user, current_package, g, Permission.READ[0]
|
||||||
|
)
|
||||||
return redirect(current_package.url)
|
return redirect(current_package.url)
|
||||||
elif hidden == "copy":
|
elif hidden == "copy":
|
||||||
object_to_copy = request.POST.get("object_to_copy")
|
object_to_copy = request.POST.get("object_to_copy")
|
||||||
@ -1159,11 +1165,6 @@ def package_compounds(request, package_uuid):
|
|||||||
context["meta"]["current_package"] = current_package
|
context["meta"]["current_package"] = current_package
|
||||||
context["object_type"] = "compound"
|
context["object_type"] = "compound"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "compound")
|
context["breadcrumbs"] = breadcrumbs(current_package, "compound")
|
||||||
context["entity_type"] = "compound"
|
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/compound/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_mode"] = "tabbed"
|
|
||||||
context["list_title"] = "compounds"
|
|
||||||
|
|
||||||
reviewed_compound_qs = Compound.objects.none()
|
reviewed_compound_qs = Compound.objects.none()
|
||||||
unreviewed_compound_qs = Compound.objects.none()
|
unreviewed_compound_qs = Compound.objects.none()
|
||||||
@ -1189,18 +1190,17 @@ def package_compounds(request, package_uuid):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(request, "collections/compounds_paginated.html", context)
|
context["reviewed_objects"] = reviewed_compound_qs
|
||||||
|
context["unreviewed_objects"] = unreviewed_compound_qs
|
||||||
|
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
compound_name = request.POST.get("compound-name")
|
compound_name = request.POST.get("compound-name")
|
||||||
compound_smiles = request.POST.get("compound-smiles")
|
compound_smiles = request.POST.get("compound-smiles")
|
||||||
compound_description = request.POST.get("compound-description")
|
compound_description = request.POST.get("compound-description")
|
||||||
try:
|
|
||||||
c = Compound.create(
|
c = Compound.create(current_package, compound_smiles, compound_name, compound_description)
|
||||||
current_package, compound_smiles, compound_name, compound_description
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
raise BadRequest(str(e))
|
|
||||||
|
|
||||||
return redirect(c.url)
|
return redirect(c.url)
|
||||||
|
|
||||||
@ -1308,17 +1308,19 @@ def package_compound_structures(request, package_uuid, compound_uuid):
|
|||||||
context["breadcrumbs"] = breadcrumbs(
|
context["breadcrumbs"] = breadcrumbs(
|
||||||
current_package, "compound", current_compound, "structure"
|
current_package, "compound", current_compound, "structure"
|
||||||
)
|
)
|
||||||
context["entity_type"] = "structure"
|
|
||||||
context["page_title"] = f"{current_compound.name} - Structures"
|
|
||||||
context["api_endpoint"] = (
|
|
||||||
f"/api/v1/package/{current_package.uuid}/compound/{current_compound.uuid}/structure/"
|
|
||||||
)
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["compound"] = current_compound
|
|
||||||
context["list_mode"] = "combined"
|
|
||||||
context["list_title"] = "structures"
|
|
||||||
|
|
||||||
return render(request, "collections/structures_paginated.html", context)
|
reviewed_compound_structure_qs = CompoundStructure.objects.none()
|
||||||
|
unreviewed_compound_structure_qs = CompoundStructure.objects.none()
|
||||||
|
|
||||||
|
if current_package.reviewed:
|
||||||
|
reviewed_compound_structure_qs = current_compound.structures.order_by("name")
|
||||||
|
else:
|
||||||
|
unreviewed_compound_structure_qs = current_compound.structures.order_by("name")
|
||||||
|
|
||||||
|
context["reviewed_objects"] = reviewed_compound_structure_qs
|
||||||
|
context["unreviewed_objects"] = unreviewed_compound_structure_qs
|
||||||
|
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
structure_name = request.POST.get("structure-name")
|
structure_name = request.POST.get("structure-name")
|
||||||
@ -1465,10 +1467,6 @@ def package_rules(request, package_uuid):
|
|||||||
context["meta"]["current_package"] = current_package
|
context["meta"]["current_package"] = current_package
|
||||||
context["object_type"] = "rule"
|
context["object_type"] = "rule"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "rule")
|
context["breadcrumbs"] = breadcrumbs(current_package, "rule")
|
||||||
context["entity_type"] = "rule"
|
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/rule/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "rules"
|
|
||||||
|
|
||||||
reviewed_rule_qs = Rule.objects.none()
|
reviewed_rule_qs = Rule.objects.none()
|
||||||
unreviewed_rule_qs = Rule.objects.none()
|
unreviewed_rule_qs = Rule.objects.none()
|
||||||
@ -1490,7 +1488,10 @@ def package_rules(request, package_uuid):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(request, "collections/rules_paginated.html", context)
|
context["reviewed_objects"] = reviewed_rule_qs
|
||||||
|
context["unreviewed_objects"] = unreviewed_rule_qs
|
||||||
|
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
log_post_params(request)
|
log_post_params(request)
|
||||||
@ -1668,15 +1669,11 @@ def package_reactions(request, package_uuid):
|
|||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
context["title"] = f"enviPath - {current_package.name} - Reactions"
|
context["title"] = f"enviPath - {current_package.name} - {current_package.name} - Reactions"
|
||||||
|
|
||||||
context["meta"]["current_package"] = current_package
|
context["meta"]["current_package"] = current_package
|
||||||
context["object_type"] = "reaction"
|
context["object_type"] = "reaction"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "reaction")
|
context["breadcrumbs"] = breadcrumbs(current_package, "reaction")
|
||||||
context["entity_type"] = "reaction"
|
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/reaction/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "reactions"
|
|
||||||
|
|
||||||
reviewed_reaction_qs = Reaction.objects.none()
|
reviewed_reaction_qs = Reaction.objects.none()
|
||||||
unreviewed_reaction_qs = Reaction.objects.none()
|
unreviewed_reaction_qs = Reaction.objects.none()
|
||||||
@ -1702,7 +1699,10 @@ def package_reactions(request, package_uuid):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(request, "collections/reactions_paginated.html", context)
|
context["reviewed_objects"] = reviewed_reaction_qs
|
||||||
|
context["unreviewed_objects"] = unreviewed_reaction_qs
|
||||||
|
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
reaction_name = request.POST.get("reaction-name")
|
reaction_name = request.POST.get("reaction-name")
|
||||||
@ -1821,10 +1821,6 @@ def package_pathways(request, package_uuid):
|
|||||||
context["meta"]["current_package"] = current_package
|
context["meta"]["current_package"] = current_package
|
||||||
context["object_type"] = "pathway"
|
context["object_type"] = "pathway"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "pathway")
|
context["breadcrumbs"] = breadcrumbs(current_package, "pathway")
|
||||||
context["entity_type"] = "pathway"
|
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/pathway/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "pathways"
|
|
||||||
|
|
||||||
reviewed_pathway_qs = Pathway.objects.none()
|
reviewed_pathway_qs = Pathway.objects.none()
|
||||||
unreviewed_pathway_qs = Pathway.objects.none()
|
unreviewed_pathway_qs = Pathway.objects.none()
|
||||||
@ -1848,7 +1844,10 @@ def package_pathways(request, package_uuid):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return render(request, "collections/pathways_paginated.html", context)
|
context["reviewed_objects"] = reviewed_pathway_qs
|
||||||
|
context["unreviewed_objects"] = unreviewed_pathway_qs
|
||||||
|
|
||||||
|
return render(request, "collections/objects_list.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
log_post_params(request)
|
log_post_params(request)
|
||||||
@ -1865,6 +1864,7 @@ def package_pathways(request, package_uuid):
|
|||||||
"Pathway prediction failed!",
|
"Pathway prediction failed!",
|
||||||
"Pathway prediction failed due to missing or empty SMILES",
|
"Pathway prediction failed due to missing or empty SMILES",
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
stand_smiles = FormatConverter.standardize(smiles)
|
stand_smiles = FormatConverter.standardize(smiles)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -1887,13 +1887,8 @@ def package_pathways(request, package_uuid):
|
|||||||
prediction_setting = SettingManager.get_setting_by_url(current_user, prediction_setting)
|
prediction_setting = SettingManager.get_setting_by_url(current_user, prediction_setting)
|
||||||
else:
|
else:
|
||||||
prediction_setting = current_user.prediction_settings()
|
prediction_setting = current_user.prediction_settings()
|
||||||
pw = Pathway.create(
|
|
||||||
current_package,
|
pw = Pathway.create(current_package, stand_smiles, name=name, description=description)
|
||||||
stand_smiles,
|
|
||||||
name=name,
|
|
||||||
description=description,
|
|
||||||
predicted=pw_mode in {"predict", "incremental"},
|
|
||||||
)
|
|
||||||
|
|
||||||
# set mode
|
# set mode
|
||||||
pw.kv.update({"mode": pw_mode})
|
pw.kv.update({"mode": pw_mode})
|
||||||
@ -1901,7 +1896,7 @@ def package_pathways(request, package_uuid):
|
|||||||
|
|
||||||
if pw_mode == "predict" or pw_mode == "incremental":
|
if pw_mode == "predict" or pw_mode == "incremental":
|
||||||
# unlimited pred (will be handled by setting)
|
# unlimited pred (will be handled by setting)
|
||||||
limit = None
|
limit = -1
|
||||||
|
|
||||||
# For incremental predict first level and return
|
# For incremental predict first level and return
|
||||||
if pw_mode == "incremental":
|
if pw_mode == "incremental":
|
||||||
@ -1937,7 +1932,6 @@ def package_pathway(request, package_uuid, pathway_uuid):
|
|||||||
{
|
{
|
||||||
"status": current_pathway.status(),
|
"status": current_pathway.status(),
|
||||||
"modified": current_pathway.modified.strftime("%Y-%m-%d %H:%M:%S"),
|
"modified": current_pathway.modified.strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
"emptyDueToThreshold": current_pathway.empty_due_to_threshold(),
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1958,7 +1952,7 @@ def package_pathway(request, package_uuid, pathway_uuid):
|
|||||||
rule_package = PackageManager.get_package_by_url(
|
rule_package = PackageManager.get_package_by_url(
|
||||||
current_user, request.GET.get("rule-package")
|
current_user, request.GET.get("rule-package")
|
||||||
)
|
)
|
||||||
_, res = dispatch_eager(
|
res = dispatch_eager(
|
||||||
current_user, identify_missing_rules, [current_pathway.pk], rule_package.pk
|
current_user, identify_missing_rules, [current_pathway.pk], rule_package.pk
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -2386,10 +2380,6 @@ def package_scenarios(request, package_uuid):
|
|||||||
context["meta"]["current_package"] = current_package
|
context["meta"]["current_package"] = current_package
|
||||||
context["object_type"] = "scenario"
|
context["object_type"] = "scenario"
|
||||||
context["breadcrumbs"] = breadcrumbs(current_package, "scenario")
|
context["breadcrumbs"] = breadcrumbs(current_package, "scenario")
|
||||||
context["entity_type"] = "scenario"
|
|
||||||
context["api_endpoint"] = f"/api/v1/package/{current_package.uuid}/scenario/"
|
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
|
||||||
context["list_title"] = "scenarios"
|
|
||||||
|
|
||||||
reviewed_scenario_qs = Scenario.objects.none()
|
reviewed_scenario_qs = Scenario.objects.none()
|
||||||
unreviewed_scenario_qs = Scenario.objects.none()
|
unreviewed_scenario_qs = Scenario.objects.none()
|
||||||
@ -2415,10 +2405,13 @@ def package_scenarios(request, package_uuid):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
context["reviewed_objects"] = reviewed_scenario_qs
|
||||||
|
context["unreviewed_objects"] = unreviewed_scenario_qs
|
||||||
|
|
||||||
from envipy_additional_information import (
|
from envipy_additional_information import (
|
||||||
SEDIMENT_ADDITIONAL_INFORMATION,
|
|
||||||
SLUDGE_ADDITIONAL_INFORMATION,
|
SLUDGE_ADDITIONAL_INFORMATION,
|
||||||
SOIL_ADDITIONAL_INFORMATION,
|
SOIL_ADDITIONAL_INFORMATION,
|
||||||
|
SEDIMENT_ADDITIONAL_INFORMATION,
|
||||||
)
|
)
|
||||||
|
|
||||||
context["scenario_types"] = {
|
context["scenario_types"] = {
|
||||||
@ -2449,7 +2442,7 @@ def package_scenarios(request, package_uuid):
|
|||||||
context["soil_additional_information"] = SOIL_ADDITIONAL_INFORMATION
|
context["soil_additional_information"] = SOIL_ADDITIONAL_INFORMATION
|
||||||
context["sediment_additional_information"] = SEDIMENT_ADDITIONAL_INFORMATION
|
context["sediment_additional_information"] = SEDIMENT_ADDITIONAL_INFORMATION
|
||||||
|
|
||||||
return render(request, "collections/scenarios_paginated.html", context)
|
return render(request, "collections/objects_list.html", context)
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
log_post_params(request)
|
log_post_params(request)
|
||||||
|
|
||||||
@ -2664,14 +2657,6 @@ def user(request, user_uuid):
|
|||||||
|
|
||||||
return redirect(current_user.url)
|
return redirect(current_user.url)
|
||||||
|
|
||||||
if "change_default" in request.POST:
|
|
||||||
new_default_uuid = request.POST["change_default"]
|
|
||||||
current_user.default_setting = SettingManager.get_setting_by_id(
|
|
||||||
current_user, new_default_uuid
|
|
||||||
)
|
|
||||||
current_user.save()
|
|
||||||
return redirect(current_user.url)
|
|
||||||
|
|
||||||
return HttpResponseBadRequest()
|
return HttpResponseBadRequest()
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@ -2772,18 +2757,14 @@ def settings(request):
|
|||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
|
||||||
context["title"] = "enviPath - Settings"
|
|
||||||
|
|
||||||
context["object_type"] = "setting"
|
context["object_type"] = "setting"
|
||||||
|
# Even if settings are aready in "meta", for consistency add it on root level
|
||||||
|
context["settings"] = SettingManager.get_all_settings(current_user)
|
||||||
context["breadcrumbs"] = [
|
context["breadcrumbs"] = [
|
||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Group": s.SERVER_URL + "/setting"},
|
{"Group": s.SERVER_URL + "/setting"},
|
||||||
]
|
]
|
||||||
|
return
|
||||||
context["objects"] = SettingManager.get_all_settings(current_user)
|
|
||||||
|
|
||||||
return render(request, "collections/objects_list.html", context)
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
if s.DEBUG:
|
if s.DEBUG:
|
||||||
for k, v in request.POST.items():
|
for k, v in request.POST.items():
|
||||||
@ -2821,25 +2802,15 @@ def settings(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if not PackageManager.readable(current_user, params["model"].package):
|
if not PackageManager.readable(current_user, params["model"].package):
|
||||||
raise PermissionDenied("You're not allowed to access this model!")
|
raise ValueError("")
|
||||||
|
|
||||||
expansion_scheme = request.POST.get(
|
|
||||||
"model-based-prediction-setting-expansion-scheme", "BFS"
|
|
||||||
)
|
|
||||||
|
|
||||||
if expansion_scheme not in ExpansionSchemeChoice.values:
|
|
||||||
raise BadRequest(f"Unknown expansion scheme: {expansion_scheme}")
|
|
||||||
|
|
||||||
params["expansion_scheme"] = ExpansionSchemeChoice(expansion_scheme)
|
|
||||||
|
|
||||||
elif tp_gen_method == "rule-based-prediction-setting":
|
elif tp_gen_method == "rule-based-prediction-setting":
|
||||||
rule_packages = request.POST.getlist("rule-based-prediction-setting-packages")
|
rule_packages = request.POST.getlist("rule-based-prediction-setting-packages")
|
||||||
params["rule_packages"] = [
|
params["rule_packages"] = [
|
||||||
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
PackageManager.get_package_by_url(current_user, p) for p in rule_packages
|
||||||
]
|
]
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise BadRequest("Neither Model-Based nor Rule-Based as Method selected!")
|
raise ValueError("")
|
||||||
|
|
||||||
created_setting = SettingManager.create_setting(
|
created_setting = SettingManager.create_setting(
|
||||||
current_user,
|
current_user,
|
||||||
@ -2881,143 +2852,6 @@ def jobs(request):
|
|||||||
|
|
||||||
return render(request, "collections/joblog.html", context)
|
return render(request, "collections/joblog.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
|
||||||
job_name = request.POST.get("job-name")
|
|
||||||
|
|
||||||
if job_name == "engineer-pathway":
|
|
||||||
pathway_to_engineer = request.POST.get("pathway-to-engineer")
|
|
||||||
engineer_setting = request.POST.get("engineer-setting")
|
|
||||||
|
|
||||||
if not all([pathway_to_engineer, engineer_setting]):
|
|
||||||
raise BadRequest(
|
|
||||||
f"Unable to run {job_name} as it requires 'pathway-to-engineer' and 'engineer-setting' parameters."
|
|
||||||
)
|
|
||||||
|
|
||||||
pathway_package = PackageManager.get_package_by_url(current_user, pathway_to_engineer)
|
|
||||||
pathway_to_engineer = Pathway.objects.get(
|
|
||||||
url=pathway_to_engineer, package=pathway_package
|
|
||||||
)
|
|
||||||
|
|
||||||
engineer_setting = SettingManager.get_setting_by_url(current_user, engineer_setting)
|
|
||||||
|
|
||||||
target_package = PackageManager.create_package(
|
|
||||||
current_user,
|
|
||||||
f"Autogenerated Package for Pathway Engineering of {pathway_to_engineer.name}",
|
|
||||||
f"This Package was generated automatically for the engineering Task of {pathway_to_engineer.name}.",
|
|
||||||
)
|
|
||||||
|
|
||||||
from .tasks import dispatch, engineer_pathways
|
|
||||||
|
|
||||||
res = dispatch(
|
|
||||||
current_user,
|
|
||||||
engineer_pathways,
|
|
||||||
[pathway_to_engineer.pk],
|
|
||||||
engineer_setting.pk,
|
|
||||||
target_package.pk,
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(f"{s.SERVER_URL}/jobs/{res.task_id}")
|
|
||||||
|
|
||||||
elif job_name == "batch-predict":
|
|
||||||
substrates = request.POST.get("substrates")
|
|
||||||
prediction_setting_url = request.POST.get("prediction-setting")
|
|
||||||
num_tps = request.POST.get("num-tps")
|
|
||||||
|
|
||||||
if substrates is None or substrates.strip() == "":
|
|
||||||
raise BadRequest("No substrates provided.")
|
|
||||||
|
|
||||||
pred_data = []
|
|
||||||
for pair in substrates.split("\n"):
|
|
||||||
parts = pair.split(",")
|
|
||||||
|
|
||||||
try:
|
|
||||||
smiles = FormatConverter.standardize(parts[0])
|
|
||||||
except ValueError:
|
|
||||||
raise BadRequest(f"Couldn't standardize SMILES {parts[0]}!")
|
|
||||||
|
|
||||||
# name is optional
|
|
||||||
name = parts[1] if len(parts) > 1 else None
|
|
||||||
pred_data.append([smiles, name])
|
|
||||||
|
|
||||||
max_tps = 50
|
|
||||||
if num_tps is not None and num_tps.strip() != "":
|
|
||||||
try:
|
|
||||||
num_tps = int(num_tps)
|
|
||||||
max_tps = max(min(num_tps, 50), 1)
|
|
||||||
except ValueError:
|
|
||||||
raise BadRequest(f"Parameter for num-tps {num_tps} is not a valid integer.")
|
|
||||||
|
|
||||||
batch_predict_setting = SettingManager.get_setting_by_url(
|
|
||||||
current_user, prediction_setting_url
|
|
||||||
)
|
|
||||||
|
|
||||||
target_package = PackageManager.create_package(
|
|
||||||
current_user,
|
|
||||||
f"Autogenerated Package for Batch Prediction {datetime.now()}",
|
|
||||||
"This Package was generated automatically for the batch prediction task.",
|
|
||||||
)
|
|
||||||
|
|
||||||
from .tasks import dispatch, batch_predict
|
|
||||||
|
|
||||||
res = dispatch(
|
|
||||||
current_user,
|
|
||||||
batch_predict,
|
|
||||||
pred_data,
|
|
||||||
batch_predict_setting.pk,
|
|
||||||
target_package.pk,
|
|
||||||
num_tps=max_tps,
|
|
||||||
)
|
|
||||||
|
|
||||||
return redirect(f"{s.SERVER_URL}/jobs/{res.task_id}")
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise BadRequest(f"Job {job_name} is not supported!")
|
|
||||||
else:
|
|
||||||
return HttpResponseNotAllowed(["GET", "POST"])
|
|
||||||
|
|
||||||
|
|
||||||
def job(request, job_uuid):
|
|
||||||
current_user = _anonymous_or_real(request)
|
|
||||||
context = get_base_context(request)
|
|
||||||
|
|
||||||
if request.method == "GET":
|
|
||||||
if current_user.is_superuser:
|
|
||||||
job = JobLog.objects.get(task_id=job_uuid)
|
|
||||||
else:
|
|
||||||
job = JobLog.objects.get(task_id=job_uuid, user=current_user)
|
|
||||||
|
|
||||||
# No op if status is already in a terminal state
|
|
||||||
job.check_for_update()
|
|
||||||
|
|
||||||
if request.GET.get("download", False) == "true":
|
|
||||||
if not job.is_result_downloadable():
|
|
||||||
raise BadRequest("Result is not downloadable!")
|
|
||||||
|
|
||||||
if job.job_name == "batch_predict":
|
|
||||||
filename = f"{job.job_name.replace(' ', '_')}_{job.task_id}.csv"
|
|
||||||
else:
|
|
||||||
raise BadRequest("Result is not downloadable!")
|
|
||||||
|
|
||||||
res_str = job.task_result
|
|
||||||
response = HttpResponse(res_str, content_type="text/csv")
|
|
||||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
|
||||||
|
|
||||||
return response
|
|
||||||
|
|
||||||
context["object_type"] = "joblog"
|
|
||||||
context["breadcrumbs"] = [
|
|
||||||
{"Home": s.SERVER_URL},
|
|
||||||
{"Jobs": s.SERVER_URL + "/jobs"},
|
|
||||||
{job.job_name: f"{s.SERVER_URL}/jobs/{job.task_id}"},
|
|
||||||
]
|
|
||||||
|
|
||||||
context["job"] = job
|
|
||||||
|
|
||||||
return render(request, "objects/joblog.html", context)
|
|
||||||
|
|
||||||
else:
|
|
||||||
return HttpResponseNotAllowed(["GET"])
|
|
||||||
|
|
||||||
|
|
||||||
###########
|
###########
|
||||||
# KETCHER #
|
# KETCHER #
|
||||||
|
|||||||
Binary file not shown.
@ -1,21 +1,24 @@
|
|||||||
|
import gzip
|
||||||
import json
|
import json
|
||||||
import logging
|
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.http import HttpResponseNotAllowed
|
||||||
from django.shortcuts import render
|
from django.shortcuts import render
|
||||||
|
|
||||||
|
from epdb.logic import PackageManager
|
||||||
|
from epdb.models import Rule, SimpleAmbitRule, Package, CompoundStructure
|
||||||
|
from epdb.views import get_base_context, _anonymous_or_real
|
||||||
|
from utilities.chem import FormatConverter
|
||||||
|
|
||||||
|
|
||||||
from rdkit import Chem
|
from rdkit import Chem
|
||||||
from rdkit.Chem.MolStandardize import rdMolStandardize
|
from rdkit.Chem.MolStandardize import rdMolStandardize
|
||||||
|
|
||||||
from epdb.models import CompoundStructure, Rule, SimpleAmbitRule
|
|
||||||
from epdb.views import get_base_context
|
|
||||||
from utilities.chem import FormatConverter
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_smiles(smiles):
|
def normalize_smiles(smiles):
|
||||||
m1 = Chem.MolFromSmiles(smiles)
|
m1 = Chem.MolFromSmiles(smiles)
|
||||||
@ -56,7 +59,9 @@ def run_both_engines(SMILES, SMIRKS):
|
|||||||
set(
|
set(
|
||||||
[
|
[
|
||||||
normalize_smiles(str(x))
|
normalize_smiles(str(x))
|
||||||
for x in FormatConverter.sanitize_smiles([str(s) for s in all_rdkit_prods])[0]
|
for x in FormatConverter.sanitize_smiles(
|
||||||
|
[str(s) for s in all_rdkit_prods]
|
||||||
|
)[0]
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -80,7 +85,8 @@ def migration(request):
|
|||||||
url="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1"
|
url="http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1"
|
||||||
)
|
)
|
||||||
ALL_SMILES = [
|
ALL_SMILES = [
|
||||||
cs.smiles for cs in CompoundStructure.objects.filter(compound__package=BBD)
|
cs.smiles
|
||||||
|
for cs in CompoundStructure.objects.filter(compound__package=BBD)
|
||||||
]
|
]
|
||||||
RULES = SimpleAmbitRule.objects.filter(package=BBD)
|
RULES = SimpleAmbitRule.objects.filter(package=BBD)
|
||||||
|
|
||||||
@ -136,7 +142,9 @@ def migration(request):
|
|||||||
)
|
)
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
@ -144,6 +152,8 @@ def migration(request):
|
|||||||
|
|
||||||
|
|
||||||
def migration_detail(request, package_uuid, rule_uuid):
|
def migration_detail(request, package_uuid, rule_uuid):
|
||||||
|
current_user = _anonymous_or_real(request)
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
context = get_base_context(request)
|
context = get_base_context(request)
|
||||||
|
|
||||||
@ -225,7 +235,9 @@ def compare(request):
|
|||||||
context["smirks"] = (
|
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"
|
"[#1,#6:6][#7;X3;!$(NC1CC1)!$([N][C]=O)!$([!#8]CNC=O):1]([#1,#6:7])[#6;A;X4:2][H:3]>>[#1,#6:6][#7;X3:1]([#1,#6:7])[H:3].[#6;A:2]=O"
|
||||||
)
|
)
|
||||||
context["smiles"] = "C(CC(=O)N[C@@H](CS[Se-])C(=O)NCC(=O)[O-])[C@@H](C(=O)[O-])N"
|
context["smiles"] = (
|
||||||
|
"C(CC(=O)N[C@@H](CS[Se-])C(=O)NCC(=O)[O-])[C@@H](C(=O)[O-])N"
|
||||||
|
)
|
||||||
return render(request, "compare.html", context)
|
return render(request, "compare.html", context)
|
||||||
|
|
||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
|
|||||||
@ -9,8 +9,7 @@ dependencies = [
|
|||||||
"django>=5.2.1",
|
"django>=5.2.1",
|
||||||
"django-extensions>=4.1",
|
"django-extensions>=4.1",
|
||||||
"django-model-utils>=5.0.0",
|
"django-model-utils>=5.0.0",
|
||||||
"django-ninja>=1.4.5",
|
"django-ninja>=1.4.1",
|
||||||
"django-ninja-extra>=0.30.6",
|
|
||||||
"django-oauth-toolkit>=3.0.1",
|
"django-oauth-toolkit>=3.0.1",
|
||||||
"django-polymorphic>=4.1.0",
|
"django-polymorphic>=4.1.0",
|
||||||
"enviformer",
|
"enviformer",
|
||||||
@ -35,7 +34,7 @@ dependencies = [
|
|||||||
[tool.uv.sources]
|
[tool.uv.sources]
|
||||||
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" }
|
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.4" }
|
||||||
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.7" }
|
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" }
|
envipy-ambit = { git = "ssh://git@git.envipath.com/enviPath/enviPy-ambit.git" }
|
||||||
|
|
||||||
[project.optional-dependencies]
|
[project.optional-dependencies]
|
||||||
@ -46,9 +45,6 @@ dev = [
|
|||||||
"poethepoet>=0.37.0",
|
"poethepoet>=0.37.0",
|
||||||
"pre-commit>=4.3.0",
|
"pre-commit>=4.3.0",
|
||||||
"ruff>=0.13.3",
|
"ruff>=0.13.3",
|
||||||
"pytest-playwright>=0.7.1",
|
|
||||||
"pytest-django>=4.11.1",
|
|
||||||
"pytest-cov>=7.0.0",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
@ -70,31 +66,47 @@ docstring-code-format = true
|
|||||||
|
|
||||||
[tool.poe.tasks]
|
[tool.poe.tasks]
|
||||||
# Main tasks
|
# Main tasks
|
||||||
setup = { sequence = [
|
setup = { sequence = ["db-up", "migrate", "bootstrap"], help = "Complete setup: start database, run migrations, and bootstrap data" }
|
||||||
"db-up",
|
dev = { shell = """
|
||||||
"migrate",
|
# Start pnpm CSS watcher in background
|
||||||
"bootstrap",
|
pnpm run dev &
|
||||||
], help = "Complete setup: start database, run migrations, and bootstrap data" }
|
PNPM_PID=$!
|
||||||
dev = { cmd = "uv run python scripts/dev_server.py", help = "Start the development server with CSS watcher", deps = [
|
echo "Started CSS watcher (PID: $PNPM_PID)"
|
||||||
"db-up",
|
|
||||||
"js-deps",
|
# Cleanup function
|
||||||
] }
|
cleanup() {
|
||||||
build = { sequence = [
|
echo "\nShutting down..."
|
||||||
"build-frontend",
|
if kill -0 $PNPM_PID 2>/dev/null; then
|
||||||
"collectstatic",
|
kill $PNPM_PID
|
||||||
], help = "Build frontend assets and collect static files" }
|
echo "✓ CSS watcher stopped"
|
||||||
|
fi
|
||||||
|
if [ ! -z "${DJ_PID:-}" ] && kill -0 $DJ_PID 2>/dev/null; then
|
||||||
|
kill $DJ_PID
|
||||||
|
echo "✓ Django server stopped"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set trap for cleanup
|
||||||
|
trap cleanup EXIT INT TERM
|
||||||
|
|
||||||
|
# Start Django dev server in background
|
||||||
|
uv run python manage.py runserver &
|
||||||
|
DJ_PID=$!
|
||||||
|
|
||||||
|
# Wait for Django to finish
|
||||||
|
wait $DJ_PID
|
||||||
|
""", help = "Start the development server with CSS watcher", deps = ["db-up", "js-deps"] }
|
||||||
|
build = { sequence = ["build-frontend", "collectstatic"], help = "Build frontend assets and collect static files" }
|
||||||
|
|
||||||
# Database tasks
|
# Database tasks
|
||||||
db-up = { cmd = "docker compose -f docker-compose.dev.yml up -d", help = "Start PostgreSQL database using Docker Compose" }
|
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" }
|
db-down = { cmd = "docker compose -f docker-compose.dev.yml down", help = "Stop PostgreSQL database" }
|
||||||
|
|
||||||
# Frontend tasks
|
# Frontend tasks
|
||||||
js-deps = { cmd = "uv run python scripts/pnpm_wrapper.py install", help = "Install frontend dependencies" }
|
js-deps = { cmd = "pnpm install", help = "Install frontend dependencies" }
|
||||||
|
|
||||||
# Full cleanup tasks
|
# Full cleanup tasks
|
||||||
clean = { sequence = [
|
clean = { sequence = ["clean-db"], help = "Remove model files and database volumes (WARNING: destroys all data!)" }
|
||||||
"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." }
|
clean-db = { cmd = "docker compose -f docker-compose.dev.yml down -v", help = "Removes the database container and volume." }
|
||||||
|
|
||||||
# Django tasks
|
# Django tasks
|
||||||
@ -112,33 +124,6 @@ echo " Password: SuperSafe"
|
|||||||
""", help = "Bootstrap initial data (anonymous user, packages, models)" }
|
""", help = "Bootstrap initial data (anonymous user, packages, models)" }
|
||||||
shell = { cmd = "uv run python manage.py shell", help = "Open Django shell" }
|
shell = { cmd = "uv run python manage.py shell", help = "Open Django shell" }
|
||||||
|
|
||||||
|
# Build tasks
|
||||||
build-frontend = { cmd = "uv run python scripts/pnpm_wrapper.py run build", help = "Build frontend assets using pnpm", deps = [
|
build-frontend = { cmd = "pnpm run build", help = "Build frontend assets using pnpm", deps = ["js-deps"] }
|
||||||
"js-deps",
|
collectstatic = { cmd = "uv run python manage.py collectstatic --noinput", help = "Collect static files for production", deps = ["build-frontend"] }
|
||||||
] } # Build tasks
|
|
||||||
|
|
||||||
|
|
||||||
collectstatic = { cmd = "uv run python manage.py collectstatic --noinput", help = "Collect static files for production", deps = [
|
|
||||||
"build-frontend",
|
|
||||||
] }
|
|
||||||
|
|
||||||
frontend-test-setup = { cmd = "playwright install", help = "Install the browsers required for frontend testing" }
|
|
||||||
|
|
||||||
[tool.pytest.ini_options]
|
|
||||||
addopts = "--verbose --capture=no --durations=10"
|
|
||||||
testpaths = ["tests", "*/tests"]
|
|
||||||
pythonpath = ["."]
|
|
||||||
norecursedirs = [
|
|
||||||
"env",
|
|
||||||
"venv",
|
|
||||||
"envipy-plugins",
|
|
||||||
"envipy-additional-information",
|
|
||||||
"envipy-ambit",
|
|
||||||
"enviformer",
|
|
||||||
]
|
|
||||||
markers = [
|
|
||||||
"api: API tests",
|
|
||||||
"frontend: Frontend tests",
|
|
||||||
"end2end: End-to-end tests",
|
|
||||||
"slow: Slow tests",
|
|
||||||
]
|
|
||||||
|
|||||||
@ -1,201 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Cross-platform development server script.
|
|
||||||
Starts pnpm CSS watcher and Django dev server, handling cleanup on exit.
|
|
||||||
Works on both Windows and Unix systems.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import atexit
|
|
||||||
import shutil
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def find_pnpm():
|
|
||||||
"""
|
|
||||||
Find pnpm executable on the system.
|
|
||||||
Returns the path to pnpm or None if not found.
|
|
||||||
"""
|
|
||||||
# Try to find pnpm using shutil.which
|
|
||||||
# On Windows, this will find pnpm.cmd if it's in PATH
|
|
||||||
pnpm_path = shutil.which("pnpm")
|
|
||||||
|
|
||||||
if pnpm_path:
|
|
||||||
return pnpm_path
|
|
||||||
|
|
||||||
# On Windows, also try pnpm.cmd explicitly
|
|
||||||
if sys.platform == "win32":
|
|
||||||
pnpm_cmd = shutil.which("pnpm.cmd")
|
|
||||||
if pnpm_cmd:
|
|
||||||
return pnpm_cmd
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class DevServerManager:
|
|
||||||
"""Manages background processes for development server."""
|
|
||||||
|
|
||||||
def __init__(self):
|
|
||||||
self.processes = []
|
|
||||||
self._cleanup_registered = False
|
|
||||||
|
|
||||||
def start_process(self, command, description, shell=False):
|
|
||||||
"""Start a background process and return the process object."""
|
|
||||||
print(f"Starting {description}...")
|
|
||||||
try:
|
|
||||||
if shell:
|
|
||||||
# Use shell=True for commands that need shell interpretation
|
|
||||||
process = subprocess.Popen(
|
|
||||||
command,
|
|
||||||
shell=True,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
text=True,
|
|
||||||
bufsize=1,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Split command into list for subprocess
|
|
||||||
process = subprocess.Popen(
|
|
||||||
command,
|
|
||||||
stdout=subprocess.PIPE,
|
|
||||||
stderr=subprocess.STDOUT,
|
|
||||||
text=True,
|
|
||||||
bufsize=1,
|
|
||||||
)
|
|
||||||
self.processes.append((process, description))
|
|
||||||
print(f"✓ Started {description} (PID: {process.pid})")
|
|
||||||
return process
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Failed to start {description}: {e}", file=sys.stderr)
|
|
||||||
self.cleanup()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
def cleanup(self):
|
|
||||||
"""Terminate all running processes."""
|
|
||||||
if not self.processes:
|
|
||||||
return
|
|
||||||
|
|
||||||
print("\nShutting down...")
|
|
||||||
for process, description in self.processes:
|
|
||||||
if process.poll() is None: # Process is still running
|
|
||||||
try:
|
|
||||||
# Try graceful termination first
|
|
||||||
if sys.platform == "win32":
|
|
||||||
process.terminate()
|
|
||||||
else:
|
|
||||||
process.send_signal(signal.SIGTERM)
|
|
||||||
|
|
||||||
# Wait up to 5 seconds for graceful shutdown
|
|
||||||
try:
|
|
||||||
process.wait(timeout=5)
|
|
||||||
except subprocess.TimeoutExpired:
|
|
||||||
# Force kill if graceful shutdown failed
|
|
||||||
if sys.platform == "win32":
|
|
||||||
process.kill()
|
|
||||||
else:
|
|
||||||
process.send_signal(signal.SIGKILL)
|
|
||||||
process.wait()
|
|
||||||
|
|
||||||
print(f"✓ {description} stopped")
|
|
||||||
except Exception as e:
|
|
||||||
print(f"✗ Error stopping {description}: {e}", file=sys.stderr)
|
|
||||||
|
|
||||||
self.processes.clear()
|
|
||||||
|
|
||||||
def register_cleanup(self):
|
|
||||||
"""Register cleanup handlers for various exit scenarios."""
|
|
||||||
if self._cleanup_registered:
|
|
||||||
return
|
|
||||||
|
|
||||||
self._cleanup_registered = True
|
|
||||||
|
|
||||||
# Register atexit handler (works on all platforms)
|
|
||||||
atexit.register(self.cleanup)
|
|
||||||
|
|
||||||
# Register signal handlers (Unix only)
|
|
||||||
if sys.platform != "win32":
|
|
||||||
signal.signal(signal.SIGINT, self._signal_handler)
|
|
||||||
signal.signal(signal.SIGTERM, self._signal_handler)
|
|
||||||
|
|
||||||
def _signal_handler(self, signum, frame):
|
|
||||||
"""Handle Unix signals."""
|
|
||||||
self.cleanup()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
def wait_for_process(self, process, description):
|
|
||||||
"""Wait for a process to finish and handle its output."""
|
|
||||||
try:
|
|
||||||
# Stream output from the process
|
|
||||||
for line in iter(process.stdout.readline, ""):
|
|
||||||
if line:
|
|
||||||
print(f"[{description}] {line.rstrip()}")
|
|
||||||
|
|
||||||
process.wait()
|
|
||||||
return process.returncode
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
# Handle Ctrl+C
|
|
||||||
self.cleanup()
|
|
||||||
sys.exit(0)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error waiting for {description}: {e}", file=sys.stderr)
|
|
||||||
self.cleanup()
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point."""
|
|
||||||
manager = DevServerManager()
|
|
||||||
manager.register_cleanup()
|
|
||||||
|
|
||||||
# Find pnpm executable
|
|
||||||
pnpm_path = find_pnpm()
|
|
||||||
if not pnpm_path:
|
|
||||||
print("Error: pnpm not found in PATH.", file=sys.stderr)
|
|
||||||
print("\nPlease install pnpm:", file=sys.stderr)
|
|
||||||
print(" Windows: https://pnpm.io/installation#on-windows", file=sys.stderr)
|
|
||||||
print(" Unix: https://pnpm.io/installation#on-posix-systems", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Determine shell usage based on platform
|
|
||||||
use_shell = sys.platform == "win32"
|
|
||||||
|
|
||||||
# Start pnpm CSS watcher
|
|
||||||
# Use the found pnpm path to ensure it works on Windows
|
|
||||||
pnpm_command = f'"{pnpm_path}" run dev' if use_shell else [pnpm_path, "run", "dev"]
|
|
||||||
manager.start_process(
|
|
||||||
pnpm_command,
|
|
||||||
"CSS watcher",
|
|
||||||
shell=use_shell,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Give pnpm a moment to start
|
|
||||||
time.sleep(1)
|
|
||||||
|
|
||||||
# Start Django dev server
|
|
||||||
django_process = manager.start_process(
|
|
||||||
["uv", "run", "python", "manage.py", "runserver"],
|
|
||||||
"Django server",
|
|
||||||
shell=False,
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\nDevelopment servers are running. Press Ctrl+C to stop.\n")
|
|
||||||
|
|
||||||
try:
|
|
||||||
# Wait for Django server (main process)
|
|
||||||
# If Django exits, we should clean up everything
|
|
||||||
return_code = manager.wait_for_process(django_process, "Django")
|
|
||||||
|
|
||||||
# If Django exited unexpectedly, clean up and exit
|
|
||||||
if return_code != 0:
|
|
||||||
manager.cleanup()
|
|
||||||
sys.exit(return_code)
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
# Ctrl+C was pressed
|
|
||||||
manager.cleanup()
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@ -1,59 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Cross-platform pnpm command wrapper.
|
|
||||||
Finds pnpm correctly on Windows (handles pnpm.cmd) and Unix systems.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
|
|
||||||
|
|
||||||
def find_pnpm():
|
|
||||||
"""
|
|
||||||
Find pnpm executable on the system.
|
|
||||||
Returns the path to pnpm or None if not found.
|
|
||||||
"""
|
|
||||||
# Try to find pnpm using shutil.which
|
|
||||||
# On Windows, this will find pnpm.cmd if it's in PATH
|
|
||||||
pnpm_path = shutil.which("pnpm")
|
|
||||||
|
|
||||||
if pnpm_path:
|
|
||||||
return pnpm_path
|
|
||||||
|
|
||||||
# On Windows, also try pnpm.cmd explicitly
|
|
||||||
if sys.platform == "win32":
|
|
||||||
pnpm_cmd = shutil.which("pnpm.cmd")
|
|
||||||
if pnpm_cmd:
|
|
||||||
return pnpm_cmd
|
|
||||||
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Main entry point - execute pnpm with provided arguments."""
|
|
||||||
pnpm_path = find_pnpm()
|
|
||||||
|
|
||||||
if not pnpm_path:
|
|
||||||
print("Error: pnpm not found in PATH.", file=sys.stderr)
|
|
||||||
print("\nPlease install pnpm:", file=sys.stderr)
|
|
||||||
print(" Windows: https://pnpm.io/installation#on-windows", file=sys.stderr)
|
|
||||||
print(" Unix: https://pnpm.io/installation#on-posix-systems", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
# Get all arguments passed to this script
|
|
||||||
args = sys.argv[1:]
|
|
||||||
|
|
||||||
# Execute pnpm with the provided arguments
|
|
||||||
try:
|
|
||||||
sys.exit(subprocess.call([pnpm_path] + args))
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
# Handle Ctrl+C gracefully
|
|
||||||
sys.exit(130)
|
|
||||||
except Exception as e:
|
|
||||||
print(f"Error executing pnpm: {e}", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
BIN
static/images/wait.gif
Normal file
BIN
static/images/wait.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 814 B |
@ -1,265 +0,0 @@
|
|||||||
/**
|
|
||||||
* Alpine.js Components for enviPath
|
|
||||||
*
|
|
||||||
* This module provides reusable Alpine.js data components for modals,
|
|
||||||
* form validation, and form submission.
|
|
||||||
*/
|
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
/**
|
|
||||||
* Modal Form Component
|
|
||||||
*
|
|
||||||
* Provides form validation using HTML5 Constraint Validation API,
|
|
||||||
* loading states for submission, and error message management.
|
|
||||||
*
|
|
||||||
* Basic Usage:
|
|
||||||
* <dialog x-data="modalForm()" @close="reset()">
|
|
||||||
* <form id="my-form">
|
|
||||||
* <input name="field" required>
|
|
||||||
* </form>
|
|
||||||
* <button @click="submit('my-form')" :disabled="isSubmitting">Submit</button>
|
|
||||||
* </dialog>
|
|
||||||
*
|
|
||||||
* With Custom State:
|
|
||||||
* <dialog x-data="modalForm({ state: { selectedItem: '', imageUrl: '' } })" @close="reset()">
|
|
||||||
* <select x-model="selectedItem" @change="updateImagePreview(selectedItem + '?image=svg')">
|
|
||||||
* <img :src="imageUrl" x-show="imageUrl">
|
|
||||||
* </dialog>
|
|
||||||
*
|
|
||||||
* With AJAX:
|
|
||||||
* <button @click="submitAsync('my-form', { onSuccess: (data) => console.log(data) })">
|
|
||||||
*/
|
|
||||||
Alpine.data('modalForm', (options = {}) => ({
|
|
||||||
isSubmitting: false,
|
|
||||||
errors: {},
|
|
||||||
// Spread custom initial state from options
|
|
||||||
...(options.state || {}),
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate a single field using HTML5 Constraint Validation API
|
|
||||||
* @param {HTMLElement} field - The input/select/textarea element
|
|
||||||
*/
|
|
||||||
validateField(field) {
|
|
||||||
const name = field.name || field.id;
|
|
||||||
if (!name) return;
|
|
||||||
|
|
||||||
if (!field.validity.valid) {
|
|
||||||
this.errors[name] = field.validationMessage;
|
|
||||||
} else {
|
|
||||||
delete this.errors[name];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clear error for a field (call on input)
|
|
||||||
* @param {HTMLElement} field - The input element
|
|
||||||
*/
|
|
||||||
clearError(field) {
|
|
||||||
const name = field.name || field.id;
|
|
||||||
if (name && this.errors[name]) {
|
|
||||||
delete this.errors[name];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get error message for a field
|
|
||||||
* @param {string} name - Field name
|
|
||||||
* @returns {string|undefined} Error message or undefined
|
|
||||||
*/
|
|
||||||
getError(name) {
|
|
||||||
return this.errors[name];
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if form has any errors
|
|
||||||
* @returns {boolean} True if there are errors
|
|
||||||
*/
|
|
||||||
hasErrors() {
|
|
||||||
return Object.keys(this.errors).length > 0;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate all fields in a form
|
|
||||||
* @param {string} formId - The form element ID
|
|
||||||
* @returns {boolean} True if form is valid
|
|
||||||
*/
|
|
||||||
validateAll(formId) {
|
|
||||||
const form = document.getElementById(formId);
|
|
||||||
if (!form) return false;
|
|
||||||
|
|
||||||
this.errors = {};
|
|
||||||
const fields = form.querySelectorAll('input, select, textarea');
|
|
||||||
|
|
||||||
fields.forEach(field => {
|
|
||||||
if (field.name && !field.validity.valid) {
|
|
||||||
this.errors[field.name] = field.validationMessage;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return !this.hasErrors();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Validate that two password fields match
|
|
||||||
* @param {string} password1Id - ID of first password field
|
|
||||||
* @param {string} password2Id - ID of second password field
|
|
||||||
* @returns {boolean} True if passwords match
|
|
||||||
*/
|
|
||||||
validatePasswordMatch(password1Id, password2Id) {
|
|
||||||
const pw1 = document.getElementById(password1Id);
|
|
||||||
const pw2 = document.getElementById(password2Id);
|
|
||||||
|
|
||||||
if (!pw1 || !pw2) return false;
|
|
||||||
|
|
||||||
if (pw1.value !== pw2.value) {
|
|
||||||
this.errors[pw2.name || password2Id] = 'Passwords do not match';
|
|
||||||
pw2.setCustomValidity('Passwords do not match');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
delete this.errors[pw2.name || password2Id];
|
|
||||||
pw2.setCustomValidity('');
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit a form with loading state
|
|
||||||
* @param {string} formId - The form element ID
|
|
||||||
*/
|
|
||||||
submit(formId) {
|
|
||||||
const form = document.getElementById(formId);
|
|
||||||
if (!form) return;
|
|
||||||
|
|
||||||
// Validate before submit
|
|
||||||
if (!form.checkValidity()) {
|
|
||||||
form.reportValidity();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set action to current URL if empty
|
|
||||||
if (!form.action || form.action === window.location.href + '#') {
|
|
||||||
form.action = window.location.href;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set loading state and submit
|
|
||||||
this.isSubmitting = true;
|
|
||||||
form.submit();
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Submit form via AJAX (fetch)
|
|
||||||
* @param {string} formId - The form element ID
|
|
||||||
* @param {Object} options - Options { onSuccess, onError, closeOnSuccess }
|
|
||||||
*/
|
|
||||||
async submitAsync(formId, options = {}) {
|
|
||||||
const form = document.getElementById(formId);
|
|
||||||
if (!form) return;
|
|
||||||
|
|
||||||
// Validate before submit
|
|
||||||
if (!form.checkValidity()) {
|
|
||||||
form.reportValidity();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isSubmitting = true;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const response = await fetch(form.action || window.location.href, {
|
|
||||||
method: form.method || 'POST',
|
|
||||||
body: formData,
|
|
||||||
headers: {
|
|
||||||
'X-Requested-With': 'XMLHttpRequest',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json().catch(() => ({}));
|
|
||||||
|
|
||||||
if (response.ok) {
|
|
||||||
if (options.onSuccess) {
|
|
||||||
options.onSuccess(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.redirect || data.success) {
|
|
||||||
window.location.href = data.redirect || data.success;
|
|
||||||
} else if (options.closeOnSuccess) {
|
|
||||||
this.$el.closest('dialog')?.close();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const errorMsg = data.error || data.message || `Error: ${response.status}`;
|
|
||||||
this.errors['_form'] = errorMsg;
|
|
||||||
|
|
||||||
if (options.onError) {
|
|
||||||
options.onError(errorMsg, data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
this.errors['_form'] = error.message;
|
|
||||||
|
|
||||||
if (options.onError) {
|
|
||||||
options.onError(error.message);
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
this.isSubmitting = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set form action URL dynamically
|
|
||||||
* @param {string} formId - The form element ID
|
|
||||||
* @param {string} url - The URL to set as action
|
|
||||||
*/
|
|
||||||
setFormAction(formId, url) {
|
|
||||||
const form = document.getElementById(formId);
|
|
||||||
if (form) {
|
|
||||||
form.action = url;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update image preview
|
|
||||||
* @param {string} url - Image URL (with query params)
|
|
||||||
* @param {string} targetId - Target element ID for the image
|
|
||||||
*/
|
|
||||||
updateImagePreview(url) {
|
|
||||||
// Store URL for reactive binding with :src
|
|
||||||
this.imageUrl = url;
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Reset form state (call on modal close)
|
|
||||||
* Resets to initial state from options
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.isSubmitting = false;
|
|
||||||
this.errors = {};
|
|
||||||
this.imageUrl = '';
|
|
||||||
|
|
||||||
// Reset custom state to initial values
|
|
||||||
if (options.state) {
|
|
||||||
Object.keys(options.state).forEach(key => {
|
|
||||||
this[key] = options.state[key];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call custom reset handler if provided
|
|
||||||
if (options.onReset) {
|
|
||||||
options.onReset.call(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Simple Modal Component (no form)
|
|
||||||
*
|
|
||||||
* For modals that don't need form validation.
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* <dialog x-data="modal()">
|
|
||||||
* <button @click="$el.closest('dialog').close()">Close</button>
|
|
||||||
* </dialog>
|
|
||||||
*/
|
|
||||||
Alpine.data('modal', () => ({
|
|
||||||
// Placeholder for simple modals that may need state later
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
@ -1,148 +0,0 @@
|
|||||||
/**
|
|
||||||
* Alpine.js Pagination Component
|
|
||||||
*
|
|
||||||
* Provides client-side pagination for large lists.
|
|
||||||
*/
|
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
Alpine.data('remotePaginatedList', (options = {}) => ({
|
|
||||||
items: [],
|
|
||||||
currentPage: 1,
|
|
||||||
totalPages: 0,
|
|
||||||
totalItems: 0,
|
|
||||||
perPage: options.perPage || 50,
|
|
||||||
endpoint: options.endpoint || '',
|
|
||||||
isReviewed: options.isReviewed || false,
|
|
||||||
instanceId: options.instanceId || Math.random().toString(36).substring(2, 9),
|
|
||||||
isLoading: false,
|
|
||||||
error: null,
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.endpoint) {
|
|
||||||
this.fetchPage(1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
get paginatedItems() {
|
|
||||||
return this.items;
|
|
||||||
},
|
|
||||||
|
|
||||||
get showingStart() {
|
|
||||||
if (this.totalItems === 0) return 0;
|
|
||||||
return (this.currentPage - 1) * this.perPage + 1;
|
|
||||||
},
|
|
||||||
|
|
||||||
get showingEnd() {
|
|
||||||
if (this.totalItems === 0) return 0;
|
|
||||||
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
|
|
||||||
},
|
|
||||||
|
|
||||||
async fetchPage(page) {
|
|
||||||
if (!this.endpoint) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.isLoading = true;
|
|
||||||
this.error = null;
|
|
||||||
this.$dispatch('loading-start');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const url = new URL(this.endpoint, window.location.origin);
|
|
||||||
// Preserve existing query parameters and add pagination params
|
|
||||||
url.searchParams.set('page', page.toString());
|
|
||||||
url.searchParams.set('page_size', this.perPage.toString());
|
|
||||||
|
|
||||||
const response = await fetch(url.toString(), {
|
|
||||||
headers: { Accept: 'application/json' },
|
|
||||||
credentials: 'same-origin'
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`Failed to load ${this.endpoint} (status ${response.status})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
this.items = data.items || [];
|
|
||||||
this.totalItems = data.total_items || 0;
|
|
||||||
this.totalPages = data.total_pages || 0;
|
|
||||||
this.currentPage = data.page || page;
|
|
||||||
this.perPage = data.page_size || this.perPage;
|
|
||||||
|
|
||||||
// Dispatch event for parent components (e.g., tab count updates)
|
|
||||||
this.$dispatch('items-loaded', { totalItems: this.totalItems });
|
|
||||||
} catch (err) {
|
|
||||||
console.error(err);
|
|
||||||
this.error = `Unable to load ${this.endpoint}. Please try again.`;
|
|
||||||
} finally {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.$dispatch('loading-end');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
nextPage() {
|
|
||||||
if (this.currentPage < this.totalPages) {
|
|
||||||
this.fetchPage(this.currentPage + 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
prevPage() {
|
|
||||||
if (this.currentPage > 1) {
|
|
||||||
this.fetchPage(this.currentPage - 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
goToPage(page) {
|
|
||||||
if (page >= 1 && page <= this.totalPages) {
|
|
||||||
this.fetchPage(page);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
get pageNumbers() {
|
|
||||||
const pages = [];
|
|
||||||
const total = this.totalPages;
|
|
||||||
const current = this.currentPage;
|
|
||||||
|
|
||||||
if (total === 0) {
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (total <= 7) {
|
|
||||||
for (let i = 1; i <= total; i++) {
|
|
||||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
|
|
||||||
|
|
||||||
let rangeStart;
|
|
||||||
let rangeEnd;
|
|
||||||
|
|
||||||
if (current <= 4) {
|
|
||||||
rangeStart = 2;
|
|
||||||
rangeEnd = 5;
|
|
||||||
} else if (current >= total - 3) {
|
|
||||||
rangeStart = total - 4;
|
|
||||||
rangeEnd = total - 1;
|
|
||||||
} else {
|
|
||||||
rangeStart = current - 1;
|
|
||||||
rangeEnd = current + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeStart > 2) {
|
|
||||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
|
|
||||||
}
|
|
||||||
|
|
||||||
for (let i = rangeStart; i <= rangeEnd; i++) {
|
|
||||||
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (rangeEnd < total - 1) {
|
|
||||||
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
|
|
||||||
}
|
|
||||||
|
|
||||||
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
|
|
||||||
}
|
|
||||||
|
|
||||||
return pages;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
@ -1,106 +0,0 @@
|
|||||||
/**
|
|
||||||
* Pathway Viewer Alpine.js Component
|
|
||||||
*
|
|
||||||
* Provides reactive status management and polling for pathway predictions.
|
|
||||||
* Handles status updates, change detection, and update notices.
|
|
||||||
*/
|
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
/**
|
|
||||||
* Pathway Viewer Component
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* <div x-data="pathwayViewer({
|
|
||||||
* status: 'running',
|
|
||||||
* modified: '2024-01-01T00:00:00Z',
|
|
||||||
* statusUrl: '/pathway/123?status=true'
|
|
||||||
* })" x-init="init()">
|
|
||||||
* ...
|
|
||||||
* </div>
|
|
||||||
*/
|
|
||||||
Alpine.data('pathwayViewer', (config) => ({
|
|
||||||
status: config.status,
|
|
||||||
modified: config.modified,
|
|
||||||
statusUrl: config.statusUrl,
|
|
||||||
emptyDueToThreshold: config.emptyDueToThreshold === "True",
|
|
||||||
showUpdateNotice: false,
|
|
||||||
showEmptyDueToThresholdNotice: false,
|
|
||||||
emptyDueToThresholdMessage: 'The Pathway is empty due to the selected threshold. Please try a different threshold.',
|
|
||||||
updateMessage: '',
|
|
||||||
pollInterval: null,
|
|
||||||
|
|
||||||
get statusTooltip() {
|
|
||||||
const tooltips = {
|
|
||||||
'completed': 'Pathway prediction completed.',
|
|
||||||
'failed': 'Pathway prediction failed.',
|
|
||||||
'running': 'Pathway prediction running.'
|
|
||||||
};
|
|
||||||
return tooltips[this.status] || '';
|
|
||||||
},
|
|
||||||
|
|
||||||
init() {
|
|
||||||
if (this.status === 'running') {
|
|
||||||
this.startPolling();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.emptyDueToThreshold) {
|
|
||||||
this.showEmptyDueToThresholdNotice = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
|
|
||||||
startPolling() {
|
|
||||||
if (this.pollInterval) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.pollInterval = setInterval(() => this.checkStatus(), 5000);
|
|
||||||
},
|
|
||||||
|
|
||||||
async checkStatus() {
|
|
||||||
try {
|
|
||||||
const response = await fetch(this.statusUrl);
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
if (data.emptyDueToThreshold) {
|
|
||||||
this.emptyDueToThreshold = true;
|
|
||||||
this.showEmptyDueToThresholdNotice = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.modified > this.modified) {
|
|
||||||
if (!this.emptyDueToThreshold) {
|
|
||||||
this.showUpdateNotice = true;
|
|
||||||
this.updateMessage = this.getUpdateMessage(data.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (data.status !== 'running') {
|
|
||||||
this.status = data.status;
|
|
||||||
if (this.pollInterval) {
|
|
||||||
clearInterval(this.pollInterval);
|
|
||||||
this.pollInterval = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Polling error:', err);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
getUpdateMessage(status) {
|
|
||||||
let msg = 'Prediction ';
|
|
||||||
|
|
||||||
if (status === 'running') {
|
|
||||||
msg += 'is still running. But the Pathway was updated.';
|
|
||||||
} else if (status === 'completed') {
|
|
||||||
msg += 'is completed. Reload the page to see the updated Pathway.';
|
|
||||||
} else if (status === 'failed') {
|
|
||||||
msg += 'failed. Reload the page to see the current shape.';
|
|
||||||
}
|
|
||||||
|
|
||||||
return msg;
|
|
||||||
},
|
|
||||||
|
|
||||||
reloadPage() {
|
|
||||||
location.reload();
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
/**
|
|
||||||
* Search Modal Alpine.js Component
|
|
||||||
*
|
|
||||||
* Provides package selection, search mode switching, and results display
|
|
||||||
* for the search modal.
|
|
||||||
*/
|
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
/**
|
|
||||||
* Search Modal Component
|
|
||||||
*
|
|
||||||
* Usage:
|
|
||||||
* <dialog x-data="searchModal()" @close="reset()">
|
|
||||||
* ...
|
|
||||||
* </dialog>
|
|
||||||
*/
|
|
||||||
Alpine.data('searchModal', () => ({
|
|
||||||
// Package selector state
|
|
||||||
selectedPackages: [],
|
|
||||||
|
|
||||||
// Search state
|
|
||||||
searchMode: 'text',
|
|
||||||
searchModeLabel: 'Text',
|
|
||||||
query: '',
|
|
||||||
|
|
||||||
// Results state
|
|
||||||
results: null,
|
|
||||||
isSearching: false,
|
|
||||||
error: null,
|
|
||||||
|
|
||||||
// Initialize on modal open
|
|
||||||
init() {
|
|
||||||
// Load reviewed packages by default
|
|
||||||
this.loadInitialSelection();
|
|
||||||
|
|
||||||
// Watch for modal open to focus searchbar
|
|
||||||
this.$watch('$el.open', (open) => {
|
|
||||||
if (open) {
|
|
||||||
setTimeout(() => {
|
|
||||||
this.$refs.searchbar.focus();
|
|
||||||
}, 320);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
loadInitialSelection() {
|
|
||||||
// Select all reviewed packages by default
|
|
||||||
const menuItems = this.$refs.packageDropdown.querySelectorAll('li');
|
|
||||||
|
|
||||||
for (const item of menuItems) {
|
|
||||||
// Stop at 'Unreviewed Packages' section
|
|
||||||
if (item.classList.contains('menu-title') &&
|
|
||||||
item.textContent.trim() === 'Unreviewed Packages') {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const packageOption = item.querySelector('.package-option');
|
|
||||||
if (packageOption) {
|
|
||||||
this.selectedPackages.push({
|
|
||||||
url: packageOption.dataset.packageUrl,
|
|
||||||
name: packageOption.dataset.packageName
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
togglePackage(url, name) {
|
|
||||||
const index = this.selectedPackages.findIndex(pkg => pkg.url === url);
|
|
||||||
|
|
||||||
if (index !== -1) {
|
|
||||||
this.selectedPackages.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
this.selectedPackages.push({ url, name });
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
removePackage(url) {
|
|
||||||
const index = this.selectedPackages.findIndex(pkg => pkg.url === url);
|
|
||||||
if (index !== -1) {
|
|
||||||
this.selectedPackages.splice(index, 1);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
isPackageSelected(url) {
|
|
||||||
return this.selectedPackages.some(pkg => pkg.url === url);
|
|
||||||
},
|
|
||||||
|
|
||||||
setSearchMode(mode, label) {
|
|
||||||
this.searchMode = mode;
|
|
||||||
this.searchModeLabel = label;
|
|
||||||
this.$refs.modeDropdown.hidePopover();
|
|
||||||
},
|
|
||||||
|
|
||||||
async performSearch(serverBase) {
|
|
||||||
if (!this.query.trim()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.selectedPackages.length < 1) {
|
|
||||||
this.results = { error: 'no_packages' };
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
this.selectedPackages.forEach(pkg => params.append('packages', pkg.url));
|
|
||||||
params.append('search', this.query.trim());
|
|
||||||
params.append('mode', this.searchModeLabel.toLowerCase());
|
|
||||||
|
|
||||||
this.isSearching = true;
|
|
||||||
this.results = null;
|
|
||||||
this.error = null;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await fetch(`${serverBase}/search?${params.toString()}`, {
|
|
||||||
method: 'GET',
|
|
||||||
headers: { 'Accept': 'application/json' }
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Search request failed');
|
|
||||||
}
|
|
||||||
|
|
||||||
this.results = await response.json();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Search error:', err);
|
|
||||||
this.error = 'Search failed. Please try again.';
|
|
||||||
} finally {
|
|
||||||
this.isSearching = false;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
hasResults() {
|
|
||||||
if (!this.results || this.results.error) return false;
|
|
||||||
const categories = ['Compounds', 'Compound Structures', 'Rules', 'Reactions', 'Pathways'];
|
|
||||||
return categories.some(cat => this.results[cat] && this.results[cat].length > 0);
|
|
||||||
},
|
|
||||||
|
|
||||||
reset() {
|
|
||||||
this.query = '';
|
|
||||||
this.results = null;
|
|
||||||
this.error = null;
|
|
||||||
this.isSearching = false;
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
@ -63,20 +63,17 @@ class DiscourseAPI {
|
|||||||
* @returns {string} Cleaned excerpt
|
* @returns {string} Cleaned excerpt
|
||||||
*/
|
*/
|
||||||
extractExcerpt(excerpt) {
|
extractExcerpt(excerpt) {
|
||||||
if (!excerpt) return 'No preview available yet';
|
if (!excerpt) return 'Click to read more';
|
||||||
|
|
||||||
// Remove HTML tags and clean up; collapse whitespace; do not add manual ellipsis
|
// Remove HTML tags and clean up; collapse whitespace; do not add manual ellipsis
|
||||||
const cleaned = excerpt
|
return excerpt
|
||||||
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
.replace(/<[^>]*>/g, '') // Remove HTML tags
|
||||||
.replace(/ /g, ' ') // Replace with spaces
|
.replace(/ /g, ' ') // Replace with spaces
|
||||||
.replace(/&/g, '&') // Replace & with &
|
.replace(/&/g, '&') // Replace & with &
|
||||||
.replace(/</g, '<') // Replace < with <
|
.replace(/</g, '<') // Replace < with <
|
||||||
.replace(/>/g, '>') // Replace > with >
|
.replace(/>/g, '>') // Replace > with >
|
||||||
.replace(/\s+/g, ' ') // Collapse all whitespace/newlines
|
.replace(/\s+/g, ' ') // Collapse all whitespace/newlines
|
||||||
.trim();
|
.trim()
|
||||||
|
|
||||||
// Check if excerpt is empty after cleaning
|
|
||||||
return cleaned || 'No preview available yet';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
1291
static/js/pps.js
1291
static/js/pps.js
File diff suppressed because it is too large
Load Diff
237
static/js/pw.js
237
static/js/pw.js
@ -1,21 +1,14 @@
|
|||||||
console.log("loaded pw.js")
|
console.log("loaded pw.js")
|
||||||
|
|
||||||
function predictFromNode(url) {
|
function predictFromNode(url) {
|
||||||
fetch("", {
|
$.post("", {node: url})
|
||||||
method: "POST",
|
.done(function (data) {
|
||||||
headers: {
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
"X-CSRFToken": document.querySelector('[name=csrfmiddlewaretoken]')?.value || ''
|
|
||||||
},
|
|
||||||
body: new URLSearchParams({node: url})
|
|
||||||
})
|
|
||||||
.then(response => response.json())
|
|
||||||
.then(data => {
|
|
||||||
console.log("Success:", data);
|
console.log("Success:", data);
|
||||||
window.location.href = data.success;
|
window.location.href = data.success;
|
||||||
})
|
})
|
||||||
.catch(error => {
|
.fail(function (xhr, status, error) {
|
||||||
console.error("Error:", error);
|
console.error("Error:", xhr.status, xhr.responseText);
|
||||||
|
// show user-friendly message or log error
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,9 +103,6 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dragstarted(event, d) {
|
function dragstarted(event, d) {
|
||||||
// Prevent zoom pan when dragging nodes
|
|
||||||
event.sourceEvent.stopPropagation();
|
|
||||||
|
|
||||||
if (!event.active) simulation.alphaTarget(0.3).restart();
|
if (!event.active) simulation.alphaTarget(0.3).restart();
|
||||||
d.fx = d.x;
|
d.fx = d.x;
|
||||||
d.fy = d.y;
|
d.fy = d.y;
|
||||||
@ -127,9 +117,6 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dragged(event, d) {
|
function dragged(event, d) {
|
||||||
// Prevent zoom pan when dragging nodes
|
|
||||||
event.sourceEvent.stopPropagation();
|
|
||||||
|
|
||||||
d.fx = event.x;
|
d.fx = event.x;
|
||||||
d.fy = event.y;
|
d.fy = event.y;
|
||||||
|
|
||||||
@ -140,9 +127,6 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dragended(event, d) {
|
function dragended(event, d) {
|
||||||
// Prevent zoom pan when dragging nodes
|
|
||||||
event.sourceEvent.stopPropagation();
|
|
||||||
|
|
||||||
if (!event.active) simulation.alphaTarget(0);
|
if (!event.active) simulation.alphaTarget(0);
|
||||||
|
|
||||||
// Mark that dragging has ended
|
// Mark that dragging has ended
|
||||||
@ -208,163 +192,58 @@ function draw(pathway, elem) {
|
|||||||
d3.select(t).select("circle").classed("highlighted", !d3.select(t).select("circle").classed("highlighted"));
|
d3.select(t).select("circle").classed("highlighted", !d3.select(t).select("circle").classed("highlighted"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait before showing popup (ms)
|
// Wait one second before showing popup
|
||||||
var popupWaitBeforeShow = 1000;
|
var popupWaitBeforeShow = 1000;
|
||||||
|
// Keep Popup at least for one second
|
||||||
|
var popushowAtLeast = 1000;
|
||||||
|
|
||||||
// Custom popover element
|
function pop_show_e(element) {
|
||||||
let popoverTimeout = null;
|
var e = element;
|
||||||
|
setTimeout(function () {
|
||||||
|
if ($(e).is(':hover')) { // if element is still hovered
|
||||||
|
$(e).popover("show");
|
||||||
|
|
||||||
function createPopover() {
|
// workaround to set fixed positions
|
||||||
const popover = document.createElement('div');
|
pop = $(e).attr("aria-describedby")
|
||||||
popover.id = 'custom-popover';
|
h = $('#' + pop).height();
|
||||||
popover.className = 'fixed z-50';
|
$('#' + pop).attr("style", `position: fixed; top: ${clientY - (h / 2.0)}px; left: ${clientX + 10}px; margin: 0px; max-width: 1000px; display: block;`)
|
||||||
popover.style.cssText = `
|
setTimeout(function () {
|
||||||
background: #ffffff;
|
var close = setInterval(function () {
|
||||||
border: 1px solid #d1d5db;
|
if (!$(".popover:hover").length // mouse outside popover
|
||||||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
|
&& !$(e).is(':hover')) { // mouse outside element
|
||||||
max-width: 320px;
|
$(e).popover('hide');
|
||||||
padding: 0.75rem;
|
clearInterval(close);
|
||||||
border-radius: 0.5rem;
|
|
||||||
opacity: 0;
|
|
||||||
visibility: hidden;
|
|
||||||
transition: opacity 150ms ease-in-out, visibility 150ms ease-in-out;
|
|
||||||
pointer-events: auto;
|
|
||||||
`;
|
|
||||||
popover.setAttribute('role', 'tooltip');
|
|
||||||
popover.innerHTML = `
|
|
||||||
<div class="font-semibold mb-2 popover-title" style="font-weight: 600; margin-bottom: 0.5rem;"></div>
|
|
||||||
<div class="text-sm popover-content" style="font-size: 0.875rem;"></div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// Add styles for content images
|
|
||||||
const style = document.createElement('style');
|
|
||||||
style.textContent = `
|
|
||||||
#custom-popover img {
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
display: block;
|
|
||||||
margin: 0.5rem 0;
|
|
||||||
}
|
}
|
||||||
#custom-popover a {
|
}, 100);
|
||||||
color: #2563eb;
|
}, popushowAtLeast);
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
#custom-popover a:hover {
|
}, popupWaitBeforeShow);
|
||||||
text-decoration: underline;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
if (!document.getElementById('popover-styles')) {
|
|
||||||
style.id = 'popover-styles';
|
|
||||||
document.head.appendChild(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Keep popover open when hovering over it
|
|
||||||
popover.addEventListener('mouseenter', () => {
|
|
||||||
if (popoverTimeout) {
|
|
||||||
clearTimeout(popoverTimeout);
|
|
||||||
popoverTimeout = null;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
popover.addEventListener('mouseleave', () => {
|
|
||||||
hidePopover();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.body.appendChild(popover);
|
|
||||||
return popover;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPopover() {
|
|
||||||
return document.getElementById('custom-popover') || createPopover();
|
|
||||||
}
|
|
||||||
|
|
||||||
function showPopover(element, title, content) {
|
|
||||||
const popover = getPopover();
|
|
||||||
popover.querySelector('.popover-title').textContent = title;
|
|
||||||
popover.querySelector('.popover-content').innerHTML = content;
|
|
||||||
|
|
||||||
// Make visible to measure
|
|
||||||
popover.style.visibility = 'hidden';
|
|
||||||
popover.style.opacity = '0';
|
|
||||||
popover.style.display = 'block';
|
|
||||||
|
|
||||||
// Smart positioning - avoid viewport overflow
|
|
||||||
const padding = 10;
|
|
||||||
const popoverRect = popover.getBoundingClientRect();
|
|
||||||
const viewportWidth = window.innerWidth;
|
|
||||||
const viewportHeight = window.innerHeight;
|
|
||||||
|
|
||||||
let left = clientX + 15;
|
|
||||||
let top = clientY - (popoverRect.height / 2);
|
|
||||||
|
|
||||||
// Prevent right overflow
|
|
||||||
if (left + popoverRect.width > viewportWidth - padding) {
|
|
||||||
left = clientX - popoverRect.width - 15;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent bottom overflow
|
|
||||||
if (top + popoverRect.height > viewportHeight - padding) {
|
|
||||||
top = viewportHeight - popoverRect.height - padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevent top overflow
|
|
||||||
if (top < padding) {
|
|
||||||
top = padding;
|
|
||||||
}
|
|
||||||
|
|
||||||
popover.style.top = `${top}px`;
|
|
||||||
popover.style.left = `${left}px`;
|
|
||||||
popover.style.visibility = 'visible';
|
|
||||||
popover.style.opacity = '1';
|
|
||||||
|
|
||||||
currentElement = element;
|
|
||||||
}
|
|
||||||
|
|
||||||
function hidePopover() {
|
|
||||||
const popover = getPopover();
|
|
||||||
popover.style.opacity = '0';
|
|
||||||
popover.style.visibility = 'hidden';
|
|
||||||
currentElement = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function pop_add(objects, title, contentFunction) {
|
function pop_add(objects, title, contentFunction) {
|
||||||
objects.each(function (d) {
|
objects.attr("id", "pop")
|
||||||
const element = this;
|
.attr("data-container", "body")
|
||||||
|
.attr("data-toggle", "popover")
|
||||||
|
.attr("data-placement", "right")
|
||||||
|
.attr("title", title);
|
||||||
|
|
||||||
element.addEventListener('mouseenter', () => {
|
objects.each(function (d, i) {
|
||||||
if (popoverTimeout) clearTimeout(popoverTimeout);
|
options = {trigger: "manual", html: true, animation: false};
|
||||||
|
this_ = this;
|
||||||
popoverTimeout = setTimeout(() => {
|
var p = $(this).popover(options).on("mouseenter", function () {
|
||||||
if (element.matches(':hover')) {
|
pop_show_e(this);
|
||||||
const content = contentFunction(d);
|
|
||||||
showPopover(element, title, content);
|
|
||||||
}
|
|
||||||
}, popupWaitBeforeShow);
|
|
||||||
});
|
});
|
||||||
|
p.on("show.bs.popover", function (e) {
|
||||||
element.addEventListener('mouseleave', () => {
|
// this is to dynamically ajdust the content and bounds of the popup
|
||||||
if (popoverTimeout) {
|
p.attr('data-content', contentFunction(d));
|
||||||
clearTimeout(popoverTimeout);
|
p.data("bs.popover").setContent();
|
||||||
popoverTimeout = null;
|
p.data("bs.popover").tip().css({"max-width": "1000px"});
|
||||||
}
|
|
||||||
|
|
||||||
// Delay hide to allow moving to popover
|
|
||||||
setTimeout(() => {
|
|
||||||
const popover = getPopover();
|
|
||||||
if (!popover.matches(':hover') && !element.matches(':hover')) {
|
|
||||||
hidePopover();
|
|
||||||
}
|
|
||||||
}, 100);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function node_popup(n) {
|
function node_popup(n) {
|
||||||
popupContent = "";
|
popupContent = "<a href='" + n.url + "'>" + n.name + "</a><br>";
|
||||||
if (n.stereo_removed) {
|
|
||||||
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
|
||||||
}
|
|
||||||
popupContent += "<a href='" + n.url + "'>" + n.name + "</a><br>";
|
|
||||||
popupContent += "Depth " + n.depth + "<br>"
|
popupContent += "Depth " + n.depth + "<br>"
|
||||||
|
|
||||||
if (appDomainViewEnabled) {
|
if (appDomainViewEnabled) {
|
||||||
@ -376,7 +255,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
popupContent += "<img src='" + n.image + "'><br>"
|
popupContent += "<img src='" + n.image + "' width='" + 20 * nodeRadius + "'><br>"
|
||||||
if (n.scenarios.length > 0) {
|
if (n.scenarios.length > 0) {
|
||||||
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
||||||
for (var s of n.scenarios) {
|
for (var s of n.scenarios) {
|
||||||
@ -386,7 +265,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0;
|
var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0;
|
||||||
if (pathway.isIncremental && isLeaf) {
|
if (pathway.isIncremental && isLeaf) {
|
||||||
popupContent += '<br><a class="btn btn-primary btn-sm mt-2" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>';
|
popupContent += '<br><a class="btn btn-primary" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>';
|
||||||
}
|
}
|
||||||
|
|
||||||
return popupContent;
|
return popupContent;
|
||||||
@ -406,7 +285,7 @@ function draw(pathway, elem) {
|
|||||||
popupContent += adcontent;
|
popupContent += adcontent;
|
||||||
}
|
}
|
||||||
|
|
||||||
popupContent += "<img src='" + e.image + "'><br>"
|
popupContent += "<img src='" + e.image + "' width='" + 20 * nodeRadius + "'><br>"
|
||||||
if (e.reaction_probability) {
|
if (e.reaction_probability) {
|
||||||
popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>';
|
popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>';
|
||||||
}
|
}
|
||||||
@ -429,23 +308,6 @@ function draw(pathway, elem) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const zoomable = d3.select("#zoomable");
|
const zoomable = d3.select("#zoomable");
|
||||||
const svg = d3.select("#pwsvg");
|
|
||||||
const container = d3.select("#vizdiv");
|
|
||||||
|
|
||||||
// Set explicit SVG dimensions for proper zoom behavior
|
|
||||||
svg.attr("width", width)
|
|
||||||
.attr("height", height);
|
|
||||||
|
|
||||||
// Add background rectangle FIRST to enable pan/zoom on empty space
|
|
||||||
// This must be inserted before zoomable group so it's behind everything
|
|
||||||
svg.insert("rect", "#zoomable")
|
|
||||||
.attr("x", 0)
|
|
||||||
.attr("y", 0)
|
|
||||||
.attr("width", width)
|
|
||||||
.attr("height", height)
|
|
||||||
.attr("fill", "transparent")
|
|
||||||
.attr("pointer-events", "all")
|
|
||||||
.style("cursor", "grab");
|
|
||||||
|
|
||||||
// Zoom Funktion aktivieren
|
// Zoom Funktion aktivieren
|
||||||
const zoom = d3.zoom()
|
const zoom = d3.zoom()
|
||||||
@ -454,12 +316,7 @@ function draw(pathway, elem) {
|
|||||||
zoomable.attr("transform", event.transform);
|
zoomable.attr("transform", event.transform);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Apply zoom to the SVG element - this enables wheel zoom
|
d3.select("svg").call(zoom);
|
||||||
svg.call(zoom);
|
|
||||||
|
|
||||||
// Also apply zoom to container to catch events that might not reach SVG
|
|
||||||
// This ensures drag-to-pan works even when clicking on empty space
|
|
||||||
container.call(zoom);
|
|
||||||
|
|
||||||
nodes = pathway['nodes'];
|
nodes = pathway['nodes'];
|
||||||
links = pathway['links'];
|
links = pathway['links'];
|
||||||
@ -524,7 +381,7 @@ function draw(pathway, elem) {
|
|||||||
node.append("circle")
|
node.append("circle")
|
||||||
// make radius "invisible" for pseudo nodes
|
// make radius "invisible" for pseudo nodes
|
||||||
.attr("r", d => d.pseudo ? 0.01 : nodeRadius)
|
.attr("r", d => d.pseudo ? 0.01 : nodeRadius)
|
||||||
.style("fill", d => d.is_engineered_intermediate ? "#42eff5" : "#e8e8e8");
|
.style("fill", "#e8e8e8");
|
||||||
|
|
||||||
// Add image only for non pseudo nodes
|
// Add image only for non pseudo nodes
|
||||||
node.filter(d => !d.pseudo).each(function (d, i) {
|
node.filter(d => !d.pseudo).each(function (d, i) {
|
||||||
|
|||||||
6
templates/actions/collections/compound.html
Normal file
6
templates/actions/collections/compound.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% if meta.can_edit %}
|
||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#new_compound_modal">
|
||||||
|
<span class="glyphicon glyphicon-plus"></span> New Compound</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
6
templates/actions/collections/compound_structure.html
Normal file
6
templates/actions/collections/compound_structure.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% if meta.can_edit %}
|
||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#new_compound_structure_modal">
|
||||||
|
<span class="glyphicon glyphicon-plus"></span> New Compound Structure</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
@ -1,10 +1,6 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#new_edge_modal">
|
||||||
role="button"
|
<span class="glyphicon glyphicon-plus"></span> New Edge</a>
|
||||||
onclick="document.getElementById('new_edge_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<span class="glyphicon glyphicon-plus"></span> New Edge</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -1,8 +1,4 @@
|
|||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#new_group_modal">
|
||||||
role="button"
|
<span class="glyphicon glyphicon-plus"></span> New Group</a>
|
||||||
onclick="document.getElementById('new_group_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<span class="glyphicon glyphicon-plus"></span> New Group</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
6
templates/actions/collections/model.html
Normal file
6
templates/actions/collections/model.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% if meta.can_edit and meta.enabled_features.MODEL_BUILDING %}
|
||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#new_model_modal">
|
||||||
|
<span class="glyphicon glyphicon-plus"></span> New Model</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
@ -1,10 +1,6 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#new_node_modal">
|
||||||
role="button"
|
<span class="glyphicon glyphicon-plus"></span> New Node</a>
|
||||||
onclick="document.getElementById('new_node_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<span class="glyphicon glyphicon-plus"></span> New Node</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
12
templates/actions/collections/package.html
Normal file
12
templates/actions/collections/package.html
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#new_package_modal">
|
||||||
|
<span class="glyphicon glyphicon-plus"></span> New Package</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#import_package_modal">
|
||||||
|
<span class="glyphicon glyphicon-import"></span> Import Package from JSON</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#import_legacy_package_modal">
|
||||||
|
<span class="glyphicon glyphicon-import"></span> Import Package from legacy JSON</a>
|
||||||
|
</li>
|
||||||
9
templates/actions/collections/pathway.html
Normal file
9
templates/actions/collections/pathway.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% if meta.can_edit %}
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="{% if meta.current_package %}{{ meta.current_package.url }}/predict{% else %}{{ meta.server_url }}/predict{% endif %}"
|
||||||
|
>
|
||||||
|
<span class="glyphicon glyphicon-plus"></span> New Pathway</a
|
||||||
|
>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
6
templates/actions/collections/reaction.html
Normal file
6
templates/actions/collections/reaction.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% if meta.can_edit %}
|
||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#new_reaction_modal">
|
||||||
|
<span class="glyphicon glyphicon-plus"></span> New Reaction</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
6
templates/actions/collections/rule.html
Normal file
6
templates/actions/collections/rule.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% if meta.can_edit %}
|
||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#new_rule_modal">
|
||||||
|
<span class="glyphicon glyphicon-plus"></span> New Rule</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
6
templates/actions/collections/scenario.html
Normal file
6
templates/actions/collections/scenario.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% if meta.can_edit %}
|
||||||
|
<li>
|
||||||
|
<a role="button" data-toggle="modal" data-target="#new_scenario_modal">
|
||||||
|
<span class="glyphicon glyphicon-plus"></span> New Scenario</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
@ -1,10 +1,6 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#new_setting_modal">
|
||||||
role="button"
|
<span class="glyphicon glyphicon-plus"></span>New Setting</a>
|
||||||
onclick="document.getElementById('new_setting_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<span class="glyphicon glyphicon-plus"></span>New Setting</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -1,60 +1,32 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_compound_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-edit"></i> Edit Compound</a>
|
||||||
onclick="document.getElementById('edit_compound_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-edit"></i> Edit Compound</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
|
||||||
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#add_structure_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Add Structure</a>
|
||||||
onclick="document.getElementById('add_structure_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Add Structure</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||||
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#generic_set_external_reference_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a>
|
||||||
onclick="document.getElementById('generic_set_external_reference_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||||
onclick="document.getElementById('generic_copy_object_modal').showModal(); return false;"
|
</li>
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,42 +1,22 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_compound_structure_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-edit"></i> Edit Compound Structure</a>
|
||||||
onclick="document.getElementById('edit_compound_structure_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-edit"></i> Edit Compound Structure</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
|
||||||
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||||
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#generic_set_external_reference_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a>
|
||||||
onclick="document.getElementById('generic_set_external_reference_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Compound Structure</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound Structure</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,26 +1,14 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
|
||||||
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||||
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Edge</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Edge</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,18 +1,10 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_group_member_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-trash"></i> Add/Remove Member</a>
|
||||||
onclick="document.getElementById('edit_group_member_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Add/Remove Member</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Group</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Group</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{% if job.is_result_downloadable %}
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
onclick="document.getElementById('download_job_result_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-floppy-save"></i> Download Result</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
|
||||||
@ -1,38 +1,18 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_model_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-edit"></i> Edit Model</a>
|
||||||
onclick="document.getElementById('edit_model_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-edit"></i> Edit Model</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% if model.model_status == 'BUILT_NOT_EVALUATED' or model.model_status == 'FINISHED' %}
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#evaluate_model_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-ok"></i> Evaluate Model</a>
|
||||||
onclick="document.getElementById('evaluate_model_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-ok"></i> Evaluate Model</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
{% if model.model_status == 'BUILT_NOT_EVALUATED' or model.model_status == 'FINISHED' %}
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#retrain_model_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-repeat"></i> Retrain Model</a>
|
||||||
onclick="document.getElementById('retrain_model_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-repeat"></i> Retrain Model</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Model</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Model</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -1,34 +1,18 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_node_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-edit"></i> Edit Node</a>
|
||||||
onclick="document.getElementById('edit_node_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-edit"></i> Edit Node</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
|
||||||
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||||
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Node</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Node</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,50 +1,26 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_package_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-edit"></i> Edit Package</a>
|
||||||
onclick="document.getElementById('edit_package_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-edit"></i> Edit Package</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_package_permissions_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-user"></i> Edit Permissions</a>
|
||||||
onclick="document.getElementById('edit_package_permissions_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-user"></i> Edit Permissions</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#publish_package_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-bullhorn"></i> Publish Package</a>
|
||||||
onclick="document.getElementById('publish_package_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-bullhorn"></i> Publish Package</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#export_package_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-bullhorn"></i> Export Package as JSON</a>
|
||||||
onclick="document.getElementById('export_package_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-bullhorn"></i> Export Package as JSON</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_license_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-duplicate"></i> License</a>
|
||||||
onclick="document.getElementById('set_license_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-duplicate"></i> License</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Package</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Package</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -1,112 +1,59 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#add_pathway_node_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-plus"></i> Add Compound</a>
|
||||||
onclick="document.getElementById('add_pathway_node_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Add Compound</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a>
|
||||||
onclick="document.getElementById('add_pathway_edge_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider"></li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||||
onclick="document.getElementById('generic_copy_object_modal').showModal(); return false;"
|
</li>
|
||||||
>
|
<li>
|
||||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a
|
<a class="button" data-toggle="modal" data-target="#download_pathway_csv_modal">
|
||||||
>
|
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as CSV</a>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#download_pathway_image_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as Image</a>
|
||||||
onclick="document.getElementById('download_pathway_csv_modal').showModal(); return false;"
|
</li>
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as CSV</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
class="button"
|
|
||||||
onclick="document.getElementById('download_pathway_image_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway as Image</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
role="button"
|
|
||||||
onclick="document.getElementById('engineer_pathway_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-cog"></i> Engineer Pathway</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#identify_missing_rules_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-question-sign"></i> Identify Missing Rules</a>
|
||||||
onclick="document.getElementById('identify_missing_rules_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-question-sign"></i> Identify Missing
|
|
||||||
Rules</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider"></li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#edit_pathway_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-edit"></i> Edit Pathway</a>
|
||||||
onclick="document.getElementById('edit_pathway_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-edit"></i> Edit Pathway</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||||
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
|
||||||
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
|
{# <li>#}
|
||||||
|
{# <a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal">#}
|
||||||
|
{# <i class="glyphicon glyphicon-plus"></i> Calculate Compound Properties</a>#}
|
||||||
|
{# </li>#}
|
||||||
<li role="separator" class="divider"></li>
|
<li role="separator" class="divider"></li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#delete_pathway_node_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
|
||||||
onclick="document.getElementById('delete_pathway_node_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#delete_pathway_edge_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
|
||||||
onclick="document.getElementById('delete_pathway_edge_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Pathway</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Pathway</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,52 +1,28 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_reaction_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-edit"></i> Edit Reaction</a>
|
||||||
onclick="document.getElementById('edit_reaction_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-edit"></i> Edit Reaction</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
|
||||||
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||||
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#generic_set_external_reference_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a>
|
||||||
onclick="document.getElementById('generic_set_external_reference_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set External Reference</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||||
onclick="document.getElementById('generic_copy_object_modal').showModal(); return false;"
|
</li>
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,44 +1,24 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_rule_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-edit"></i> Edit Rule</a>
|
||||||
onclick="document.getElementById('edit_rule_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-edit"></i> Edit Rule</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_aliases_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a>
|
||||||
onclick="document.getElementById('set_aliases_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Aliases</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||||
onclick="document.getElementById('set_scenario_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||||
onclick="document.getElementById('generic_copy_object_modal').showModal(); return false;"
|
</li>
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Rule</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Rule</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|||||||
@ -1,26 +1,14 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#add_additional_information_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Add Additional Information</a>
|
||||||
onclick="document.getElementById('add_additional_information_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Add Additional Information</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#update_scenario_additional_information_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Set Additional Information</a>
|
||||||
onclick="document.getElementById('update_scenario_additional_information_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Set Additional Information</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Scenario</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Scenario</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -1,38 +1,22 @@
|
|||||||
{% if meta.can_edit %}
|
{% if meta.can_edit %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_user_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-edit"></i> Update</a>
|
||||||
onclick="document.getElementById('edit_user_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-edit"></i> Update</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#edit_password_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-lock"></i> Update Password</a>
|
||||||
onclick="document.getElementById('edit_password_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-lock"></i> Update Password</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#new_prediction_setting_modal">
|
||||||
role="button"
|
<i class="glyphicon glyphicon-plus"></i> New Prediction Setting</a>
|
||||||
onclick="document.getElementById('new_prediction_setting_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-plus"></i> New Prediction Setting</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{# <li>#}
|
{# <li>#}
|
||||||
{# <a role="button" data-toggle="modal" data-target="#manage_api_token_modal">#}
|
{# <a role="button" data-toggle="modal" data-target="#manage_api_token_modal">#}
|
||||||
{# <i class="glyphicon glyphicon-console"></i> Manage API Token</a>#}
|
{# <i class="glyphicon glyphicon-console"></i> Manage API Token</a>#}
|
||||||
{# </li>#}
|
{# </li>#}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a role="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||||
class="button"
|
<i class="glyphicon glyphicon-trash"></i> Delete Account</a>
|
||||||
onclick="document.getElementById('generic_delete_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<i class="glyphicon glyphicon-trash"></i> Delete Account</a
|
|
||||||
>
|
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -1,17 +1,12 @@
|
|||||||
{% extends "framework.html" %}
|
{% extends "framework.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div id="searchContent">
|
|
||||||
|
<div id=searchContent>
|
||||||
<form id="admin-form" action="{{ SERVER_BASE }}/admin" method="post">
|
<form id="admin-form" action="{{ SERVER_BASE }}/admin" method="post">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="textarea">Query</label>
|
<label for="textarea">Query</label>
|
||||||
<textarea
|
<textarea id="textarea" class="form-control" rows="10" placeholder="Paste query here" required>
|
||||||
id="textarea"
|
|
||||||
class="form-control"
|
|
||||||
rows="10"
|
|
||||||
placeholder="Paste query here"
|
|
||||||
required
|
|
||||||
>
|
|
||||||
PREFIX pps: <http://localhost:8080/vocabulary#>
|
PREFIX pps: <http://localhost:8080/vocabulary#>
|
||||||
SELECT ?name (count(?objId) as ?xcnt)
|
SELECT ?name (count(?objId) as ?xcnt)
|
||||||
WHERE {
|
WHERE {
|
||||||
@ -20,29 +15,32 @@ WHERE {
|
|||||||
?packageId pps:reviewStatus 'reviewed' .
|
?packageId pps:reviewStatus 'reviewed' .
|
||||||
?packageId pps:pathway ?objId .
|
?packageId pps:pathway ?objId .
|
||||||
} GROUP BY ?name
|
} GROUP BY ?name
|
||||||
</textarea
|
</textarea>
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
<button id="submit" type="button" class="btn btn-primary">Submit</button>
|
<button id="submit" type="button" class="btn btn-primary">Submit</button>
|
||||||
</form>
|
</form>
|
||||||
<p></p>
|
<p></p>
|
||||||
</div>
|
</div>
|
||||||
<div id="results"></div>
|
<div id="results">
|
||||||
<div id="loading"></div>
|
</div>
|
||||||
<script>
|
<div id="loading"></div>
|
||||||
$(function () {
|
</div>
|
||||||
$("#submit").on("click", function () {
|
<script>
|
||||||
|
$(function() {
|
||||||
|
$('#submit').on('click', function() {
|
||||||
|
|
||||||
makeLoadingGif("#loading", "{% static '/images/wait.gif' %}");
|
makeLoadingGif("#loading", "{% static '/images/wait.gif' %}");
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
query: $("#textarea").val(),
|
"query": $("#textarea").val()
|
||||||
};
|
}
|
||||||
|
|
||||||
$.post("{{ SERVER_BASE }}/expire", data, function (result) {
|
$.post("{{ SERVER_BASE }}/expire", data, function(result) {
|
||||||
$("#loading").empty();
|
$("#loading").empty();
|
||||||
queryResultToTable("results", result);
|
queryResultToTable("results", result);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
});
|
</script>
|
||||||
</script>
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@ -1,168 +0,0 @@
|
|||||||
{% extends "framework_modern.html" %}
|
|
||||||
{% load static %}
|
|
||||||
{% block content %}
|
|
||||||
<div class="mx-auto w-full p-8">
|
|
||||||
<h1 class="h1 mb-4 text-3xl font-bold">Batch Predict Pathways</h1>
|
|
||||||
<form id="smiles-form" method="POST" action="{% url "jobs" %}">
|
|
||||||
{% csrf_token %}
|
|
||||||
<input type="hidden" name="substrates" id="substrates" />
|
|
||||||
<input type="hidden" name="job-name" value="batch-predict" />
|
|
||||||
|
|
||||||
<fieldset class="flex flex-col gap-4 md:flex-3/4">
|
|
||||||
<table class="table table-zebra w-full">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>SMILES</th>
|
|
||||||
<th>Name</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="smiles-table-body">
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full smiles-input"
|
|
||||||
placeholder="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
|
|
||||||
{% if meta.debug %}
|
|
||||||
value="CN1C=NC2=C1C(=O)N(C(=O)N2C)C"
|
|
||||||
{% endif %}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full name-input"
|
|
||||||
placeholder="Caffeine"
|
|
||||||
{% if meta.debug %}
|
|
||||||
value="Caffeine"
|
|
||||||
{% endif %}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full smiles-input"
|
|
||||||
placeholder="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
|
|
||||||
{% if meta.debug %}
|
|
||||||
value="CC(C)CC1=CC=C(C=C1)C(C)C(=O)O"
|
|
||||||
{% endif %}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="input input-bordered w-full name-input"
|
|
||||||
placeholder="Ibuprofen"
|
|
||||||
{% if meta.debug %}
|
|
||||||
value="Ibuprofen"
|
|
||||||
{% endif %}
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
<label class="select mb-2 w-full">
|
|
||||||
<span class="label">Predictor</span>
|
|
||||||
<select id="prediction-setting" name="prediction-setting">
|
|
||||||
<option disabled>Select a Setting</option>
|
|
||||||
{% for s in meta.available_settings %}
|
|
||||||
<option
|
|
||||||
value="{{ s.url }}"
|
|
||||||
{% if s.id == meta.user.default_setting.id %}selected{% endif %}
|
|
||||||
>
|
|
||||||
{{ s.name }}{% if s.id == meta.user.default_setting.id %}
|
|
||||||
(User default)
|
|
||||||
{% endif %}
|
|
||||||
</option>
|
|
||||||
{% endfor %}
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="floating-label" for="num-tps">
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
name="num-tps"
|
|
||||||
value="50"
|
|
||||||
step="1"
|
|
||||||
min="1"
|
|
||||||
max="100"
|
|
||||||
id="num-tps"
|
|
||||||
class="input input-md w-full"
|
|
||||||
/>
|
|
||||||
<span>Max Transformation Products</span>
|
|
||||||
</label>
|
|
||||||
<div class="flex justify-end gap-2">
|
|
||||||
<button type="button" id="add-row-btn" class="btn btn-outline">
|
|
||||||
Add row
|
|
||||||
</button>
|
|
||||||
<button type="submit" class="btn btn-primary">Submit</button>
|
|
||||||
</div>
|
|
||||||
</fieldset>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
const tableBody = document.getElementById("smiles-table-body");
|
|
||||||
const addRowBtn = document.getElementById("add-row-btn");
|
|
||||||
const form = document.getElementById("smiles-form");
|
|
||||||
const hiddenField = document.getElementById("substrates");
|
|
||||||
|
|
||||||
addRowBtn.addEventListener("click", () => {
|
|
||||||
const row = document.createElement("tr");
|
|
||||||
|
|
||||||
const tdSmiles = document.createElement("td");
|
|
||||||
const tdName = document.createElement("td");
|
|
||||||
|
|
||||||
const smilesInput = document.createElement("input");
|
|
||||||
smilesInput.type = "text";
|
|
||||||
smilesInput.className = "input input-bordered w-full smiles-input";
|
|
||||||
smilesInput.placeholder = "SMILES";
|
|
||||||
|
|
||||||
const nameInput = document.createElement("input");
|
|
||||||
nameInput.type = "text";
|
|
||||||
nameInput.className = "input input-bordered w-full name-input";
|
|
||||||
nameInput.placeholder = "Name";
|
|
||||||
|
|
||||||
tdSmiles.appendChild(smilesInput);
|
|
||||||
tdName.appendChild(nameInput);
|
|
||||||
|
|
||||||
row.appendChild(tdSmiles);
|
|
||||||
row.appendChild(tdName);
|
|
||||||
|
|
||||||
tableBody.appendChild(row);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Before submit, gather table data into the hidden field
|
|
||||||
form.addEventListener("submit", (e) => {
|
|
||||||
const smilesInputs = Array.from(
|
|
||||||
document.querySelectorAll(".smiles-input"),
|
|
||||||
);
|
|
||||||
const nameInputs = Array.from(document.querySelectorAll(".name-input"));
|
|
||||||
|
|
||||||
const lines = [];
|
|
||||||
|
|
||||||
for (let i = 0; i < smilesInputs.length; i++) {
|
|
||||||
const smiles = smilesInputs[i].value.trim();
|
|
||||||
const name = nameInputs[i]?.value.trim() ?? "";
|
|
||||||
|
|
||||||
// Skip emtpy rows
|
|
||||||
if (!smiles && !name) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
lines.push(`${smiles},${name}`);
|
|
||||||
}
|
|
||||||
// Value looks like:
|
|
||||||
// "CN1C=NC2=C1C(=O)N(C(=O)N2C)C,Caffeine\nCC(C)CC1=CC=C(C=C1)C(C)C(=O)O,Ibuprofen"
|
|
||||||
hiddenField.value = lines.join("\n");
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
{% endblock content %}
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
{# Partial for paginated list content - expects to be inside a remotePaginatedList Alpine.js context #}
|
|
||||||
{# Variables: empty_text (string), show_review_badge (bool), always_show_badge (bool) #}
|
|
||||||
|
|
||||||
{# Loading state #}
|
|
||||||
<div
|
|
||||||
x-show="isLoading"
|
|
||||||
class="mx-auto flex h-32 w-32 items-center justify-center"
|
|
||||||
>
|
|
||||||
{% include "components/loading-spinner.html" %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Error state #}
|
|
||||||
<div
|
|
||||||
x-show="!isLoading && error"
|
|
||||||
class="alert alert-error/50 text-sm"
|
|
||||||
x-text="error"
|
|
||||||
></div>
|
|
||||||
|
|
||||||
{# Content #}
|
|
||||||
<template x-if="!isLoading && !error">
|
|
||||||
<div>
|
|
||||||
{# Empty state #}
|
|
||||||
<div
|
|
||||||
x-show="totalItems === 0"
|
|
||||||
class="text-base-content/70 py-8 text-center"
|
|
||||||
>
|
|
||||||
<p>No {{ empty_text|default:"items" }} found.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Items list #}
|
|
||||||
<ul class="menu bg-base-100 rounded-box w-full" x-show="totalItems > 0">
|
|
||||||
<template x-for="obj in paginatedItems" :key="obj.url">
|
|
||||||
<li>
|
|
||||||
<a :href="obj.url" class="hover:bg-base-200">
|
|
||||||
<span x-text="obj.name"></span>
|
|
||||||
{% if show_review_badge %}
|
|
||||||
<span
|
|
||||||
class="tooltip tooltip-left ml-auto"
|
|
||||||
data-tip="Reviewed"
|
|
||||||
{% if not always_show_badge %}
|
|
||||||
x-show="obj.review_status === 'reviewed'"
|
|
||||||
{% endif %}
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-check-icon lucide-check"
|
|
||||||
>
|
|
||||||
<path d="M20 6 9 17l-5-5" />
|
|
||||||
</svg>
|
|
||||||
</span>
|
|
||||||
{% endif %}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
{# Pagination controls #}
|
|
||||||
<div
|
|
||||||
x-show="totalPages > 1"
|
|
||||||
class="mt-4 flex items-center justify-between px-2"
|
|
||||||
>
|
|
||||||
<span class="text-base-content/70 text-sm">
|
|
||||||
Showing <span x-text="showingStart"></span>-<span
|
|
||||||
x-text="showingEnd"
|
|
||||||
></span>
|
|
||||||
of <span x-text="totalItems"></span>
|
|
||||||
</span>
|
|
||||||
<div class="join">
|
|
||||||
<button
|
|
||||||
class="join-item btn btn-sm"
|
|
||||||
:disabled="currentPage === 1"
|
|
||||||
@click="prevPage()"
|
|
||||||
>
|
|
||||||
«
|
|
||||||
</button>
|
|
||||||
<template x-for="item in pageNumbers" :key="item.key">
|
|
||||||
<button
|
|
||||||
class="join-item btn btn-sm"
|
|
||||||
:class="{ 'btn-active': item.page === currentPage }"
|
|
||||||
:disabled="item.isEllipsis"
|
|
||||||
@click="!item.isEllipsis && goToPage(item.page)"
|
|
||||||
x-text="item.page"
|
|
||||||
></button>
|
|
||||||
</template>
|
|
||||||
<button
|
|
||||||
class="join-item btn btn-sm"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
@click="nextPage()"
|
|
||||||
>
|
|
||||||
»
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
{% extends "collections/paginated_base.html" %}
|
|
||||||
|
|
||||||
{% block page_title %}Compounds{% endblock %}
|
|
||||||
|
|
||||||
{% block action_button %}
|
|
||||||
{% if meta.can_edit %}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
onclick="document.getElementById('new_compound_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
New Compound
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock action_button %}
|
|
||||||
|
|
||||||
{% block action_modals %}
|
|
||||||
{% include "modals/collections/new_compound_modal.html" %}
|
|
||||||
{% endblock action_modals %}
|
|
||||||
|
|
||||||
{% block description %}
|
|
||||||
<p>
|
|
||||||
A compound stores the structure of a molecule and can include
|
|
||||||
meta-information.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://wiki.envipath.org/index.php/compounds"
|
|
||||||
class="link link-primary"
|
|
||||||
>
|
|
||||||
Learn more >>
|
|
||||||
</a>
|
|
||||||
{% endblock description %}
|
|
||||||
@ -1,59 +1,48 @@
|
|||||||
{% extends "framework_modern.html" %}
|
{% extends "framework.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% load envipytags %}
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="space-y-2 p-4">
|
|
||||||
<!-- Header Section -->
|
<div class="panel-group" id="reviewListAccordion">
|
||||||
<div class="card bg-base-100">
|
<div class="panel panel-default">
|
||||||
<div class="card-body">
|
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
|
||||||
<h2 class="card-title text-2xl">User Prediction Jobs</h2>
|
Jobs
|
||||||
<p class="mt-2">Job Logs Desc</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<p>
|
||||||
|
Job Logs Desc
|
||||||
|
</p>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Jobs -->
|
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
|
||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<h4 class="panel-title">
|
||||||
<input type="checkbox" checked />
|
<a id="job-accordion-link" data-toggle="collapse" data-parent="#job-accordion" href="#jobs">
|
||||||
<div class="collapse-title text-xl font-medium">Recent Jobs</div>
|
Jobs
|
||||||
<div class="collapse-content" id="job-content">
|
</a>
|
||||||
<div class="overflow-x-auto">
|
</h4>
|
||||||
<table class="table-zebra table">
|
</div>
|
||||||
<thead>
|
<div id="jobs"
|
||||||
<tr>
|
class="panel-collapse collapse in">
|
||||||
{% if meta.user.is_superuser %}
|
<div class="panel-body list-group-item" id="job-content">
|
||||||
<th>User</th>
|
<table class="table table-bordered table-hover">
|
||||||
{% endif %}
|
<tr style="background-color: rgba(0, 0, 0, 0.08);">
|
||||||
<th>ID</th>
|
<th scope="col">ID</th>
|
||||||
<th>Name</th>
|
<th scope="col">Name</th>
|
||||||
<th>Status</th>
|
<th scope="col">Status</th>
|
||||||
<th>Queued</th>
|
<th scope="col">Queued</th>
|
||||||
<th>Done</th>
|
<th scope="col">Done</th>
|
||||||
<th>Result</th>
|
<th scope="col">Result</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for job in jobs %}
|
{% for job in jobs %}
|
||||||
<tr>
|
<tr>
|
||||||
{% if meta.user.is_superuser %}
|
<td>{{ job.task_id }}</td>
|
||||||
<td>
|
|
||||||
<a href="{{ job.user.url }}">{{ job.user.username }}</a>
|
|
||||||
</td>
|
|
||||||
{% endif %}
|
|
||||||
<td>
|
|
||||||
<a href="{% url 'job detail' job.task_id %}"
|
|
||||||
>{{ job.task_id }}</a
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
<td>{{ job.job_name }}</td>
|
<td>{{ job.job_name }}</td>
|
||||||
<td>{{ job.status }}</td>
|
<td>{{ job.status }}</td>
|
||||||
<td>{{ job.created }}</td>
|
<td>{{ job.created }}</td>
|
||||||
<td>{{ job.done_at }}</td>
|
<td>{{ job.done_at }}</td>
|
||||||
{% if job.task_result and job.task_result|is_url == True %}
|
{% if job.task_result and job.task_result|is_url == True %}
|
||||||
<td>
|
<td><a href="{{ job.task_result }}">Result</a></td>
|
||||||
<a href="{{ job.task_result }}" class="link link-primary"
|
|
||||||
>Result</a
|
|
||||||
>
|
|
||||||
</td>
|
|
||||||
{% elif job.task_result %}
|
{% elif job.task_result %}
|
||||||
<td>{{ job.task_result|slice:"40" }}...</td>
|
<td>{{ job.task_result|slice:"40" }}...</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -65,31 +54,17 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if objects %}
|
|
||||||
<!-- Unreviewable objects such as User / Group / Setting -->
|
<!-- Unreviewable objects such as User / Group / Setting -->
|
||||||
<div class="card bg-base-100 shadow-xl">
|
<ul class='list-group'>
|
||||||
<div class="card-body">
|
|
||||||
<ul class="menu bg-base-200 rounded-box">
|
|
||||||
{% for obj in objects %}
|
{% for obj in objects %}
|
||||||
{% if object_type == 'user' %}
|
{% if object_type == 'user' %}
|
||||||
<li>
|
<a class="list-group-item" href="{{ obj.url }}">{{ obj.username }}</a>
|
||||||
<a href="{{ obj.url }}" class="hover:bg-base-300"
|
|
||||||
>{{ obj.username }}</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<li>
|
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name }}</a>
|
||||||
<a href="{{ obj.url }}" class="hover:bg-base-300"
|
|
||||||
>{{ obj.name }}</a
|
|
||||||
>
|
|
||||||
</li>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@ -1,32 +0,0 @@
|
|||||||
{% extends "collections/paginated_base.html" %}
|
|
||||||
|
|
||||||
{% block page_title %}Models{% endblock %}
|
|
||||||
|
|
||||||
{% block action_button %}
|
|
||||||
{% if meta.can_edit and meta.enabled_features.MODEL_BUILDING %}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
onclick="document.getElementById('new_model_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
New Model
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock action_button %}
|
|
||||||
|
|
||||||
{% block action_modals %}
|
|
||||||
{% if meta.enabled_features.MODEL_BUILDING %}
|
|
||||||
{% include "modals/collections/new_model_modal.html" %}
|
|
||||||
{% endif %}
|
|
||||||
{% endblock action_modals %}
|
|
||||||
|
|
||||||
{% block description %}
|
|
||||||
<p>A model applies machine learning to limit the combinatorial explosion.</p>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://wiki.envipath.org/index.php/relative_reasoning"
|
|
||||||
class="link link-primary"
|
|
||||||
>
|
|
||||||
Learn more >>
|
|
||||||
</a>
|
|
||||||
{% endblock description %}
|
|
||||||
@ -1,323 +1,319 @@
|
|||||||
{% extends "framework_modern.html" %}
|
{% extends "framework.html" %}
|
||||||
{% load static %}
|
{% load static %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
|
|
||||||
{# Serialize objects data for Alpine pagination #}
|
{% if object_type != 'package' %}
|
||||||
{# prettier-ignore-start #}
|
<div>
|
||||||
{# FIXME: This is a hack to get the objects data into the JavaScript code. #}
|
<div id="load-all-error" style="display: none;">
|
||||||
{% if object_type != 'scenario' %}
|
<div class="alert alert-danger" role="alert">
|
||||||
<script>
|
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
|
||||||
window.reviewedObjects = [
|
<span class="sr-only">Error:</span>
|
||||||
{% for obj in reviewed_objects %}
|
Getting objects failed!
|
||||||
{ "name": "{{ obj.name|escapejs }}", "url": "{{ obj.url }}" }{% if not forloop.last %},{% endif %}
|
</div>
|
||||||
{% endfor %}
|
|
||||||
];
|
|
||||||
window.unreviewedObjects = [
|
|
||||||
{% for obj in unreviewed_objects %}
|
|
||||||
{ "name": "{{ obj.name|escapejs }}", "url": "{{ obj.url }}" }{% if not forloop.last %},{% endif %}
|
|
||||||
{% endfor %}
|
|
||||||
];
|
|
||||||
</script>
|
|
||||||
{% endif %}
|
|
||||||
{# prettier-ignore-end #}
|
|
||||||
|
|
||||||
<div class="px-8 py-4">
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
id="object-search"
|
|
||||||
class="input input-bordered hidden w-full max-w-xs"
|
|
||||||
placeholder="Search by name"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<input type="text" id="object-search" class="form-control" placeholder="Search by name"
|
||||||
|
style="display: none;">
|
||||||
|
<p></p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block action_modals %}
|
{% block action_modals %}
|
||||||
{% if object_type == 'node' %}
|
{% if object_type == 'package' %}
|
||||||
|
{% include "modals/collections/new_package_modal.html" %}
|
||||||
|
{% include "modals/collections/import_package_modal.html" %}
|
||||||
|
{% include "modals/collections/import_legacy_package_modal.html" %}
|
||||||
|
{% elif object_type == 'compound' %}
|
||||||
|
{% include "modals/collections/new_compound_modal.html" %}
|
||||||
|
{% elif object_type == 'rule' %}
|
||||||
|
{% include "modals/collections/new_rule_modal.html" %}
|
||||||
|
{% elif object_type == 'reaction' %}
|
||||||
|
{% include "modals/collections/new_reaction_modal.html" %}
|
||||||
|
{% elif object_type == 'pathway' %}
|
||||||
|
{# {% include "modals/collections/new_pathway_modal.html" %} #}
|
||||||
|
{% elif object_type == 'node' %}
|
||||||
{% include "modals/collections/new_node_modal.html" %}
|
{% include "modals/collections/new_node_modal.html" %}
|
||||||
{% elif object_type == 'edge' %}
|
{% elif object_type == 'edge' %}
|
||||||
{% include "modals/collections/new_edge_modal.html" %}
|
{% include "modals/collections/new_edge_modal.html" %}
|
||||||
|
{% elif object_type == 'scenario' %}
|
||||||
|
{% include "modals/collections/new_scenario_modal.html" %}
|
||||||
|
{% elif object_type == 'model' %}
|
||||||
|
{% include "modals/collections/new_model_modal.html" %}
|
||||||
|
{% elif object_type == 'setting' %}
|
||||||
|
{#{% include "modals/collections/new_setting_modal.html" %}#}
|
||||||
|
{% elif object_type == 'user' %}
|
||||||
|
<div></div>
|
||||||
|
{% elif object_type == 'group' %}
|
||||||
|
{% include "modals/collections/new_group_modal.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock action_modals %}
|
{% endblock action_modals %}
|
||||||
|
|
||||||
<div class="px-8 py-4">
|
<div class="panel-group" id="reviewListAccordion">
|
||||||
<!-- Header Section -->
|
<div class="panel panel-default">
|
||||||
<div class="card bg-base-100">
|
<div class="panel-heading" id="headingPanel" style="font-size:2rem;height: 46px">
|
||||||
<div class="card-body px-0 py-4">
|
{% if object_type == 'package' %}
|
||||||
<div class="flex items-center justify-between">
|
Packages
|
||||||
<h2 class="card-title text-2xl">
|
{% elif object_type == 'compound' %}
|
||||||
{% if object_type == 'node' %}
|
Compounds
|
||||||
|
{% elif object_type == 'structure' %}
|
||||||
|
Compound structures
|
||||||
|
{% elif object_type == 'rule' %}
|
||||||
|
Rules
|
||||||
|
{% elif object_type == 'reaction' %}
|
||||||
|
Reactions
|
||||||
|
{% elif object_type == 'pathway' %}
|
||||||
|
Pathways
|
||||||
|
{% elif object_type == 'node' %}
|
||||||
Nodes
|
Nodes
|
||||||
{% elif object_type == 'edge' %}
|
{% elif object_type == 'edge' %}
|
||||||
Edges
|
Edges
|
||||||
|
{% elif object_type == 'scenario' %}
|
||||||
|
Scenarios
|
||||||
|
{% elif object_type == 'model' %}
|
||||||
|
Model
|
||||||
|
{% elif object_type == 'setting' %}
|
||||||
|
Settings
|
||||||
|
{% elif object_type == 'user' %}
|
||||||
|
Users
|
||||||
|
{% elif object_type == 'group' %}
|
||||||
|
Groups
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h2>
|
<div id="actionsButton"
|
||||||
<div id="actionsButton" class="dropdown dropdown-end hidden">
|
style="float: right;font-weight: normal;font-size: medium;position: relative; top: 50%; transform: translateY(-50%);z-index:100;display: none;"
|
||||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm">
|
class="dropdown"><a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
|
||||||
<svg
|
aria-haspopup="true" aria-expanded="false"><span
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
class="glyphicon glyphicon-wrench"></span> Actions <span class="caret"></span><span
|
||||||
width="16"
|
style="padding-right:1em"></span></a>
|
||||||
height="16"
|
<ul id="actionsList" class="dropdown-menu">
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-wrench"
|
|
||||||
>
|
|
||||||
<path
|
|
||||||
d="M14.7 6.3a1 1 0 0 0 0 1.4l1.6 1.6a1 1 0 0 0 1.4 0l3.77-3.77a6 6 0 0 1-7.94 7.94l-6.91 6.91a2.12 2.12 0 0 1-3-3l6.91-6.91a6 6 0 0 1 7.94-7.94l-3.76 3.76z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
Actions
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
tabindex="-1"
|
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-52 p-2"
|
|
||||||
>
|
|
||||||
{% block actions %}
|
{% block actions %}
|
||||||
{% if object_type == 'node' %}
|
{% if object_type == 'package' %}
|
||||||
|
{% include "actions/collections/package.html" %}
|
||||||
|
{% elif object_type == 'compound' %}
|
||||||
|
{% include "actions/collections/compound.html" %}
|
||||||
|
{% elif object_type == 'structure' %}
|
||||||
|
{% include "actions/collections/compound_structure.html" %}
|
||||||
|
{% elif object_type == 'rule' %}
|
||||||
|
{% include "actions/collections/rule.html" %}
|
||||||
|
{% elif object_type == 'reaction' %}
|
||||||
|
{% include "actions/collections/reaction.html" %}
|
||||||
|
{% elif object_type == 'setting' %}
|
||||||
|
{% include "actions/collections/setting.html" %}
|
||||||
|
{% elif object_type == 'scenario' %}
|
||||||
|
{% include "actions/collections/scenario.html" %}
|
||||||
|
{% elif object_type == 'model' %}
|
||||||
|
{% include "actions/collections/model.html" %}
|
||||||
|
{% elif object_type == 'pathway' %}
|
||||||
|
{% include "actions/collections/pathway.html" %}
|
||||||
|
{% elif object_type == 'node' %}
|
||||||
{% include "actions/collections/node.html" %}
|
{% include "actions/collections/node.html" %}
|
||||||
{% elif object_type == 'edge' %}
|
{% elif object_type == 'edge' %}
|
||||||
{% include "actions/collections/edge.html" %}
|
{% include "actions/collections/edge.html" %}
|
||||||
|
{% elif object_type == 'group' %}
|
||||||
|
{% include "actions/collections/group.html" %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-2">
|
<div class="panel-body">
|
||||||
{% if object_type == 'node' %}
|
<!-- Set Text above links -->
|
||||||
<p>
|
{% if object_type == 'package' %}
|
||||||
Nodes represent the (predicted) compounds in a graph.
|
<p>A package contains pathways, rules, etc. and can reflect specific experimental
|
||||||
<a
|
conditions. <a target="_blank" href="https://wiki.envipath.org/index.php/packages" role="button">Learn
|
||||||
target="_blank"
|
more >></a></p>
|
||||||
href="https://wiki.envipath.org/index.php/nodes"
|
{% elif object_type == 'compound' %}
|
||||||
class="link link-primary"
|
<p>A compound stores the structure of a molecule and can include meta-information.
|
||||||
>Learn more >></a
|
<a target="_blank" href="https://wiki.envipath.org/index.php/compounds" role="button">Learn more
|
||||||
>
|
>></a></p>
|
||||||
</p>
|
{% elif object_type == 'structure' %}
|
||||||
|
<p>The structures stored in this compound
|
||||||
|
<a target="_blank" href="https://wiki.envipath.org/index.php/compounds" role="button">Learn more
|
||||||
|
>></a></p>
|
||||||
|
{% elif object_type == 'rule' %}
|
||||||
|
<p>A rule describes a biotransformation reaction template that is defined as SMIRKS.
|
||||||
|
<a target="_blank" href="https://wiki.envipath.org/index.php/Rules" role="button">Learn more
|
||||||
|
>></a></p>
|
||||||
|
{% elif object_type == 'reaction' %}
|
||||||
|
<p>A reaction is a specific biotransformation from educt compounds to product compounds.
|
||||||
|
<a target="_blank" href="https://wiki.envipath.org/index.php/reactions" role="button">Learn more
|
||||||
|
>></a></p>
|
||||||
|
{% elif object_type == 'pathway' %}
|
||||||
|
<p>A pathway displays the (predicted) biodegradation of a compound as graph.
|
||||||
|
<a target="_blank" href="https://wiki.envipath.org/index.php/pathways" role="button">Learn more
|
||||||
|
>></a></p>
|
||||||
|
{% elif object_type == 'node' %}
|
||||||
|
<p>Nodes represent the (predicted) compounds in a graph.
|
||||||
|
<a target="_blank" href="https://wiki.envipath.org/index.php/nodes" role="button">Learn more
|
||||||
|
>></a></p>
|
||||||
{% elif object_type == 'edge' %}
|
{% elif object_type == 'edge' %}
|
||||||
<p>
|
<p>Edges represent the links between Nodes in a graph
|
||||||
Edges represent the links between nodes in a graph.
|
<a target="_blank" href="https://wiki.envipath.org/index.php/edges" role="button">Learn more
|
||||||
<a
|
>></a></p>
|
||||||
target="_blank"
|
{% elif object_type == 'scenario' %}
|
||||||
href="https://wiki.envipath.org/index.php/edges"
|
<p>A scenario contains meta-information that can be attached to other data (compounds, rules, ..).
|
||||||
class="link link-primary"
|
<a target="_blank" href="https://wiki.envipath.org/index.php/scenarios" role="button">Learn more
|
||||||
>Learn more >></a
|
>></a></p>
|
||||||
>
|
{% elif object_type == 'model' %}
|
||||||
</p>
|
<p>A model applies machine learning to limit the combinatorial explosion.
|
||||||
|
<a target="_blank" href="https://wiki.envipath.org/index.php/relative_reasoning" role="button">Learn
|
||||||
|
more
|
||||||
|
>></a></p>
|
||||||
|
{% elif object_type == 'setting' %}
|
||||||
|
<p>A setting includes configuration parameters for pathway predictions.
|
||||||
|
<a target="_blank" href="https://wiki.envipath.org/index.php/settings" role="button">Learn more
|
||||||
|
>></a></p>
|
||||||
|
{% elif object_type == 'user' %}
|
||||||
|
<p>Register now to create own packages and to submit and manage your data.
|
||||||
|
<a target="_blank" href="https://wiki.envipath.org/index.php/users" role="button">Learn more
|
||||||
|
>></a></p>
|
||||||
|
{% elif object_type == 'group' %}
|
||||||
|
<p>Users can team up in groups to share packages.
|
||||||
|
<a target="_blank" href="https://wiki.envipath.org/index.php/groups" role="button">Learn more
|
||||||
|
>></a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- If theres nothing to show extend the text above -->
|
||||||
{% if reviewed_objects and unreviewed_objects %}
|
{% if reviewed_objects and unreviewed_objects %}
|
||||||
{% if reviewed_objects|length == 0 and unreviewed_objects|length == 0 %}
|
{% if reviewed_objects|length == 0 and unreviewed_objects|length == 0 %}
|
||||||
<p class="mt-4">
|
<p>Nothing found. There are two possible reasons: <br><br>1. There is no content yet.<br>2. You have no
|
||||||
Nothing found. There are two possible reasons:<br /><br />
|
reading permissions.<br><br>Please be sure you have at least reading permissions.</p>
|
||||||
1. There is no content yet.<br />
|
|
||||||
2. You have no reading permissions.<br /><br />
|
|
||||||
Please ensure you have at least reading permissions.
|
|
||||||
</p>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Lists Container -->
|
|
||||||
<div class="w-full">
|
|
||||||
{% if reviewed_objects %}
|
{% if reviewed_objects %}
|
||||||
{% if reviewed_objects|length > 0 %}
|
{% if reviewed_objects|length > 0 %}
|
||||||
<!-- Reviewed -->
|
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
|
||||||
<div
|
<h4 class="panel-title">
|
||||||
class="collapse-arrow bg-base-200 collapse order-2 w-full"
|
<a id="ReviewedLink" data-toggle="collapse" data-parent="#reviewListAccordion"
|
||||||
x-data="paginatedList(window.reviewedObjects || [], { isReviewed: true, instanceId: 'reviewed' })"
|
href="#Reviewed">Reviewed</a>
|
||||||
>
|
</h4>
|
||||||
<input type="checkbox" checked />
|
|
||||||
<div class="collapse-title text-xl font-medium">
|
|
||||||
Reviewed
|
|
||||||
<span
|
|
||||||
class="badge badge-sm badge-neutral ml-2"
|
|
||||||
x-text="totalItems"
|
|
||||||
></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content w-full">
|
<div id="Reviewed" class="panel-collapse collapse in">
|
||||||
<ul class="menu bg-base-100 rounded-box w-full">
|
<div class="panel-body list-group-item" id="ReviewedContent">
|
||||||
<template x-for="obj in paginatedItems" :key="obj.url">
|
{% if object_type == 'package' %}
|
||||||
<li>
|
{% for obj in reviewed_objects %}
|
||||||
<a :href="obj.url" class="hover:bg-base-200">
|
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}
|
||||||
<span x-text="obj.name"></span>
|
<span class="glyphicon glyphicon-star" aria-hidden="true"
|
||||||
<span
|
style="float:right" data-toggle="tooltip"
|
||||||
class="tooltip tooltip-left ml-auto"
|
data-placement="top" title="" data-original-title="Reviewed">
|
||||||
data-tip="Reviewed"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-star"
|
|
||||||
>
|
|
||||||
<polygon
|
|
||||||
points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
{% endfor %}
|
||||||
</template>
|
{% else %}
|
||||||
</ul>
|
{% for obj in reviewed_objects|slice:":50" %}
|
||||||
<!-- Pagination Controls -->
|
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}{# <i>({{ obj.package.name }})</i> #}
|
||||||
<div
|
<span class="glyphicon glyphicon-star" aria-hidden="true"
|
||||||
x-show="totalPages > 1"
|
style="float:right" data-toggle="tooltip"
|
||||||
class="mt-4 flex items-center justify-between px-2"
|
data-placement="top" title="" data-original-title="Reviewed">
|
||||||
>
|
|
||||||
<span class="text-base-content/70 text-sm">
|
|
||||||
Showing <span x-text="showingStart"></span>-<span
|
|
||||||
x-text="showingEnd"
|
|
||||||
></span>
|
|
||||||
of <span x-text="totalItems"></span>
|
|
||||||
</span>
|
</span>
|
||||||
<div class="join">
|
</a>
|
||||||
<button
|
{% endfor %}
|
||||||
class="join-item btn btn-sm"
|
{% endif %}
|
||||||
:disabled="currentPage === 1"
|
|
||||||
@click="prevPage()"
|
|
||||||
>
|
|
||||||
«
|
|
||||||
</button>
|
|
||||||
<template x-for="item in pageNumbers" :key="item.key">
|
|
||||||
<button
|
|
||||||
class="join-item btn btn-sm"
|
|
||||||
:class="{ 'btn-active': item.page === currentPage }"
|
|
||||||
:disabled="item.isEllipsis"
|
|
||||||
@click="!item.isEllipsis && goToPage(item.page)"
|
|
||||||
x-text="item.page"
|
|
||||||
></button>
|
|
||||||
</template>
|
|
||||||
<button
|
|
||||||
class="join-item btn btn-sm"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
@click="nextPage()"
|
|
||||||
>
|
|
||||||
»
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if unreviewed_objects %}
|
{% if unreviewed_objects %}
|
||||||
<!-- Unreviewed -->
|
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"><h4
|
||||||
<div
|
class="panel-title"><a id="UnreviewedLink" data-toggle="collapse" data-parent="#unReviewListAccordion"
|
||||||
class="collapse-arrow bg-base-200 collapse order-1 w-full"
|
href="#Unreviewed">Unreviewed</a></h4></div>
|
||||||
x-data="paginatedList(window.unreviewedObjects || [], { isReviewed: false, instanceId: 'unreviewed' })"
|
<div id="Unreviewed" class="panel-collapse collapse {% if reviewed_objects|length == 0 or object_type == 'package' %}in{% endif %}">
|
||||||
>
|
<div class="panel-body list-group-item" id="UnreviewedContent">
|
||||||
<input
|
{% if object_type == 'package' %}
|
||||||
type="checkbox"
|
{% for obj in unreviewed_objects %}
|
||||||
{% if reviewed_objects|length == 0 %}checked{% endif %}
|
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a>
|
||||||
/>
|
{% endfor %}
|
||||||
<div class="collapse-title text-xl font-medium">
|
{% else %}
|
||||||
Unreviewed
|
{% for obj in unreviewed_objects|slice:":50" %}
|
||||||
<span
|
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a>
|
||||||
class="badge badge-sm badge-neutral ml-2"
|
{% endfor %}
|
||||||
x-text="totalItems"
|
|
||||||
></span>
|
|
||||||
</div>
|
|
||||||
<div class="collapse-content w-full">
|
|
||||||
<ul class="menu bg-base-100 rounded-box w-full">
|
|
||||||
<template x-for="obj in paginatedItems" :key="obj.url">
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
:href="obj.url"
|
|
||||||
class="hover:bg-base-200"
|
|
||||||
x-text="obj.name"
|
|
||||||
></a>
|
|
||||||
</li>
|
|
||||||
</template>
|
|
||||||
</ul>
|
|
||||||
<!-- Pagination Controls -->
|
|
||||||
<div
|
|
||||||
x-show="totalPages > 1"
|
|
||||||
class="mt-4 flex items-center justify-between px-2"
|
|
||||||
>
|
|
||||||
<span class="text-base-content/70 text-sm">
|
|
||||||
Showing <span x-text="showingStart"></span>-<span
|
|
||||||
x-text="showingEnd"
|
|
||||||
></span>
|
|
||||||
of <span x-text="totalItems"></span>
|
|
||||||
</span>
|
|
||||||
<div class="join">
|
|
||||||
<button
|
|
||||||
class="join-item btn btn-sm"
|
|
||||||
:disabled="currentPage === 1"
|
|
||||||
@click="prevPage()"
|
|
||||||
>
|
|
||||||
«
|
|
||||||
</button>
|
|
||||||
<template x-for="item in pageNumbers" :key="item.key">
|
|
||||||
<button
|
|
||||||
class="join-item btn btn-sm"
|
|
||||||
:class="{ 'btn-active': item.page === currentPage }"
|
|
||||||
:disabled="item.isEllipsis"
|
|
||||||
@click="!item.isEllipsis && goToPage(item.page)"
|
|
||||||
x-text="item.page"
|
|
||||||
></button>
|
|
||||||
</template>
|
|
||||||
<button
|
|
||||||
class="join-item btn btn-sm"
|
|
||||||
:disabled="currentPage === totalPages"
|
|
||||||
@click="nextPage()"
|
|
||||||
>
|
|
||||||
»
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% if objects %}
|
||||||
|
<!-- Unreviewable objects such as User / Group / Setting -->
|
||||||
|
<ul class='list-group'>
|
||||||
|
{% for obj in objects %}
|
||||||
|
{% if object_type == 'user' %}
|
||||||
|
<a class="list-group-item" href="{{ obj.url }}">{{ obj.username|safe }}</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="list-group-item" href="{{ obj.url }}">{{ obj.name|safe }}</a>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.spinner-widget {
|
||||||
|
position: fixed; /* stays in place on scroll */
|
||||||
|
bottom: 20px; /* distance from bottom */
|
||||||
|
right: 20px; /* distance from right */
|
||||||
|
z-index: 9999; /* above most elements */
|
||||||
|
width: 60px; /* adjust to gif size */
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.spinner-widget img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div id="load-all-loading" class="spinner-widget" style="display: none">
|
||||||
|
<img id="loading-gif" src="{% static '/images/wait.gif' %}" alt="Loading...">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
document.addEventListener("DOMContentLoaded", function () {
|
$(function () {
|
||||||
// Show actions button if there are actions
|
|
||||||
const actionsButton = document.getElementById("actionsButton");
|
$('#object-search').show();
|
||||||
const actionsList = actionsButton?.querySelector("ul");
|
|
||||||
if (actionsList && actionsList.children.length > 0) {
|
{% if object_type != 'package' and object_type != 'user' and object_type != 'group' %}
|
||||||
actionsButton?.classList.remove("hidden");
|
{% if reviewed_objects|length > 50 or unreviewed_objects|length > 50 %}
|
||||||
|
$('#load-all-loading').show()
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
$('#load-all-error').hide();
|
||||||
|
|
||||||
|
$.getJSON('?all=true', function (resp) {
|
||||||
|
$('#ReviewedContent').empty();
|
||||||
|
$('#UnreviewedContent').empty();
|
||||||
|
|
||||||
|
for (o in resp.objects) {
|
||||||
|
obj = resp.objects[o];
|
||||||
|
if (obj.reviewed) {
|
||||||
|
$('#ReviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + ' <span class="glyphicon glyphicon-star" aria-hidden="true" style="float:right" data-toggle="tooltip" data-placement="top" title="" data-original-title="Reviewed"></span></a>');
|
||||||
|
} else {
|
||||||
|
$('#UnreviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + '</a>');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show search input and connect to Alpine pagination
|
$('#load-all-loading').hide();
|
||||||
const objectSearch = document.getElementById("object-search");
|
$('#load-remaining').hide();
|
||||||
if (objectSearch) {
|
}).fail(function (resp) {
|
||||||
objectSearch.classList.remove("hidden");
|
$('#load-all-loading').hide();
|
||||||
objectSearch.addEventListener("input", function () {
|
$('#load-all-error').show();
|
||||||
const query = this.value;
|
|
||||||
// Dispatch search to all paginatedList components
|
|
||||||
document
|
|
||||||
.querySelectorAll('[x-data*="paginatedList"]')
|
|
||||||
.forEach((el) => {
|
|
||||||
if (el._x_dataStack && el._x_dataStack[0]) {
|
|
||||||
el._x_dataStack[0].search(query);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete form submit handler
|
}, 2500);
|
||||||
const deleteSubmit = document.getElementById("modal-form-delete-submit");
|
{% endif %}
|
||||||
const deleteForm = document.getElementById("modal-form-delete");
|
{% endif %}
|
||||||
if (deleteSubmit && deleteForm) {
|
|
||||||
deleteSubmit.addEventListener("click", function (e) {
|
$('#modal-form-delete-submit').on('click', function (e) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteForm.submit();
|
$('#modal-form-delete').submit();
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
$('#object-search').on('keyup', function () {
|
||||||
|
let query = $(this).val().toLowerCase();
|
||||||
|
$('a.list-group-item').each(function () {
|
||||||
|
let text = $(this).text().toLowerCase();
|
||||||
|
$(this).toggle(text.indexOf(query) !== -1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
|||||||
@ -1,95 +0,0 @@
|
|||||||
{% extends "collections/paginated_base.html" %}
|
|
||||||
|
|
||||||
{% block page_title %}Packages{% endblock %}
|
|
||||||
|
|
||||||
{% block action_button %}
|
|
||||||
{% if meta.can_edit %}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
id="new-package-button"
|
|
||||||
onclick="document.getElementById('new_package_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="24"
|
|
||||||
height="24"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-folder-plus-icon lucide-folder-plus"
|
|
||||||
>
|
|
||||||
<path d="M12 10v6" />
|
|
||||||
<path d="M9 13h6" />
|
|
||||||
<path
|
|
||||||
d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"
|
|
||||||
/>
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div class="dropdown dropdown-end">
|
|
||||||
<div tabindex="0" role="button" class="btn btn-sm">
|
|
||||||
Import
|
|
||||||
<svg
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
width="16"
|
|
||||||
height="16"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
class="lucide lucide-chevron-down ml-1"
|
|
||||||
>
|
|
||||||
<path d="m6 9 6 6 6-6" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<ul
|
|
||||||
tabindex="-1"
|
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-56 p-2"
|
|
||||||
>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
role="button"
|
|
||||||
onclick="document.getElementById('import_package_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
Import Package from JSON
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
role="button"
|
|
||||||
onclick="document.getElementById('import_legacy_package_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
Import Package from legacy JSON
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock action_button %}
|
|
||||||
|
|
||||||
{% block action_modals %}
|
|
||||||
{% include "modals/collections/new_package_modal.html" %}
|
|
||||||
{% include "modals/collections/import_package_modal.html" %}
|
|
||||||
{% include "modals/collections/import_legacy_package_modal.html" %}
|
|
||||||
{% endblock action_modals %}
|
|
||||||
|
|
||||||
{% block description %}
|
|
||||||
<p>
|
|
||||||
A package contains pathways, rules, etc. and can reflect specific
|
|
||||||
experimental conditions.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://wiki.envipath.org/index.php/packages"
|
|
||||||
class="link link-primary"
|
|
||||||
>
|
|
||||||
Learn more >>
|
|
||||||
</a>
|
|
||||||
{% endblock description %}
|
|
||||||
@ -1,128 +0,0 @@
|
|||||||
{% extends "framework_modern.html" %}
|
|
||||||
{% load static %}
|
|
||||||
|
|
||||||
{# List title for empty text - defaults to "items", should be overridden by child templates #}
|
|
||||||
{% block list_title %}items{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
{% block action_modals %}
|
|
||||||
{% endblock action_modals %}
|
|
||||||
|
|
||||||
<div class="px-8 py-4">
|
|
||||||
<!-- Header Section -->
|
|
||||||
<div class="card bg-base-100">
|
|
||||||
<div class="card-body px-0 py-4">
|
|
||||||
<div class="flex items-center justify-between">
|
|
||||||
<h2 class="card-title text-2xl">
|
|
||||||
{% block page_title %}{{ page_title|default:"Items" }}{% endblock %}
|
|
||||||
</h2>
|
|
||||||
{% block action_button %}
|
|
||||||
{# Can be overridden by including action buttons for entity type #}
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
<div class="mt-2">
|
|
||||||
{% block description %}
|
|
||||||
{% endblock %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if list_mode == "combined" %}
|
|
||||||
{# ===== COMBINED MODE: Single list without tabs ===== #}
|
|
||||||
<div
|
|
||||||
class="mt-6 w-full"
|
|
||||||
x-data="remotePaginatedList({
|
|
||||||
endpoint: '{{ api_endpoint }}',
|
|
||||||
instanceId: '{{ entity_type }}_combined',
|
|
||||||
perPage: {{ per_page|default:50 }}
|
|
||||||
})"
|
|
||||||
>
|
|
||||||
{% include "collections/_paginated_list_partial.html" with empty_text=list_title|default:"items" show_review_badge=True %}
|
|
||||||
</div>
|
|
||||||
{% else %}
|
|
||||||
{# ===== TABBED MODE: Reviewed/Unreviewed tabs (default) ===== #}
|
|
||||||
<div
|
|
||||||
class="mt-6 w-full"
|
|
||||||
x-data="{
|
|
||||||
activeTab: 'reviewed',
|
|
||||||
reviewedCount: null,
|
|
||||||
unreviewedCount: null,
|
|
||||||
get bothLoaded() { return this.reviewedCount !== null && this.unreviewedCount !== null },
|
|
||||||
get isEmpty() { return this.bothLoaded && this.reviewedCount === 0 && this.unreviewedCount === 0 },
|
|
||||||
updateTabSelection() {
|
|
||||||
if (this.bothLoaded && this.reviewedCount === 0 && this.unreviewedCount > 0) {
|
|
||||||
this.activeTab = 'unreviewed';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}"
|
|
||||||
>
|
|
||||||
{# No items found message - only show after both tabs have loaded #}
|
|
||||||
<div x-show="isEmpty" class="text-base-content/70 py-8 text-center">
|
|
||||||
<p>No items found.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Tabs Navigation #}
|
|
||||||
<div role="tablist" class="tabs tabs-border" x-show="!isEmpty">
|
|
||||||
<button
|
|
||||||
role="tab"
|
|
||||||
class="tab"
|
|
||||||
:class="{ 'tab-active': activeTab === 'reviewed' }"
|
|
||||||
@click="activeTab = 'reviewed'"
|
|
||||||
x-show="reviewedCount === null || reviewedCount > 0"
|
|
||||||
>
|
|
||||||
Reviewed
|
|
||||||
<span
|
|
||||||
class="badge badge-xs badge-dash badge-info mb-2 ml-2"
|
|
||||||
:class="{ 'animate-pulse': reviewedCount === null }"
|
|
||||||
x-text="reviewedCount ?? '…'"
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
role="tab"
|
|
||||||
class="tab"
|
|
||||||
:class="{ 'tab-active': activeTab === 'unreviewed' }"
|
|
||||||
@click="activeTab = 'unreviewed'"
|
|
||||||
x-show="unreviewedCount === null || unreviewedCount > 0"
|
|
||||||
>
|
|
||||||
Unreviewed
|
|
||||||
<span
|
|
||||||
class="badge badge-xs badge-dash badge-info mb-2 ml-2"
|
|
||||||
:class="{ 'animate-pulse': unreviewedCount === null }"
|
|
||||||
x-text="unreviewedCount ?? '…'"
|
|
||||||
></span>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Reviewed Tab Content #}
|
|
||||||
<div
|
|
||||||
class="mt-6"
|
|
||||||
x-show="activeTab === 'reviewed' && !isEmpty"
|
|
||||||
x-data="remotePaginatedList({
|
|
||||||
endpoint: '{{ api_endpoint }}?review_status=true',
|
|
||||||
instanceId: '{{ entity_type }}_reviewed',
|
|
||||||
isReviewed: true,
|
|
||||||
perPage: {{ per_page|default:50 }}
|
|
||||||
})"
|
|
||||||
@items-loaded="reviewedCount = totalItems; updateTabSelection()"
|
|
||||||
>
|
|
||||||
{% include "collections/_paginated_list_partial.html" with empty_text="reviewed "|add:list_title|default:"items" show_review_badge=True always_show_badge=True %}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{# Unreviewed Tab Content #}
|
|
||||||
<div
|
|
||||||
class="mt-6"
|
|
||||||
x-show="activeTab === 'unreviewed' && !isEmpty"
|
|
||||||
x-data="remotePaginatedList({
|
|
||||||
endpoint: '{{ api_endpoint }}?review_status=false',
|
|
||||||
instanceId: '{{ entity_type }}_unreviewed',
|
|
||||||
isReviewed: false,
|
|
||||||
perPage: {{ per_page|default:50 }}
|
|
||||||
})"
|
|
||||||
@items-loaded="unreviewedCount = totalItems; updateTabSelection()"
|
|
||||||
>
|
|
||||||
{% include "collections/_paginated_list_partial.html" with empty_text="unreviewed "|add:list_title|default:"items" %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
{% endblock content %}
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
{% extends "collections/paginated_base.html" %}
|
|
||||||
|
|
||||||
{% block page_title %}Pathways{% endblock %}
|
|
||||||
|
|
||||||
{% block action_button %}
|
|
||||||
{% if meta.can_edit %}
|
|
||||||
<div class="flex items-center gap-2">
|
|
||||||
<a
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
href="{% if meta.current_package %}{{ meta.current_package.url }}/predict{% else %}/predict{% endif %}"
|
|
||||||
>
|
|
||||||
New Pathway
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock action_button %}
|
|
||||||
|
|
||||||
{% block description %}
|
|
||||||
<p>
|
|
||||||
A pathway displays the (predicted) biodegradation of a compound as graph.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://wiki.envipath.org/index.php/pathways"
|
|
||||||
class="link link-primary"
|
|
||||||
>
|
|
||||||
Learn more >>
|
|
||||||
</a>
|
|
||||||
{% endblock description %}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
{% extends "collections/paginated_base.html" %}
|
|
||||||
|
|
||||||
{% block page_title %}Reactions{% endblock %}
|
|
||||||
|
|
||||||
{% block action_button %}
|
|
||||||
{% if meta.can_edit %}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
onclick="document.getElementById('new_reaction_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
New Reaction
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock action_button %}
|
|
||||||
|
|
||||||
{% block action_modals %}
|
|
||||||
{% include "modals/collections/new_reaction_modal.html" %}
|
|
||||||
{% endblock action_modals %}
|
|
||||||
|
|
||||||
{% block description %}
|
|
||||||
<p>
|
|
||||||
A reaction is a specific biotransformation from educt compounds to product
|
|
||||||
compounds.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://wiki.envipath.org/index.php/reactions"
|
|
||||||
class="link link-primary"
|
|
||||||
>
|
|
||||||
Learn more >>
|
|
||||||
</a>
|
|
||||||
{% endblock description %}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
{% extends "collections/paginated_base.html" %}
|
|
||||||
|
|
||||||
{% block page_title %}Rules{% endblock %}
|
|
||||||
|
|
||||||
{% block action_button %}
|
|
||||||
{% if meta.can_edit %}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
onclick="document.getElementById('new_rule_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
New Rule
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock action_button %}
|
|
||||||
|
|
||||||
{% block action_modals %}
|
|
||||||
{% include "modals/collections/new_rule_modal.html" %}
|
|
||||||
{% endblock action_modals %}
|
|
||||||
|
|
||||||
{% block description %}
|
|
||||||
<p>
|
|
||||||
A rule describes a biotransformation reaction template that is defined as
|
|
||||||
SMIRKS.
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://wiki.envipath.org/index.php/Rules"
|
|
||||||
class="link link-primary"
|
|
||||||
>
|
|
||||||
Learn more >>
|
|
||||||
</a>
|
|
||||||
{% endblock description %}
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
{% extends "collections/paginated_base.html" %}
|
|
||||||
|
|
||||||
{% block page_title %}Scenarios{% endblock %}
|
|
||||||
|
|
||||||
{% block action_button %}
|
|
||||||
{% if meta.can_edit %}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
onclick="document.getElementById('new_scenario_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
New Scenario
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock action_button %}
|
|
||||||
|
|
||||||
{% block action_modals %}
|
|
||||||
{% include "modals/collections/new_scenario_modal.html" %}
|
|
||||||
{% endblock action_modals %}
|
|
||||||
|
|
||||||
{% block description %}
|
|
||||||
<p>
|
|
||||||
A scenario contains meta-information that can be attached to other data
|
|
||||||
(compounds, rules, ..).
|
|
||||||
</p>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://wiki.envipath.org/index.php/scenarios"
|
|
||||||
class="link link-primary"
|
|
||||||
>
|
|
||||||
Learn more >>
|
|
||||||
</a>
|
|
||||||
{% endblock description %}
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
{% extends "collections/paginated_base.html" %}
|
|
||||||
|
|
||||||
{% block page_title %}{{ page_title|default:"Structures" }}{% endblock %}
|
|
||||||
|
|
||||||
{% block action_button %}
|
|
||||||
{% if meta.can_edit %}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="btn btn-primary btn-sm"
|
|
||||||
onclick="document.getElementById('new_compound_structure_modal').showModal(); return false;"
|
|
||||||
>
|
|
||||||
New Structure
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
|
||||||
{% endblock action_button %}
|
|
||||||
|
|
||||||
{% block action_modals %}
|
|
||||||
{# FIXME: New Compound Structure Modal #}
|
|
||||||
{% endblock action_modals %}
|
|
||||||
|
|
||||||
{% block description %}
|
|
||||||
<p>The structures stored in this compound.</p>
|
|
||||||
<a
|
|
||||||
target="_blank"
|
|
||||||
href="https://wiki.envipath.org/index.php/compounds"
|
|
||||||
class="link link-primary"
|
|
||||||
>
|
|
||||||
Learn more >>
|
|
||||||
</a>
|
|
||||||
{% endblock description %}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user