forked from enviPath/enviPy
Compare commits
9 Commits
86b456748b
...
dd0f7eaf05
| Author | SHA1 | Date | |
|---|---|---|---|
| dd0f7eaf05 | |||
| 7d828e2be0 | |||
| 8ae4f36174 | |||
| 451986082a | |||
| ca5a9a12be | |||
| 21181c80ec | |||
| 5aa39637dc | |||
| 22179f0d90 | |||
| b508511cd6 |
@ -36,7 +36,9 @@ RUN --mount=type=ssh \
|
||||
|
||||
# Now copy source and do a final sync to install the project itself
|
||||
# Ensure .dockerignore is reasonable
|
||||
COPY bayer bayer
|
||||
COPY bridge bridge
|
||||
COPY biotransformer biotransformer
|
||||
COPY envipath envipath
|
||||
COPY epapi epapi
|
||||
COPY epauth epauth
|
||||
|
||||
20
bayer/migrations/0003_package_data_pool.py
Normal file
20
bayer/migrations/0003_package_data_pool.py
Normal file
@ -0,0 +1,20 @@
|
||||
# Generated by Django 6.0.3 on 2026-04-14 19:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('bayer', '0002_initial'),
|
||||
('epdb', '0023_alter_compoundstructure_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='package',
|
||||
name='data_pool',
|
||||
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.group', verbose_name='Data pool'),
|
||||
),
|
||||
]
|
||||
@ -29,6 +29,9 @@ class Package(EnviPathModel):
|
||||
default=Classification.RESTRICTED,
|
||||
)
|
||||
|
||||
data_pool = models.ForeignKey("epdb.Group", on_delete=models.SET_NULL, blank=True, null=True,
|
||||
verbose_name="Data pool", default=None)
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# explicitly handle related Rules
|
||||
for r in self.rules.all():
|
||||
|
||||
181
bayer/templates/modals/collections/new_package_modal.html
Normal file
181
bayer/templates/modals/collections/new_package_modal.html
Normal file
@ -0,0 +1,181 @@
|
||||
{% load static %}
|
||||
|
||||
<dialog
|
||||
id="new_package_modal"
|
||||
class="modal"
|
||||
x-data="{
|
||||
isSubmitting: false,
|
||||
packageClassification: null,
|
||||
|
||||
reset() {
|
||||
this.isSubmitting = false;
|
||||
this.selectedType = '';
|
||||
this.buildAppDomain = false;
|
||||
this.requiresRulePackages = false;
|
||||
this.requiresDataPackages = false;
|
||||
this.additional_parameters = null;
|
||||
},
|
||||
|
||||
setFormData(data) {
|
||||
this.formData = data;
|
||||
},
|
||||
|
||||
get isSecret() {
|
||||
return this.packageClassification === '20';
|
||||
},
|
||||
|
||||
submit(formId) {
|
||||
const form = document.getElementById(formId);
|
||||
|
||||
// Remove previously injected inputs
|
||||
form.querySelectorAll('.dynamic-param').forEach(el => el.remove());
|
||||
|
||||
// Add values from dynamic form into the html form
|
||||
if (this.formData) {
|
||||
Object.entries(this.formData).forEach(([key, value]) => {
|
||||
const input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = key;
|
||||
input.value = value;
|
||||
input.classList.add('dynamic-param');
|
||||
|
||||
form.appendChild(input);
|
||||
});
|
||||
}
|
||||
|
||||
if (form && form.checkValidity()) {
|
||||
this.isSubmitting = true;
|
||||
form.submit();
|
||||
} else if (form) {
|
||||
form.reportValidity();
|
||||
}
|
||||
}
|
||||
|
||||
}"
|
||||
@close="reset()"
|
||||
>
|
||||
<div class="modal-box max-w-3xl">
|
||||
<!-- Header -->
|
||||
<h3 class="text-lg font-bold">New Package</h3>
|
||||
|
||||
<!-- Close button (X) -->
|
||||
<form method="dialog">
|
||||
<button
|
||||
class="btn btn-sm btn-circle btn-ghost absolute top-2 right-2"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<!-- Body -->
|
||||
<div class="py-4">
|
||||
<form
|
||||
id="new_package_form"
|
||||
accept-charset="UTF-8"
|
||||
action=""
|
||||
method="post"
|
||||
>
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Name -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="package-name">
|
||||
<span class="label-text">Name</span>
|
||||
</label>
|
||||
<input
|
||||
id="package-name"
|
||||
class="input input-bordered w-full"
|
||||
name="package-name"
|
||||
placeholder="Name"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Description -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="package-description">
|
||||
<span class="label-text">Description</span>
|
||||
</label>
|
||||
<input
|
||||
id="package-description"
|
||||
type="text"
|
||||
class="input input-bordered w-full"
|
||||
placeholder="Description..."
|
||||
name="package-description"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- Classification Level -->
|
||||
<div class="form-control mb-3">
|
||||
<label class="label" for="package-classification">
|
||||
<span class="label-text">Package Classification</span>
|
||||
</label>
|
||||
<select
|
||||
id="package-classification"
|
||||
name="package-classification"
|
||||
class="select select-bordered w-full"
|
||||
x-model="packageClassification"
|
||||
required
|
||||
>
|
||||
<option value="null" disabled selected>Select Classification</option>
|
||||
<option value="0">Internal</option>
|
||||
<option value="10">Restricted</option>
|
||||
<option value="20">Secret</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<!-- Secret Groups -->
|
||||
<div class="form-control mb-3" x-show="isSecret" x-cloak>
|
||||
<label class="label" for="package-data-pool">
|
||||
<span class="label-text">Data Pool for SECRET Package</span>
|
||||
</label>
|
||||
<p>Only users with this role can be granted access to this package</p>
|
||||
<select
|
||||
id="package-data-pool"
|
||||
name="package-data-pool"
|
||||
class="select select-bordered w-full"
|
||||
>
|
||||
<option value="" disabled selected>Select Data Pool</option>
|
||||
{% for obj in meta.available_groups %}
|
||||
{% if obj.secret %}
|
||||
<option value="{{ obj.url }}">{{ obj.name|safe }}</option>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Footer -->
|
||||
<div class="modal-action">
|
||||
<button
|
||||
type="button"
|
||||
class="btn"
|
||||
onclick="this.closest('dialog').close()"
|
||||
:disabled="isSubmitting"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary"
|
||||
@click="submit('new_package_form')"
|
||||
:disabled="isSubmitting || !selectedType || loadingSchemas"
|
||||
>
|
||||
<span x-show="!isSubmitting">Submit</span>
|
||||
<span
|
||||
x-show="isSubmitting"
|
||||
class="loading loading-spinner loading-sm"
|
||||
></span>
|
||||
<span x-show="isSubmitting">Creating...</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Backdrop -->
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button :disabled="isSubmitting">close</button>
|
||||
</form>
|
||||
</dialog>
|
||||
@ -1,15 +1,15 @@
|
||||
services:
|
||||
db:
|
||||
image: postgres:18
|
||||
container_name: envipath-postgres
|
||||
container_name: eppostgres
|
||||
environment:
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_DB: envipath
|
||||
POSTGRES_USER: ${POSTGRES_USER}
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
- ep_bayer_postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||
interval: 5s
|
||||
@ -18,9 +18,37 @@ services:
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: envipath-redis
|
||||
container_name: epredis
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- ep_bayer_redis_data:/data
|
||||
|
||||
biotransformer3:
|
||||
image: envipath/biotransformer3:1.0
|
||||
container_name: epbiotransformer3
|
||||
|
||||
# web:
|
||||
# image: envipath/envipy-bayer:1.0
|
||||
# container_name: epdjango
|
||||
# ports:
|
||||
# - "127.0.0.1:8000:8000"
|
||||
# env_file:
|
||||
# - .env
|
||||
# command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||
# volumes:
|
||||
# - ep_bayer_data:/opt/enviPy/
|
||||
|
||||
celery_worker:
|
||||
image: envipath/envipy-bayer:1.0
|
||||
container_name: epcelery
|
||||
env_file:
|
||||
- .env.dev
|
||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||
volumes:
|
||||
- ep_bayer_data:/opt/enviPy/
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
ep_bayer_postgres_data:
|
||||
ep_bayer_redis_data:
|
||||
ep_bayer_data:
|
||||
|
||||
@ -7,7 +7,7 @@ services:
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: ${POSTGRES_DB}
|
||||
volumes:
|
||||
- ep_postgres_data:/var/lib/postgresql
|
||||
- ep_bayer_postgres_data:/var/lib/postgresql
|
||||
healthcheck:
|
||||
test: [ "CMD-SHELL", "pg_isready -U postgres" ]
|
||||
interval: 5s
|
||||
@ -18,14 +18,14 @@ services:
|
||||
image: redis:7-alpine
|
||||
container_name: epredis
|
||||
volumes:
|
||||
- ep_redis_data:/data
|
||||
- ep_bayer_redis_data:/data
|
||||
|
||||
biotransformer3:
|
||||
image: envipath/biotransformer3:1.0
|
||||
container_name: epbiotransformer3
|
||||
|
||||
web:
|
||||
image: envipath/envipy:1.0
|
||||
image: envipath/envipy-bayer:1.0
|
||||
container_name: epdjango
|
||||
ports:
|
||||
- "127.0.0.1:8000:8000"
|
||||
@ -33,18 +33,18 @@ services:
|
||||
- .env
|
||||
command: gunicorn envipath.wsgi:application --bind 0.0.0.0:8000 --workers 3
|
||||
volumes:
|
||||
- ep_data:/opt/enviPy/
|
||||
- ep_bayer_data:/opt/enviPy/
|
||||
|
||||
celery_worker:
|
||||
image: envipath/envipy:1.0
|
||||
image: envipath/envipy-bayer:1.0
|
||||
container_name: epcelery
|
||||
env_file:
|
||||
- .env
|
||||
command: celery -A envipath worker --concurrency=6 -Q model,predict,background --pool threads
|
||||
volumes:
|
||||
- ep_data:/opt/enviPy/
|
||||
- ep_bayer_data:/opt/enviPy/
|
||||
|
||||
volumes:
|
||||
ep_postgres_data:
|
||||
ep_redis_data:
|
||||
ep_data:
|
||||
ep_bayer_postgres_data:
|
||||
ep_bayer_redis_data:
|
||||
ep_bayer_data:
|
||||
|
||||
@ -9,7 +9,7 @@ https://docs.djangoproject.com/en/4.2/topics/settings/
|
||||
For the full list of settings and their values, see
|
||||
https://docs.djangoproject.com/en/4.2/ref/settings/
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
@ -20,7 +20,7 @@ from sklearn.tree import DecisionTreeClassifier
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env")
|
||||
ENV_PATH = os.environ.get("ENV_PATH", BASE_DIR / ".env.dev")
|
||||
print(f"Loading env from {ENV_PATH}")
|
||||
load_dotenv(ENV_PATH, override=False)
|
||||
|
||||
@ -442,3 +442,14 @@ BIOTRANSFORMER_ENABLED = os.environ.get("BIOTRANSFORMER_ENABLED", "False") == "T
|
||||
FLAGS["BIOTRANSFORMER"] = BIOTRANSFORMER_ENABLED
|
||||
if BIOTRANSFORMER_ENABLED:
|
||||
BIOTRANSFORMER_URL = os.environ.get("BIOTRANSFORMER_URL", None)
|
||||
|
||||
# PES
|
||||
PES_API_MAPPING = os.environ.get("PES_API_MAPPING", None)
|
||||
if PES_API_MAPPING:
|
||||
import json
|
||||
PES_API_MAPPING = json.loads(PES_API_MAPPING)
|
||||
else:
|
||||
PES_API_MAPPING = {}
|
||||
|
||||
# AD Group Mapping
|
||||
|
||||
|
||||
23
epapi/v1/endpoints/groups.py
Normal file
23
epapi/v1/endpoints/groups.py
Normal file
@ -0,0 +1,23 @@
|
||||
from django.conf import settings as s
|
||||
from ninja import Router
|
||||
from ninja_extra.pagination import paginate
|
||||
|
||||
from epdb.logic import GroupManager
|
||||
|
||||
from ..pagination import EnhancedPageNumberPagination
|
||||
from ..schemas import GroupOutSchema
|
||||
|
||||
router = Router()
|
||||
|
||||
|
||||
@router.get("/groups/", response=EnhancedPageNumberPagination.Output[GroupOutSchema])
|
||||
@paginate(
|
||||
EnhancedPageNumberPagination,
|
||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||
)
|
||||
def list_all_groups(request):
|
||||
"""
|
||||
List all groups the user has access to.
|
||||
"""
|
||||
user = request.user
|
||||
return GroupManager.get_groups(user)
|
||||
@ -15,9 +15,9 @@ router = Router()
|
||||
EnhancedPageNumberPagination,
|
||||
page_size=s.API_PAGINATION_DEFAULT_PAGE_SIZE,
|
||||
)
|
||||
def list_all_pathways(request):
|
||||
def list_all_settings(request):
|
||||
"""
|
||||
List all pathways from reviewed packages.
|
||||
List all settings the user has access to.
|
||||
"""
|
||||
user = request.user
|
||||
return SettingManager.get_all_settings(user)
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
from ninja import Router
|
||||
from ninja.security import SessionAuth
|
||||
|
||||
from envipath import settings as s
|
||||
from .auth import BearerTokenAuth
|
||||
from .endpoints import (
|
||||
packages,
|
||||
@ -13,8 +14,8 @@ from .endpoints import (
|
||||
structure,
|
||||
additional_information,
|
||||
settings,
|
||||
groups,
|
||||
)
|
||||
from envipath import settings as s
|
||||
|
||||
# Main router with authentication
|
||||
router = Router(
|
||||
@ -35,6 +36,7 @@ router.add_router("", models.router)
|
||||
router.add_router("", structure.router)
|
||||
router.add_router("", additional_information.router)
|
||||
router.add_router("", settings.router)
|
||||
router.add_router("", groups.router)
|
||||
|
||||
if s.IUCLID_EXPORT_ENABLED:
|
||||
from epiuclid.api import router as iuclid_router
|
||||
|
||||
@ -126,3 +126,10 @@ class SettingOutSchema(Schema):
|
||||
url: str = ""
|
||||
name: str
|
||||
description: str
|
||||
|
||||
|
||||
class GroupOutSchema(Schema):
|
||||
uuid: UUID
|
||||
url: str = ""
|
||||
name: str
|
||||
description: str
|
||||
|
||||
@ -0,0 +1,50 @@
|
||||
# Generated by Django 6.0.3 on 2026-04-14 19:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('epdb', '0022_alter_classifierpluginmodel_data_packages_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='compoundstructure',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='epmodel',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='parallelrule',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='rule',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='sequentialrule',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='simpleambitrule',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='simplerdkitrule',
|
||||
options={},
|
||||
),
|
||||
migrations.AlterModelOptions(
|
||||
name='simplerule',
|
||||
options={},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='group',
|
||||
name='secret',
|
||||
field=models.BooleanField(default=False, verbose_name='Secret Group'),
|
||||
),
|
||||
]
|
||||
@ -203,6 +203,7 @@ class Group(TimeStampedModel):
|
||||
name = models.TextField(blank=False, null=False, verbose_name="Group name")
|
||||
owner = models.ForeignKey("User", verbose_name="Group Owner", on_delete=models.CASCADE)
|
||||
public = models.BooleanField(verbose_name="Public Group", default=False)
|
||||
secret = models.BooleanField(verbose_name="Secret Group", default=False)
|
||||
description = models.TextField(
|
||||
blank=False, null=False, verbose_name="Descriptions", default="no description"
|
||||
)
|
||||
|
||||
@ -587,10 +587,38 @@ def packages(request):
|
||||
"package-description", s.DEFAULT_VALUES["description"]
|
||||
)
|
||||
|
||||
# EDIT START
|
||||
data_pool = None
|
||||
package_classification = request.POST.get("package-classification")
|
||||
classification = Package.Classification(int(package_classification))
|
||||
# For SECRET we'll need a data pool which will be an additional perm check later
|
||||
if classification == Package.Classification.SECRET:
|
||||
package_data_pool = request.POST.get("package-data-pool")
|
||||
|
||||
if package_data_pool is None:
|
||||
return error(request, "Invalid data pool.", "Data Pool is required!")
|
||||
|
||||
data_pool = GroupManager.get_group_by_url(current_user, package_data_pool)
|
||||
|
||||
if data_pool is None:
|
||||
return error(request, "Invalid data pool.", "Data Pool does not exist or no access!")
|
||||
|
||||
if not data_pool.secret:
|
||||
return error(request, "Invalid data pool.", "Data Pool is not a secret group!")
|
||||
|
||||
created_package = PackageManager.create_package(
|
||||
current_user, package_name, package_description
|
||||
)
|
||||
|
||||
created_package.classification_level = classification
|
||||
|
||||
# Set previously determined data pool
|
||||
if classification == Package.Classification.SECRET:
|
||||
created_package.data_pool = data_pool
|
||||
|
||||
created_package.save()
|
||||
# EDIT END
|
||||
|
||||
return redirect(created_package.url)
|
||||
|
||||
elif request.method == "OPTIONS":
|
||||
@ -2846,9 +2874,15 @@ def groups(request):
|
||||
{"Group": s.SERVER_URL + "/group"},
|
||||
]
|
||||
|
||||
context["objects"] = Group.objects.all()
|
||||
# Context for paginated template
|
||||
context["entity_type"] = "group"
|
||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/groups/"
|
||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||
context["list_title"] = "groups"
|
||||
context["list_mode"] = "combined"
|
||||
|
||||
return render(request, "collections/groups_paginated.html", context)
|
||||
|
||||
return render(request, "collections/objects_list.html", context)
|
||||
elif request.method == "POST":
|
||||
group_name = request.POST.get("group-name")
|
||||
group_description = request.POST.get("group-description", s.DEFAULT_VALUES["description"])
|
||||
|
||||
@ -1,8 +0,0 @@
|
||||
<li>
|
||||
<a
|
||||
role="button"
|
||||
onclick="document.getElementById('new_group_modal').showModal(); return false;"
|
||||
>
|
||||
<span class="glyphicon glyphicon-plus"></span> New Group</a
|
||||
>
|
||||
</li>
|
||||
|
||||
21
templates/collections/groups_paginated.html
Normal file
21
templates/collections/groups_paginated.html
Normal file
@ -0,0 +1,21 @@
|
||||
{% extends "collections/paginated_base.html" %}
|
||||
|
||||
{% block page_title %}Groups{% endblock %}
|
||||
|
||||
{% block action_button %}
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-primary btn-sm"
|
||||
onclick="document.getElementById('new_group_modal').showModal(); return false;"
|
||||
>
|
||||
New Group
|
||||
</button>
|
||||
{% endblock action_button %}
|
||||
|
||||
{% block action_modals %}
|
||||
{% include "modals/collections/new_group_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
{% block description %}
|
||||
<p>Users can team up in groups to share packages.</p>
|
||||
{% endblock description %}
|
||||
Reference in New Issue
Block a user