forked from enviPath/enviPy
Compare commits
18
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
493990566a | ||
|
|
7a6d8d7aec | ||
|
|
2e2abf0287 | ||
|
|
86c9ac887e | ||
|
|
5bbe316c24 | ||
|
|
d63c40e041 | ||
|
|
8dc0e5e7a3 | ||
|
|
ff37a17edb | ||
|
|
c60eac879e | ||
|
|
b269b7dfd8 | ||
|
|
b12c2dda69 | ||
|
|
0b325a30b7 | ||
|
|
d8741c5375 | ||
|
|
887de03a19 | ||
|
|
b54c8eaab6 | ||
|
|
f885745127 | ||
|
|
48d2bce52a | ||
|
|
67aa3731cb |
@@ -85,6 +85,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
|
|||||||
libxext6 \
|
libxext6 \
|
||||||
libfontconfig1 \
|
libfontconfig1 \
|
||||||
nano \
|
nano \
|
||||||
|
openjdk-21-jre-headless \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
RUN useradd -ms /bin/bash django
|
RUN useradd -ms /bin/bash django
|
||||||
|
|||||||
+6
-1
@@ -3,7 +3,7 @@ from django.contrib import admin
|
|||||||
# Register your models here.
|
# Register your models here.
|
||||||
from .models import (
|
from .models import (
|
||||||
PESCompound,
|
PESCompound,
|
||||||
PESStructure
|
PESStructure, NewsEntry
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -15,5 +15,10 @@ class PESStructureAdmin(admin.ModelAdmin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NewsEntryAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(PESCompound, PESCompoundAdmin)
|
admin.site.register(PESCompound, PESCompoundAdmin)
|
||||||
admin.site.register(PESStructure, PESStructureAdmin)
|
admin.site.register(PESStructure, PESStructureAdmin)
|
||||||
|
admin.site.register(NewsEntry, NewsEntryAdmin)
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
"""
|
||||||
|
Context processors for enviPy application.
|
||||||
|
|
||||||
|
Context processors automatically make variables available to all templates.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
from django.utils import timezone
|
||||||
|
from .models import NewsEntry
|
||||||
|
|
||||||
|
|
||||||
|
def news_entries_context(request):
|
||||||
|
"""
|
||||||
|
Provides package data for the search modal which is included globally
|
||||||
|
in framework_modern.html.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: Context dictionary with reviewed and unreviewed packages
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
qs = NewsEntry.objects.filter(publication_date__lte=timezone.now()).order_by('-publication_date')[:3]
|
||||||
|
return {
|
||||||
|
"news_entries": qs,
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-09-08 08:54
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bayer', '0003_pescompound_pesstructure_package_data_pool'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='package',
|
||||||
|
name='shareable',
|
||||||
|
field=models.BooleanField(default=True, verbose_name='Shareable'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-10-16 07:50
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
import uuid
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bayer', '0004_package_shareable'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='NewsEntry',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
|
||||||
|
('url', models.TextField(null=True, unique=True, verbose_name='URL')),
|
||||||
|
('title', models.TextField(verbose_name='Title')),
|
||||||
|
('publication_date', models.DateTimeField(verbose_name='Date')),
|
||||||
|
('content', models.TextField(verbose_name='Content')),
|
||||||
|
('author', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-10-16 07:59
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('bayer', '0005_newsentry'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='newsentry',
|
||||||
|
name='url',
|
||||||
|
),
|
||||||
|
]
|
||||||
+32
-10
@@ -1,10 +1,12 @@
|
|||||||
from typing import List
|
from typing import List
|
||||||
|
from uuid import uuid4
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import nh3
|
import nh3
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.db.models import QuerySet
|
from django.db.models import QuerySet
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils import timezone
|
||||||
|
|
||||||
from epdb.models import (
|
from epdb.models import (
|
||||||
EnviPathModel,
|
EnviPathModel,
|
||||||
@@ -23,10 +25,11 @@ class Package(EnviPathModel):
|
|||||||
license = models.ForeignKey(
|
license = models.ForeignKey(
|
||||||
"epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License"
|
"epdb.License", on_delete=models.SET_NULL, blank=True, null=True, verbose_name="License"
|
||||||
)
|
)
|
||||||
|
shareable = models.BooleanField(verbose_name="Shareable", default=True)
|
||||||
|
|
||||||
class Classification(models.IntegerChoices):
|
class Classification(models.IntegerChoices):
|
||||||
INTERNAL = 0, "Internal"
|
INTERNAL = 0, "Internal"
|
||||||
RESTRICTED = 10 , "Restricted"
|
RESTRICTED = 10, "Restricted"
|
||||||
SECRET = 20, "Secret"
|
SECRET = 20, "Secret"
|
||||||
|
|
||||||
classification_level = models.IntegerField(
|
classification_level = models.IntegerField(
|
||||||
@@ -108,7 +111,7 @@ class PESCompound(Compound):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
package: "Package", pes_data: dict, name: str = None, description: str = None, *args, **kwargs
|
package: "Package", pes_data: dict, name: str = None, description: str = None, *args, **kwargs
|
||||||
) -> "Compound":
|
) -> "Compound":
|
||||||
|
|
||||||
pes_url = pes_data["pes_url"]
|
pes_url = pes_data["pes_url"]
|
||||||
@@ -160,7 +163,6 @@ class PESCompound(Compound):
|
|||||||
normalized_structure=True,
|
normalized_structure=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
cs = PESStructure.create(
|
cs = PESStructure.create(
|
||||||
c,
|
c,
|
||||||
pes_url,
|
pes_url,
|
||||||
@@ -217,13 +219,13 @@ class PESStructure(CompoundStructure):
|
|||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def add_structure(
|
def add_structure(
|
||||||
self,
|
self,
|
||||||
smiles: str,
|
smiles: str,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
default_structure: bool = False,
|
default_structure: bool = False,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> "CompoundStructure":
|
) -> "CompoundStructure":
|
||||||
raise ValueError("Not supported!")
|
raise ValueError("Not supported!")
|
||||||
|
|
||||||
@@ -235,3 +237,23 @@ class PESStructure(CompoundStructure):
|
|||||||
"image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
|
"image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
|
||||||
"image_type": "png",
|
"image_type": "png",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class NewsEntry(models.Model):
|
||||||
|
uuid = models.UUIDField(
|
||||||
|
null=False, blank=False, verbose_name="UUID of this object", unique=True, default=uuid4
|
||||||
|
)
|
||||||
|
title = models.TextField(verbose_name="Title")
|
||||||
|
publication_date = models.DateTimeField(verbose_name="Date", default=timezone.now)
|
||||||
|
content = models.TextField(verbose_name="Content")
|
||||||
|
author = models.ForeignKey("epdb.User", on_delete=models.SET_NULL, blank=True, null=True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return f"{s.SERVER_URL}/news/{self.uuid}"
|
||||||
|
|
||||||
|
def save(self, *args, **kwargs):
|
||||||
|
if not self.url:
|
||||||
|
self.url = self._url()
|
||||||
|
|
||||||
|
super().save(*args, **kwargs)
|
||||||
|
|||||||
@@ -36,8 +36,8 @@
|
|||||||
img.onerror = () => {
|
img.onerror = () => {
|
||||||
this.pesVizHtml = `
|
this.pesVizHtml = `
|
||||||
<div class='alert alert-error' role='alert'>
|
<div class='alert alert-error' role='alert'>
|
||||||
<h4 class='alert-heading'>Could not render PES!</h4>
|
<h4 class='alert-heading'>Failed to fetch this PES!</h4>
|
||||||
<p>Could not render PES - Do you have access?</p>
|
<p>Either the PES-ID is incorrect, or you're missing sufficient permissions to access this (potentially secret) item. In case you're missing permissions you can request the entitlement cs.u.enviPath_secret_data_user_group on go/idnow to gain access.</p>
|
||||||
</div>`;
|
</div>`;
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
|
||||||
|
{% extends "framework_modern.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="mx-auto max-w-4xl px-4 py-8">
|
||||||
|
{# Header / Title row #}
|
||||||
|
<div class="mb-6 flex items-center justify-between">
|
||||||
|
<h1 class="text-2xl font-bold">{{ news_entry.title }}</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Meta info card #}
|
||||||
|
<div class="card bg-base-100 shadow mb-6">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-semibold text-base-content/60">Published</span>
|
||||||
|
<p class="mt-1">{{ news_entry.publication_date|date:"N j, Y, P" }}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="text-sm font-semibold text-base-content/60">Author</span>
|
||||||
|
<p class="mt-1">
|
||||||
|
{% if news_entry.author %}
|
||||||
|
{{ news_entry.author.get_full_name|default:news_entry.author.username }}
|
||||||
|
{% else %}
|
||||||
|
<span class="text-base-content/40 italic">Unknown</span>
|
||||||
|
{% endif %}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Content card #}
|
||||||
|
<div class="card bg-base-100 shadow">
|
||||||
|
<div class="card-body prose max-w-none">
|
||||||
|
{{ news_entry.content|safe }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# Back link #}
|
||||||
|
<div class="mt-6">
|
||||||
|
<a href="javascript:history.back()" class="btn btn-ghost btn-sm">
|
||||||
|
← Back
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endblock content %}
|
||||||
@@ -16,4 +16,5 @@ urlpatterns = [
|
|||||||
v.create_pes_node,
|
v.create_pes_node,
|
||||||
name="create pes node",
|
name="create pes node",
|
||||||
),
|
),
|
||||||
|
re_path(rf"^news/(?P<news_entry_uuid>{UUID})$", v.news_detail, name="news detail"),
|
||||||
]
|
]
|
||||||
|
|||||||
+110
-42
@@ -4,12 +4,12 @@ import logging
|
|||||||
import requests
|
import requests
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed
|
from django.http import HttpResponse, HttpResponseBadRequest, HttpResponseNotAllowed
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect, render
|
||||||
|
|
||||||
from bayer.models import PESCompound
|
from bayer.models import PESCompound, NewsEntry
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Pathway, Node
|
from epdb.models import Pathway, Node, Group
|
||||||
from epdb.views import _anonymous_or_real, error
|
from epdb.views import _anonymous_or_real, get_base_context, error
|
||||||
from utilities.decorators import package_permission_required
|
from utilities.decorators import package_permission_required
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
@@ -18,6 +18,32 @@ Package = s.GET_PACKAGE_MODEL()
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def has_pes_allowance(user):
|
||||||
|
if user.principal_type == user.PrincipalType.APP:
|
||||||
|
return user.app_details.pes_allowance != user.app_details.PESAllowance.NONE
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def has_secret_pes_allowance(user):
|
||||||
|
"""
|
||||||
|
Determines if the specified user belongs to any secret group.
|
||||||
|
|
||||||
|
This function checks whether the given user is a member of any group
|
||||||
|
that is marked as secret.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user: The user for whom the check is performed.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the user belongs to at least one secret group,
|
||||||
|
False otherwise.
|
||||||
|
"""
|
||||||
|
if user.principal_type == user.PrincipalType.APP:
|
||||||
|
return user.app_details.pes_allowance == user.app_details.PESAllowance.SECRET
|
||||||
|
|
||||||
|
return Group.objects.filter(secret=True, user_member=user).exists()
|
||||||
|
|
||||||
|
|
||||||
@package_permission_required()
|
@package_permission_required()
|
||||||
def create_pes(request, package_uuid):
|
def create_pes(request, package_uuid):
|
||||||
current_user = _anonymous_or_real(request)
|
current_user = _anonymous_or_real(request)
|
||||||
@@ -38,7 +64,7 @@ def create_pes(request, package_uuid):
|
|||||||
|
|
||||||
if pes_link:
|
if pes_link:
|
||||||
try:
|
try:
|
||||||
pes_data = fetch_pes(request, pes_link)
|
pes_data = fetch_pes(request, pes_link, current_user)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return error(
|
return error(
|
||||||
request,
|
request,
|
||||||
@@ -98,12 +124,12 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
|
|
||||||
if pes_link:
|
if pes_link:
|
||||||
try:
|
try:
|
||||||
pes_data = fetch_pes(request, pes_link)
|
pes_data = fetch_pes(request, pes_link, current_user)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return error(
|
return error(
|
||||||
request,
|
request,
|
||||||
"Could not fetch PES",
|
"Failed to fetch this PES",
|
||||||
f"Could not fetch PES data for {pes_link}"
|
"Either the PES-ID is incorrect, or you're missing sufficient permissions to access this (potentially secret) item. In case you're missing permissions you can request the entitlement cs.u.enviPath_secret_data_user_group on go/idnow to gain access."
|
||||||
)
|
)
|
||||||
|
|
||||||
classification = pes_data.get("classificationLevel", "")
|
classification = pes_data.get("classificationLevel", "")
|
||||||
@@ -157,53 +183,86 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
return HttpResponseNotAllowed(["POST"])
|
return HttpResponseNotAllowed(["POST"])
|
||||||
|
|
||||||
|
|
||||||
def fetch_pes(request, pes_url) -> dict:
|
def get_application_token(prod: bool) -> str:
|
||||||
from epauth.views import get_access_token_from_request
|
scope = f"{s.PROD_PES_SCOPE if prod else s.NON_PROD_PES_SCOPE}/.default"
|
||||||
token = get_access_token_from_request(request)
|
|
||||||
|
|
||||||
if token is None:
|
url = f"https://login.microsoftonline.com/{s.MS_ENTRA_TENANT_ID}/oauth2/v2.0/token"
|
||||||
token = pes_url.split('/')[-1] == 'dummy'
|
data = {
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": s.MS_ENTRA_CLIENT_ID,
|
||||||
|
"client_secret": s.MS_ENTRA_CLIENT_SECRET,
|
||||||
|
"scope": scope,
|
||||||
|
}
|
||||||
|
|
||||||
if token:
|
try:
|
||||||
for k, v in s.PES_API_MAPPING.items():
|
response = requests.post(url, data=data)
|
||||||
if pes_url.startswith(k):
|
response.raise_for_status()
|
||||||
pes_id = pes_url.split('/')[-1]
|
return response.json()["access_token"]
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
logger.error(f"Could not fetch application token: {e}")
|
||||||
|
raise ValueError(f"Could not fetch application token!")
|
||||||
|
|
||||||
if pes_id == 'dummy':
|
|
||||||
import json
|
def fetch_pes(request, pes_url, user) -> dict:
|
||||||
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
|
|
||||||
|
if not has_pes_allowance(user):
|
||||||
|
logger.info(f"User {user.username} does not have permission to fetch PESs")
|
||||||
|
raise ValueError(f"User {user.username} does not have permission to fetch PESs")
|
||||||
|
|
||||||
|
for k, v in s.PES_API_MAPPING.items():
|
||||||
|
if pes_url.startswith(k):
|
||||||
|
|
||||||
|
prod = "cropkey-np" not in pes_url
|
||||||
|
|
||||||
|
pes_id = pes_url.split('/')[-1]
|
||||||
|
|
||||||
|
if pes_id == 'dummy':
|
||||||
|
import json
|
||||||
|
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
|
||||||
|
res_data["pes_url"] = pes_url
|
||||||
|
return res_data
|
||||||
|
else:
|
||||||
|
headers = {
|
||||||
|
"accept": "*/*",
|
||||||
|
"authorization": "Bearer " + get_application_token(prod),
|
||||||
|
}
|
||||||
|
|
||||||
|
# Restrict request if user is not part of any secret group
|
||||||
|
if not has_secret_pes_allowance(user):
|
||||||
|
headers["app-classification-level-restriction"] = "restrict-pes-secret-structure-access"
|
||||||
|
|
||||||
|
|
||||||
|
params = {"pes_reg_entity_corporate_id": pes_id}
|
||||||
|
|
||||||
|
res = requests.get(v, headers=headers, params=params, proxies=s.PROXIES or None)
|
||||||
|
|
||||||
|
try:
|
||||||
|
res.raise_for_status()
|
||||||
|
pes_data = res.json()
|
||||||
|
|
||||||
|
# Handle missing response
|
||||||
|
if "detail" in pes_data and "The following PES Reg Entities Corporate Ids could not be found" in pes_data["detail"]:
|
||||||
|
raise ValueError(f"PES with id {pes_id} not found")
|
||||||
|
|
||||||
|
# Ensure we have a entity
|
||||||
|
if len(pes_data) == 0:
|
||||||
|
raise ValueError(f"PES with id {pes_id} not found")
|
||||||
|
|
||||||
|
res_data = pes_data[0]
|
||||||
res_data["pes_url"] = pes_url
|
res_data["pes_url"] = pes_url
|
||||||
return res_data
|
return res_data
|
||||||
else:
|
|
||||||
headers = {"Authorization": f"Bearer {token['access_token']}"}
|
|
||||||
params = {"pes_reg_entity_corporate_id": pes_id}
|
|
||||||
|
|
||||||
res = requests.get(v, headers=headers, params=params, proxies=s.PROXIES or None)
|
except requests.exceptions.HTTPError as e:
|
||||||
|
raise ValueError(f"Error fetching PES with id {pes_id}: {e}")
|
||||||
|
|
||||||
try:
|
raise ValueError(f"Unknown URL {pes_url}")
|
||||||
res.raise_for_status()
|
|
||||||
pes_data = res.json()
|
|
||||||
|
|
||||||
if len(pes_data) == 0:
|
|
||||||
raise ValueError(f"PES with id {pes_id} not found")
|
|
||||||
|
|
||||||
res_data = pes_data[0]
|
|
||||||
res_data["pes_url"] = pes_url
|
|
||||||
return res_data
|
|
||||||
|
|
||||||
except requests.exceptions.HTTPError as e:
|
|
||||||
raise ValueError(f"Error fetching PES with id {pes_id}: {e}")
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Unknown URL {pes_url}")
|
|
||||||
else:
|
|
||||||
raise ValueError("Could not fetch access token from request.")
|
|
||||||
|
|
||||||
|
|
||||||
def visualize_pes(request):
|
def visualize_pes(request):
|
||||||
pes_link = request.GET.get('pesLink')
|
pes_link = request.GET.get('pesLink')
|
||||||
|
|
||||||
if pes_link:
|
if pes_link:
|
||||||
pes_data = fetch_pes(request, pes_link)
|
pes_data = fetch_pes(request, pes_link, request.user)
|
||||||
|
|
||||||
representations = pes_data.get('representations')
|
representations = pes_data.get('representations')
|
||||||
|
|
||||||
@@ -211,3 +270,12 @@ def visualize_pes(request):
|
|||||||
if rep.get('type') == 'color':
|
if rep.get('type') == 'color':
|
||||||
image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", ""))
|
image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", ""))
|
||||||
return HttpResponse(image_data, content_type="image/png")
|
return HttpResponse(image_data, content_type="image/png")
|
||||||
|
|
||||||
|
|
||||||
|
def news_detail(request, news_entry_uuid):
|
||||||
|
current_user = _anonymous_or_real(request)
|
||||||
|
context = get_base_context(request, current_user)
|
||||||
|
|
||||||
|
context["entry"] = NewsEntry.objects.get(uuid=news_entry_uuid)
|
||||||
|
|
||||||
|
return render(request, "objects/news_entry.html", context)
|
||||||
+17
-1
@@ -29,6 +29,8 @@ class SamplingAlgorithm(enum.Enum):
|
|||||||
class BB4GConfig(EnviPyModel):
|
class BB4GConfig(EnviPyModel):
|
||||||
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
|
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
|
||||||
cutoff: int = -5
|
cutoff: int = -5
|
||||||
|
model: str = "rat"
|
||||||
|
mode: str | None = None
|
||||||
|
|
||||||
class UI:
|
class UI:
|
||||||
title = "BB4G Configuration"
|
title = "BB4G Configuration"
|
||||||
@@ -44,6 +46,18 @@ class BB4GConfig(EnviPyModel):
|
|||||||
order=2,
|
order=2,
|
||||||
placeholder="If unset defaults to -5"
|
placeholder="If unset defaults to -5"
|
||||||
)
|
)
|
||||||
|
model = UIConfig(
|
||||||
|
widget=WidgetType.TEXT,
|
||||||
|
label="BB4G Model",
|
||||||
|
order=3,
|
||||||
|
placeholder="If unset defaults to rat"
|
||||||
|
)
|
||||||
|
mode = UIConfig(
|
||||||
|
widget=WidgetType.TEXT,
|
||||||
|
label="BB4G Mode",
|
||||||
|
order=4,
|
||||||
|
placeholder="If unset defaults to None"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# Once stable these will be exposed by enviPy-plugins lib
|
# Once stable these will be exposed by enviPy-plugins lib
|
||||||
@@ -127,7 +141,7 @@ class BB4G(Classifier):
|
|||||||
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
|
||||||
|
|
||||||
# Ensure Service is running
|
# Ensure Service is running
|
||||||
self.start()
|
# self.start()
|
||||||
|
|
||||||
smiles = [c.smiles for c in eP.get_compounds()]
|
smiles = [c.smiles for c in eP.get_compounds()]
|
||||||
preds = self._post(smiles)
|
preds = self._post(smiles)
|
||||||
@@ -167,6 +181,8 @@ class BB4G(Classifier):
|
|||||||
"smiles": smi,
|
"smiles": smi,
|
||||||
"sampling_alg": self.config.sampling_algorithm.value,
|
"sampling_alg": self.config.sampling_algorithm.value,
|
||||||
"cutoff": self.config.cutoff,
|
"cutoff": self.config.cutoff,
|
||||||
|
"model": self.config.model,
|
||||||
|
"mode": self.config.mode,
|
||||||
}
|
}
|
||||||
|
|
||||||
retries = 0
|
retries = 0
|
||||||
|
|||||||
@@ -114,6 +114,7 @@ TEMPLATES = [
|
|||||||
"django.contrib.auth.context_processors.auth",
|
"django.contrib.auth.context_processors.auth",
|
||||||
"django.contrib.messages.context_processors.messages",
|
"django.contrib.messages.context_processors.messages",
|
||||||
"epdb.context_processors.package_context",
|
"epdb.context_processors.package_context",
|
||||||
|
"bayer.context_processors.news_entries_context"
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -445,6 +446,8 @@ if MS_ENTRA_ENABLED:
|
|||||||
MS_ENTRA_AUTHORITY = f"https://login.microsoftonline.com/{MS_ENTRA_TENANT_ID}"
|
MS_ENTRA_AUTHORITY = f"https://login.microsoftonline.com/{MS_ENTRA_TENANT_ID}"
|
||||||
MS_ENTRA_REDIRECT_URI = os.environ["MS_REDIRECT_URI"]
|
MS_ENTRA_REDIRECT_URI = os.environ["MS_REDIRECT_URI"]
|
||||||
MS_ENTRA_SCOPES = os.environ.get("MS_SCOPES", "").split(",")
|
MS_ENTRA_SCOPES = os.environ.get("MS_SCOPES", "").split(",")
|
||||||
|
NON_PROD_PES_SCOPE = os.environ.get("NON_PROD_PES_SCOPE")
|
||||||
|
PROD_PES_SCOPE = os.environ.get("PROD_PES_SCOPE")
|
||||||
|
|
||||||
# Site ID 10 -> beta.envipath.org
|
# Site ID 10 -> beta.envipath.org
|
||||||
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")
|
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")
|
||||||
|
|||||||
+18
-3
@@ -9,7 +9,7 @@ from django.shortcuts import redirect
|
|||||||
|
|
||||||
from epdb.logic import UserManager, GroupManager
|
from epdb.logic import UserManager, GroupManager
|
||||||
from epdb.models import Group
|
from epdb.models import Group
|
||||||
from epdb.views import get_remote_address
|
from epdb.views import get_remote_address, error
|
||||||
|
|
||||||
auth_log = logging.getLogger("auth")
|
auth_log = logging.getLogger("auth")
|
||||||
|
|
||||||
@@ -72,6 +72,15 @@ def entra_callback(request):
|
|||||||
|
|
||||||
claims = result["id_token_claims"]
|
claims = result["id_token_claims"]
|
||||||
|
|
||||||
|
if claims.get("roles") is None or claims.get("roles") == [] or "envipath_registered_user" not in claims.get("roles"):
|
||||||
|
auth_log.error(f"Login attempt by {get_remote_address(request)} failed due to missing role")
|
||||||
|
return error(
|
||||||
|
request,
|
||||||
|
"Login Failed",
|
||||||
|
"Request access to role 1230/MON/APPS/Envipath/Envipath_Prod on go/idnow",
|
||||||
|
403,
|
||||||
|
)
|
||||||
|
|
||||||
user_name = claims.get("name")
|
user_name = claims.get("name")
|
||||||
# preferred_username is a fallback for 2nd CWID
|
# preferred_username is a fallback for 2nd CWID
|
||||||
user_email = claims.get("emailaddress", claims.get("email", claims.get("preferred_username")))
|
user_email = claims.get("emailaddress", claims.get("email", claims.get("preferred_username")))
|
||||||
@@ -93,7 +102,7 @@ def entra_callback(request):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
auth_log.info(f"Registering {user_name} with OID {user_oid}")
|
auth_log.info(f"Registering {user_name} with OID {user_oid}")
|
||||||
u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True)
|
u = UserManager.create_user(user_name, user_email, None, uuid=user_oid, is_active=True, add_to_group=False)
|
||||||
registered = True
|
registered = True
|
||||||
|
|
||||||
auth_log.info(f"User {user_name} {'(admin) ' if u.is_superuser else ''}with OID {user_oid} successfully logged in as {u.username} from {get_remote_address(request)}")
|
auth_log.info(f"User {user_name} {'(admin) ' if u.is_superuser else ''}with OID {user_oid} successfully logged in as {u.username} from {get_remote_address(request)}")
|
||||||
@@ -130,10 +139,16 @@ def entra_callback(request):
|
|||||||
auth_log.info(f"Login Group Sync: Adding {u.username} to Group {g.name} ({ uuid })")
|
auth_log.info(f"Login Group Sync: Adding {u.username} to Group {g.name} ({ uuid })")
|
||||||
else:
|
else:
|
||||||
g = Group.objects.get(uuid=uuid)
|
g = Group.objects.get(uuid=uuid)
|
||||||
if g.user_member.contains(u):
|
# Do not remove users from All enviPath Users
|
||||||
|
if g.user_member.contains(u) and g.name != 'All enviPath Users':
|
||||||
g.user_member.remove(u)
|
g.user_member.remove(u)
|
||||||
auth_log.info(f"Login Group Sync: Removing {u.username} from Group {g.name} ({ uuid })")
|
auth_log.info(f"Login Group Sync: Removing {u.username} from Group {g.name} ({ uuid })")
|
||||||
|
|
||||||
|
# Ensure people are part of All enviPath Users -> they have to as, envipath_registered_user is granted
|
||||||
|
all_envipath_users = Group.objects.get(name="All enviPath Users")
|
||||||
|
if not all_envipath_users.user_member.contains(u):
|
||||||
|
all_envipath_users.user_member.add(u)
|
||||||
|
|
||||||
if registered:
|
if registered:
|
||||||
# #72 make package secret if user is part of a secret group
|
# #72 make package secret if user is part of a secret group
|
||||||
for id, name in s.ENTRA_SECRET_GROUPS.items():
|
for id, name in s.ENTRA_SECRET_GROUPS.items():
|
||||||
|
|||||||
@@ -6,11 +6,13 @@ from django.contrib import messages
|
|||||||
|
|
||||||
from .models import (
|
from .models import (
|
||||||
AdditionalInformation,
|
AdditionalInformation,
|
||||||
|
AppDetails,
|
||||||
ClassifierPluginModel,
|
ClassifierPluginModel,
|
||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
Edge,
|
||||||
EnviFormer,
|
EnviFormer,
|
||||||
|
EnzymeLink,
|
||||||
ExternalDatabase,
|
ExternalDatabase,
|
||||||
ExternalIdentifier,
|
ExternalIdentifier,
|
||||||
Group,
|
Group,
|
||||||
@@ -39,6 +41,10 @@ class AdditionalInformationAdmin(admin.ModelAdmin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AppDetailsAdmin(admin.ModelAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class UserAdmin(admin.ModelAdmin):
|
class UserAdmin(admin.ModelAdmin):
|
||||||
list_display = [
|
list_display = [
|
||||||
"username",
|
"username",
|
||||||
@@ -212,6 +218,10 @@ class CompoundStructureAdmin(EPAdmin):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EnzymeLinkAdmin(EPAdmin):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class SimpleAmbitRuleAdmin(EPAdmin):
|
class SimpleAmbitRuleAdmin(EPAdmin):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -266,6 +276,7 @@ admin.site.register(License, LicenseAdmin)
|
|||||||
admin.site.register(ClassifierPluginModel, ClassifierPluginModelAdmin)
|
admin.site.register(ClassifierPluginModel, ClassifierPluginModelAdmin)
|
||||||
admin.site.register(Compound, CompoundAdmin)
|
admin.site.register(Compound, CompoundAdmin)
|
||||||
admin.site.register(CompoundStructure, CompoundStructureAdmin)
|
admin.site.register(CompoundStructure, CompoundStructureAdmin)
|
||||||
|
admin.site.register(EnzymeLink, EnzymeLinkAdmin)
|
||||||
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)
|
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)
|
||||||
admin.site.register(ParallelRule, ParallelRuleAdmin)
|
admin.site.register(ParallelRule, ParallelRuleAdmin)
|
||||||
admin.site.register(Reaction, ReactionAdmin)
|
admin.site.register(Reaction, ReactionAdmin)
|
||||||
@@ -276,3 +287,4 @@ admin.site.register(Setting, SettingAdmin)
|
|||||||
admin.site.register(Scenario, ScenarioAdmin)
|
admin.site.register(Scenario, ScenarioAdmin)
|
||||||
admin.site.register(ExternalDatabase, ExternalDatabaseAdmin)
|
admin.site.register(ExternalDatabase, ExternalDatabaseAdmin)
|
||||||
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)
|
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)
|
||||||
|
admin.site.register(AppDetails, AppDetailsAdmin)
|
||||||
|
|||||||
+92
-5
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
|
|||||||
import jwt
|
import jwt
|
||||||
import nh3
|
import nh3
|
||||||
import requests
|
import requests
|
||||||
|
from django.db import transaction
|
||||||
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.cache import cache
|
from django.core.cache import cache
|
||||||
@@ -43,7 +44,7 @@ from .models import (
|
|||||||
Scenario,
|
Scenario,
|
||||||
SimpleAmbitRule,
|
SimpleAmbitRule,
|
||||||
User,
|
User,
|
||||||
UserPackagePermission,
|
UserPackagePermission, AppDetails,
|
||||||
)
|
)
|
||||||
from .views import delete_with_log, get_remote_address
|
from .views import delete_with_log, get_remote_address
|
||||||
|
|
||||||
@@ -122,7 +123,6 @@ def validate_token(token: str) -> dict:
|
|||||||
class MSBearerTokenAuth(HttpBearer):
|
class MSBearerTokenAuth(HttpBearer):
|
||||||
|
|
||||||
def authenticate(self, request, token):
|
def authenticate(self, request, token):
|
||||||
|
|
||||||
auth_log.info(f"Authentication request by {get_remote_address(request)}")
|
auth_log.info(f"Authentication request by {get_remote_address(request)}")
|
||||||
|
|
||||||
if token is None:
|
if token is None:
|
||||||
@@ -130,6 +130,11 @@ class MSBearerTokenAuth(HttpBearer):
|
|||||||
|
|
||||||
claims = validate_token(token)
|
claims = validate_token(token)
|
||||||
|
|
||||||
|
if claims.get("roles") is None or claims.get("roles") == [] or "envipath_registered_user" not in claims.get(
|
||||||
|
"roles"):
|
||||||
|
auth_log.error(f"API authentication attempt by {get_remote_address(request)} failed due to missing role")
|
||||||
|
return None
|
||||||
|
|
||||||
if not User.objects.filter(uuid=claims['oid']).exists():
|
if not User.objects.filter(uuid=claims['oid']).exists():
|
||||||
auth_log.info(f"Authentication request by {get_remote_address(request)} failed!")
|
auth_log.info(f"Authentication request by {get_remote_address(request)} failed!")
|
||||||
return None
|
return None
|
||||||
@@ -564,7 +569,7 @@ class UpdatePackage(Schema):
|
|||||||
write: 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, 403: Error})
|
||||||
def update_package(request, package_uuid, pack: Form[UpdatePackage]):
|
def update_package(request, package_uuid, pack: Form[UpdatePackage]):
|
||||||
try:
|
try:
|
||||||
p = get_package_for_write(request.user, package_uuid)
|
p = get_package_for_write(request.user, package_uuid)
|
||||||
@@ -590,6 +595,9 @@ def update_package(request, package_uuid, pack: Form[UpdatePackage]):
|
|||||||
grantee = GroupManager.get_group_lp(pack.ppsURI)
|
grantee = GroupManager.get_group_lp(pack.ppsURI)
|
||||||
else:
|
else:
|
||||||
grantee = UserManager.get_user_lp(pack.ppsURI)
|
grantee = UserManager.get_user_lp(pack.ppsURI)
|
||||||
|
if grantee.principal_type == request.user.PrincipalType.APP:
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return 403, {"message": "You are not allowed to grant permissions to an app!"}
|
||||||
|
|
||||||
PackageManager.grant_read(request.user, p, grantee)
|
PackageManager.grant_read(request.user, p, grantee)
|
||||||
return HttpResponse(status=200)
|
return HttpResponse(status=200)
|
||||||
@@ -598,6 +606,9 @@ def update_package(request, package_uuid, pack: Form[UpdatePackage]):
|
|||||||
grantee = GroupManager.get_group_lp(pack.ppsURI)
|
grantee = GroupManager.get_group_lp(pack.ppsURI)
|
||||||
else:
|
else:
|
||||||
grantee = UserManager.get_user_lp(pack.ppsURI)
|
grantee = UserManager.get_user_lp(pack.ppsURI)
|
||||||
|
if grantee.principal_type == request.user.PrincipalType.APP:
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return 403, {"message": "You are not allowed to grant permissions to an app!"}
|
||||||
|
|
||||||
PackageManager.grant_write(request.user, p, grantee)
|
PackageManager.grant_write(request.user, p, grantee)
|
||||||
return HttpResponse(status=200)
|
return HttpResponse(status=200)
|
||||||
@@ -889,7 +900,7 @@ def create_package_compound(
|
|||||||
from bayer.models import PESCompound
|
from bayer.models import PESCompound
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pes_data = fetch_pes(request, c.pesLink)
|
pes_data = fetch_pes(request, c.pesLink, request.user)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
|
return 400, {"message": f"Could not fetch PES data for {c.pesLink}"}
|
||||||
|
|
||||||
@@ -2014,7 +2025,7 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
|||||||
from bayer.models import PESCompound
|
from bayer.models import PESCompound
|
||||||
|
|
||||||
try:
|
try:
|
||||||
pes_data = fetch_pes(request, n.pesLink)
|
pes_data = fetch_pes(request, n.pesLink, request.user)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return 400, {"message": f"Could not fetch PES data for {n.pesLink}"}
|
return 400, {"message": f"Could not fetch PES data for {n.pesLink}"}
|
||||||
|
|
||||||
@@ -2473,3 +2484,79 @@ def export(request, q: Query[PackageExportInSchema]):
|
|||||||
return 403, {
|
return 403, {
|
||||||
"message": f"Exporting Package with id {q.package_uuid} failed due to insufficient rights!"
|
"message": f"Exporting Package with id {q.package_uuid} failed due to insufficient rights!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RegisterApplicationInSchema(Schema):
|
||||||
|
oid: str
|
||||||
|
client_id: str
|
||||||
|
name: str
|
||||||
|
secret: bool = False
|
||||||
|
data_pool_uuid: str | None = None
|
||||||
|
pes: str | None
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/integration", response={200: Any, 403: Error})
|
||||||
|
@transaction.atomic
|
||||||
|
def register_app(request, app: Form[RegisterApplicationInSchema]):
|
||||||
|
try:
|
||||||
|
user = request.user
|
||||||
|
if user.is_superuser:
|
||||||
|
user_name = f"APP_{app.oid}"
|
||||||
|
user_email = f"{app.oid}@bayer.com"
|
||||||
|
u = UserManager.create_user(user_name, user_email, None, uuid=app.oid, is_active=True, add_to_group=False)
|
||||||
|
|
||||||
|
# Adjust objects
|
||||||
|
u.principal_type = User.PrincipalType.APP
|
||||||
|
|
||||||
|
if app.pes:
|
||||||
|
pes_choice = next(
|
||||||
|
choice for choice in AppDetails.PESAllowance
|
||||||
|
if choice.label.lower() == app.pes.lower()
|
||||||
|
)
|
||||||
|
|
||||||
|
if pes_choice is None:
|
||||||
|
pes_choice = AppDetails.PESAllowance.NONE
|
||||||
|
else:
|
||||||
|
pes_choice = AppDetails.PESAllowance.NONE
|
||||||
|
|
||||||
|
if app.data_pool_uuid is not None:
|
||||||
|
if not Group.objects.filter(uuid=app.data_pool_uuid, secret=True).exists():
|
||||||
|
return 400, {"message": f"Group with id {app.data_pool_uuid} does not exist!"}
|
||||||
|
|
||||||
|
data_pool = Group.objects.get(uuid=app.data_pool_uuid, secret=True)
|
||||||
|
else:
|
||||||
|
data_pool = None
|
||||||
|
|
||||||
|
app_details = AppDetails.objects.create(
|
||||||
|
owner=user,
|
||||||
|
client_id=app.client_id,
|
||||||
|
display_name=app.name,
|
||||||
|
secret_allowed=app.secret,
|
||||||
|
data_pool=data_pool,
|
||||||
|
pes_allowance=pes_choice,
|
||||||
|
)
|
||||||
|
u.app_details = app_details
|
||||||
|
|
||||||
|
u.save()
|
||||||
|
|
||||||
|
# Add the integration to the "All enviPath Users" Group
|
||||||
|
all_envipath_users = Group.objects.get(name="All enviPath Users")
|
||||||
|
if not all_envipath_users.user_member.contains(u):
|
||||||
|
all_envipath_users.user_member.add(u)
|
||||||
|
|
||||||
|
if app.secret:
|
||||||
|
if app.data_pool_uuid is None or app.data_pool_uuid.strip() == "":
|
||||||
|
return 400, {"message": "Secret is only allowed if data pool is specified!"}
|
||||||
|
|
||||||
|
default_package = u.default_package
|
||||||
|
default_package.data_pool = Group.objects.get(uuid=app.data_pool_uuid, secret=True)
|
||||||
|
default_package.classification_level = default_package.Classification.SECRET
|
||||||
|
default_package.save()
|
||||||
|
|
||||||
|
return 200, {"message": "Application registered successfully"}
|
||||||
|
else:
|
||||||
|
raise ValueError("Only superusers can register apps")
|
||||||
|
except ValueError as e:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Registering application failed due to {e}"
|
||||||
|
}
|
||||||
+31
-11
@@ -211,7 +211,7 @@ class UserManager(object):
|
|||||||
# Create package
|
# Create package
|
||||||
package_name = f"{u.username}{'’' if u.username[-1] in 'sxzß' else 's'} Package"
|
package_name = f"{u.username}{'’' if u.username[-1] in 'sxzß' else 's'} Package"
|
||||||
package_description = "This package was generated during registration."
|
package_description = "This package was generated during registration."
|
||||||
p = PackageManager.create_package(u, package_name, package_description)
|
p = PackageManager.create_package(u, package_name, package_description, shareable=False)
|
||||||
u.default_package = p
|
u.default_package = p
|
||||||
u.save()
|
u.save()
|
||||||
|
|
||||||
@@ -220,11 +220,14 @@ class UserManager(object):
|
|||||||
u.save()
|
u.save()
|
||||||
|
|
||||||
if add_to_group:
|
if add_to_group:
|
||||||
g = Group.objects.get(public=True, name="enviPath Users")
|
if Group.objects.filter(public=True, name="enviPath Users").exists():
|
||||||
g.user_member.add(u)
|
g = Group.objects.get(public=True, name="enviPath Users")
|
||||||
g.save()
|
g.user_member.add(u)
|
||||||
u.default_group = g
|
g.save()
|
||||||
u.save()
|
u.default_group = g
|
||||||
|
u.save()
|
||||||
|
else:
|
||||||
|
logger.info(f"add_to_group was set to True but no public group named 'enviPath Users' exists.")
|
||||||
|
|
||||||
return u
|
return u
|
||||||
|
|
||||||
@@ -547,7 +550,7 @@ class PackageManager(object):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create_package(current_user, name: str, description: str = None):
|
def create_package(current_user, name: str, description: str = None, *args, **kwargs):
|
||||||
p = Package()
|
p = Package()
|
||||||
|
|
||||||
# Clean for potential XSS
|
# Clean for potential XSS
|
||||||
@@ -556,6 +559,9 @@ class PackageManager(object):
|
|||||||
if description is not None and description.strip() != "":
|
if description is not None and description.strip() != "":
|
||||||
p.description = nh3.clean(description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
p.description = nh3.clean(description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
|
if "shareable" in kwargs:
|
||||||
|
p.shareable = kwargs["shareable"]
|
||||||
|
|
||||||
p.save()
|
p.save()
|
||||||
|
|
||||||
up = UserPackagePermission()
|
up = UserPackagePermission()
|
||||||
@@ -578,6 +584,9 @@ class PackageManager(object):
|
|||||||
if caller_perm != Permission.ALL[0] and not caller.is_superuser:
|
if caller_perm != Permission.ALL[0] and not caller.is_superuser:
|
||||||
raise ValueError("Only owner are allowed to modify permissions")
|
raise ValueError("Only owner are allowed to modify permissions")
|
||||||
|
|
||||||
|
if isinstance(grantee, Group) and package.classification_level == package.Classification.SECRET:
|
||||||
|
raise ValueError("Cannot grant permissions to a Group on a secret package")
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
"package": package,
|
"package": package,
|
||||||
}
|
}
|
||||||
@@ -623,6 +632,10 @@ class PackageManager(object):
|
|||||||
def grant_write(caller: User, package: Package, grantee: Union[User, Group]):
|
def grant_write(caller: User, package: Package, grantee: Union[User, Group]):
|
||||||
PackageManager.update_permissions(caller, package, grantee, Permission.WRITE[0])
|
PackageManager.update_permissions(caller, package, grantee, Permission.WRITE[0])
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def grant_owner(caller: User, package: Package, grantee: Union[User, Group]):
|
||||||
|
PackageManager.update_permissions(caller, package, grantee, Permission.ALL[0])
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def import_legacy_package(
|
def import_legacy_package(
|
||||||
@@ -665,11 +678,11 @@ class PackageManager(object):
|
|||||||
# EDIT START
|
# EDIT START
|
||||||
if data.get("classification"):
|
if data.get("classification"):
|
||||||
if data["classification"] == "INTERNAL":
|
if data["classification"] == "INTERNAL":
|
||||||
pack.classification = Package.Classification.RESTRICTED
|
pack.classification_level = Package.Classification.RESTRICTED
|
||||||
elif data["classification"] == "RESTRICTED":
|
elif data["classification"] == "RESTRICTED":
|
||||||
pack.classification = Package.Classification.RESTRICTED
|
pack.classification_level = Package.Classification.RESTRICTED
|
||||||
elif data["classification"] == "SECRET":
|
elif data["classification"] == "SECRET":
|
||||||
pack.classification = Package.Classification.SECRET
|
pack.classification_level = Package.Classification.SECRET
|
||||||
|
|
||||||
if not "datapool" in data:
|
if not "datapool" in data:
|
||||||
raise ValueError("Missing datapool in package")
|
raise ValueError("Missing datapool in package")
|
||||||
@@ -821,7 +834,14 @@ class PackageManager(object):
|
|||||||
r.name = rule["name"]
|
r.name = rule["name"]
|
||||||
r.description = rule["description"]
|
r.description = rule["description"]
|
||||||
r.aliases = rule.get("aliases", [])
|
r.aliases = rule.get("aliases", [])
|
||||||
r.smirks = rule["smirks"]
|
|
||||||
|
if rule.get("smirks") is not None:
|
||||||
|
r.smirks = rule["smirks"]
|
||||||
|
elif rule.get("reactionSmarts") is not None:
|
||||||
|
r.smirks = rule["reactionSmarts"]
|
||||||
|
else:
|
||||||
|
raise ValueError(f"No SMIRKS or reactionSmarts found for rule {rule['id']}")
|
||||||
|
|
||||||
r.reactant_filter_smarts = rule.get("reactantFilterSmarts", None)
|
r.reactant_filter_smarts = rule.get("reactantFilterSmarts", None)
|
||||||
r.product_filter_smarts = rule.get("productFilterSmarts", None)
|
r.product_filter_smarts = rule.get("productFilterSmarts", None)
|
||||||
r.save()
|
r.save()
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-10-07 08:08
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0030_auto_20260814_0741'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='principal_type',
|
||||||
|
field=models.IntegerField(choices=[(0, 'Internal'), (10, 'Restricted')], default=0),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='AppDetails',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('client_id', models.TextField(verbose_name='Client ID')),
|
||||||
|
('display_name', models.TextField(verbose_name='Display Name')),
|
||||||
|
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='app_details',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.appdetails'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-10-07 08:56
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0031_user_principal_type_appdetails_user_app_details'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='user',
|
||||||
|
name='principal_type',
|
||||||
|
field=models.IntegerField(choices=[(0, 'User'), (10, 'App')], default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-10-07 08:57
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0032_alter_user_principal_type'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='user',
|
||||||
|
name='app_details',
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-10-07 10:14
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0033_remove_user_app_details'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='user',
|
||||||
|
name='app_details',
|
||||||
|
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.appdetails'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-10-07 10:26
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0034_user_app_details'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='appdetails',
|
||||||
|
name='data_pool',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.group'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='appdetails',
|
||||||
|
name='secret_allowed',
|
||||||
|
field=models.BooleanField(default=False),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-10-07 10:27
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('epdb', '0035_appdetails_data_pool_appdetails_secret_allowed'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='appdetails',
|
||||||
|
name='pes_allowance',
|
||||||
|
field=models.IntegerField(choices=[(0, 'None'), (10, 'Non Secret'), (20, 'Secret')], default=0),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -84,6 +84,17 @@ class User(AbstractUser):
|
|||||||
USERNAME_FIELD = "email"
|
USERNAME_FIELD = "email"
|
||||||
REQUIRED_FIELDS = ["username"]
|
REQUIRED_FIELDS = ["username"]
|
||||||
|
|
||||||
|
class PrincipalType(models.IntegerChoices):
|
||||||
|
USER = 0, "User"
|
||||||
|
APP = 10 , "App"
|
||||||
|
|
||||||
|
principal_type = models.IntegerField(
|
||||||
|
choices=PrincipalType,
|
||||||
|
default=PrincipalType.USER,
|
||||||
|
)
|
||||||
|
|
||||||
|
app_details = models.OneToOneField("epdb.AppDetails", on_delete=models.CASCADE, null=True, blank=True)
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self):
|
||||||
return self.username
|
return self.username
|
||||||
|
|
||||||
@@ -103,6 +114,25 @@ class User(AbstractUser):
|
|||||||
return self.default_setting
|
return self.default_setting
|
||||||
|
|
||||||
|
|
||||||
|
class AppDetails(models.Model):
|
||||||
|
owner = models.ForeignKey(User, on_delete=models.CASCADE)
|
||||||
|
client_id = models.TextField(blank=False, null=False, verbose_name="Client ID")
|
||||||
|
display_name = models.TextField(blank=False, null=False, verbose_name="Display Name")
|
||||||
|
|
||||||
|
secret_allowed = models.BooleanField(default=False)
|
||||||
|
data_pool = models.ForeignKey("epdb.Group", null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
|
|
||||||
|
class PESAllowance(models.IntegerChoices):
|
||||||
|
NONE = 0, "None"
|
||||||
|
NON_SECRET = 10 , "Non Secret"
|
||||||
|
SECRET = 20, "Secret"
|
||||||
|
|
||||||
|
pes_allowance = models.IntegerField(
|
||||||
|
choices=PESAllowance,
|
||||||
|
default=PESAllowance.NONE,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
class APIToken(TimeStampedModel):
|
class APIToken(TimeStampedModel):
|
||||||
"""
|
"""
|
||||||
API authentication token for users.
|
API authentication token for users.
|
||||||
@@ -2456,6 +2486,20 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
else:
|
else:
|
||||||
return self.default_node_label.name
|
return self.default_node_label.name
|
||||||
|
|
||||||
|
def get_description(self, include_suffix=True):
|
||||||
|
default_desc = False
|
||||||
|
|
||||||
|
if self.description == "no description":
|
||||||
|
default_desc = True
|
||||||
|
|
||||||
|
if not default_desc:
|
||||||
|
return self.description
|
||||||
|
else:
|
||||||
|
if include_suffix:
|
||||||
|
return f"{self.default_node_label.description} (taken from underlying structure)"
|
||||||
|
else:
|
||||||
|
return self.default_node_label.description
|
||||||
|
|
||||||
def d3_json(self):
|
def d3_json(self):
|
||||||
app_domain_data = self.get_app_domain_assessment_data()
|
app_domain_data = self.get_app_domain_assessment_data()
|
||||||
|
|
||||||
@@ -2773,6 +2817,20 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
else:
|
else:
|
||||||
return self.edge_label.name
|
return self.edge_label.name
|
||||||
|
|
||||||
|
def get_description(self, include_suffix=True):
|
||||||
|
default_desc = False
|
||||||
|
|
||||||
|
if self.description == "no description":
|
||||||
|
default_desc = True
|
||||||
|
|
||||||
|
if not default_desc:
|
||||||
|
return self.description
|
||||||
|
else:
|
||||||
|
if include_suffix:
|
||||||
|
return f"{self.edge_label.description} (taken from underlying reaction)"
|
||||||
|
else:
|
||||||
|
return self.edge_label.description
|
||||||
|
|
||||||
|
|
||||||
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
||||||
package = models.ForeignKey(
|
package = models.ForeignKey(
|
||||||
|
|||||||
+26
-5
@@ -1292,14 +1292,21 @@ def package(request, package_uuid):
|
|||||||
"user_id", flat=True
|
"user_id", flat=True
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
users = users.filter(is_active=True)
|
users = users.filter(is_active=True).exclude(username="anonymous")
|
||||||
|
|
||||||
|
if not current_user.is_superuser:
|
||||||
|
users = users.exclude(principal_type=User.PrincipalType.APP)
|
||||||
|
|
||||||
group_perms = GroupPackagePermission.objects.filter(package=current_package)
|
group_perms = GroupPackagePermission.objects.filter(package=current_package)
|
||||||
groups = Group.objects.exclude(
|
|
||||||
id__in=GroupPackagePermission.objects.filter(package=current_package).values_list(
|
if current_package.classification_level == current_package.Classification.SECRET:
|
||||||
"group_id", flat=True
|
groups = Group.objects.none()
|
||||||
|
else:
|
||||||
|
groups = Group.objects.exclude(
|
||||||
|
id__in=GroupPackagePermission.objects.filter(package=current_package).values_list(
|
||||||
|
"group_id", flat=True
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
|
||||||
|
|
||||||
context["users"] = users
|
context["users"] = users
|
||||||
context["groups"] = groups
|
context["groups"] = groups
|
||||||
@@ -1324,6 +1331,12 @@ 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":
|
||||||
|
if current_package.classification_level == current_package.Classification.SECRET:
|
||||||
|
return error(
|
||||||
|
request,
|
||||||
|
"Cannot publish a secret package",
|
||||||
|
"You cannot publish a secret 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.grant_read(current_user, current_package, g)
|
||||||
return redirect(current_package.url)
|
return redirect(current_package.url)
|
||||||
@@ -1376,6 +1389,9 @@ def package(request, package_uuid):
|
|||||||
elif any([grantee_url, read, write, owner]):
|
elif any([grantee_url, read, write, owner]):
|
||||||
if "user" in grantee_url:
|
if "user" in grantee_url:
|
||||||
grantee = UserManager.get_user_lp(grantee_url)
|
grantee = UserManager.get_user_lp(grantee_url)
|
||||||
|
if grantee.principal_type == current_user.PrincipalType.APP:
|
||||||
|
if not request.user.is_superuser:
|
||||||
|
return error(request, "You are not allowed to grant permissions to an app!")
|
||||||
else:
|
else:
|
||||||
grantee = GroupManager.get_group_lp(grantee_url)
|
grantee = GroupManager.get_group_lp(grantee_url)
|
||||||
|
|
||||||
@@ -3314,6 +3330,11 @@ def jobs(request):
|
|||||||
"This Package was generated automatically for the batch prediction task.",
|
"This Package was generated automatically for the batch prediction task.",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if current_user.default_package.classification_level == current_user.default_package.Classification.SECRET:
|
||||||
|
target_package.classification_level = current_user.default_package.Classification.SECRET
|
||||||
|
target_package.data_pool = current_user.default_package.data_pool
|
||||||
|
target_package.save()
|
||||||
|
|
||||||
from .tasks import batch_predict, dispatch
|
from .tasks import batch_predict, dispatch
|
||||||
|
|
||||||
res = dispatch(
|
res = dispatch(
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<i class="glyphicon glyphicon-edit"></i> Edit Package</a
|
<i class="glyphicon glyphicon-edit"></i> Edit Package</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
{% if meta.current_package.shareable %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
<i class="glyphicon glyphicon-user"></i> Edit Permissions</a
|
<i class="glyphicon glyphicon-user"></i> Edit Permissions</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
{% if meta.current_package.get_classification_level_display != "Secret" %}
|
{% if meta.current_package.get_classification_level_display != "Secret" %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
@@ -52,7 +54,7 @@
|
|||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if not meta.can_edit %}
|
{% if not meta.can_edit and meta.current_package.shareable %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<i class="glyphicon glyphicon-edit"></i> Update</a
|
<i class="glyphicon glyphicon-edit"></i> Update</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
{% if 1 == 0 %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
@@ -15,6 +16,7 @@
|
|||||||
<i class="glyphicon glyphicon-lock"></i> Update Password</a
|
<i class="glyphicon glyphicon-lock"></i> Update Password</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
<li>
|
<li>
|
||||||
<a
|
<a
|
||||||
role="button"
|
role="button"
|
||||||
|
|||||||
+59
-18
@@ -180,12 +180,25 @@
|
|||||||
>Aspartame</a
|
>Aspartame</a
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<a
|
|
||||||
class="absolute top-0 left-[calc(100%-5.4rem)]"
|
|
||||||
href="/predict"
|
|
||||||
>Advanced</a
|
|
||||||
>
|
|
||||||
</div>
|
</div>
|
||||||
|
{% if meta.current_package %}
|
||||||
|
<div
|
||||||
|
class="mt-4 rounded-lg border border-base-300 bg-base-50 px-4 py-2.5 text-sm text-base-content/70"
|
||||||
|
>
|
||||||
|
Prediction will be stored in Package
|
||||||
|
<strong class="text-base-content"
|
||||||
|
><a class="link" href="{{ meta.current_package.url }}">{{ meta.current_package.name|safe }}</a></strong
|
||||||
|
>
|
||||||
|
{% if meta.user.default_setting %}
|
||||||
|
using setting
|
||||||
|
<strong class="text-base-content"
|
||||||
|
><a class="link" href="{{ meta.user.default_setting.url}}">{{ meta.user.default_setting.name|safe }}</a></strong
|
||||||
|
>
|
||||||
|
{% endif %}
|
||||||
|
To use a different setting click
|
||||||
|
<a class="link" href="/predict">here</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
id="ketcher-container"
|
id="ketcher-container"
|
||||||
@@ -208,9 +221,24 @@
|
|||||||
>
|
>
|
||||||
Predict!
|
Predict!
|
||||||
</button>
|
</button>
|
||||||
<div class="mt-1 flex w-full justify-end">
|
{% if meta.current_package %}
|
||||||
<a class="label justify-end" href="/predict">Advanced</a>
|
<div
|
||||||
</div>
|
class="mt-4 rounded-lg border border-base-300 bg-base-50 px-4 py-2.5 text-sm text-base-content/70"
|
||||||
|
>
|
||||||
|
Prediction will be stored in Package
|
||||||
|
<strong class="text-base-content"
|
||||||
|
><a class="link" href="{{ meta.current_package.url }}">{{ meta.current_package.name|safe }}</a></strong
|
||||||
|
>
|
||||||
|
{% if meta.user.default_setting %}
|
||||||
|
using setting
|
||||||
|
<strong class="text-base-content"
|
||||||
|
><a class="link" href="{{ meta.user.default_setting.url}}">{{ meta.user.default_setting.name|safe }}</a></strong
|
||||||
|
>
|
||||||
|
{% endif %}
|
||||||
|
To use a different setting click
|
||||||
|
<a class="link" href="/predict">here</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="hidden"
|
type="hidden"
|
||||||
@@ -232,15 +260,31 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Community News Section -->
|
<!-- Community News Section -->
|
||||||
<section class="bg-base-200 z-10 mx-8 py-16">
|
<section class="bg-base-200 z-10 mx-8 py-6">
|
||||||
<div class="mx-auto max-w-7xl px-4">
|
<div class="mx-auto max-w-7xl px-4">
|
||||||
<h2 class="h2 mb-8 text-left font-bold">Community Updates</h2>
|
<h2 class="h2 mb-8 text-left font-bold">News</h2>
|
||||||
|
|
||||||
<div id="community-news-container" class="flex justify-center gap-4">
|
<div id="services-container" class="flex justify-center gap-4">
|
||||||
<!-- News cards will be populated here -->
|
{% for ne in news_entries %}
|
||||||
<div id="loading" class="flex w-full justify-center">
|
<div class="card-body flex flex-col h-full justify-between p-5">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<div>
|
||||||
</div>
|
<div class="flex items-center gap-2 mb-2">
|
||||||
|
<div class="bg-primary/10 text-primary rounded-lg p-1.5">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
|
||||||
|
<polyline points="9 22 9 12 15 12 15 22"></polyline>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<h3 class="card-title text-base font-semibold tracking-tight">
|
||||||
|
{{ ne.title }} - {{ ne.publication_date|date }}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-base-content/60 leading-snug line-clamp-4">
|
||||||
|
{{ ne.content }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 text-right">
|
<div class="mt-6 text-right">
|
||||||
@@ -252,9 +296,6 @@
|
|||||||
Read More Announcements
|
Read More Announcements
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Discourse API integration -->
|
|
||||||
<script src="{% static 'js/discourse-api.js' %}"></script>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,7 @@
|
|||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="form-control mb-3">
|
<div class="form-control mb-3">
|
||||||
|
{% if 1 == 0 %}
|
||||||
<label class="label" for="default-package">
|
<label class="label" for="default-package">
|
||||||
<span class="label-text">Default Package</span>
|
<span class="label-text">Default Package</span>
|
||||||
</label>
|
</label>
|
||||||
@@ -50,7 +51,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
<div class="form-control mb-3">
|
<div class="form-control mb-3">
|
||||||
<label class="label" for="default-group">
|
<label class="label" for="default-group">
|
||||||
<span class="label-text">Default Group</span>
|
<span class="label-text">Default Group</span>
|
||||||
|
|||||||
@@ -57,7 +57,7 @@
|
|||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">Description</div>
|
<div class="collapse-title text-xl font-medium">Description</div>
|
||||||
<div class="collapse-content">{{ edge.description }}</div>
|
<div class="collapse-content">{{ edge.get_description }}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if edge.aliases %}
|
{% if edge.aliases %}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@
|
|||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">Description</div>
|
<div class="collapse-title text-xl font-medium">Description</div>
|
||||||
<div class="collapse-content">{{ node.description }}</div>
|
<div class="collapse-content">{{ node.get_description }}</div>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
||||||
|
|||||||
@@ -2,11 +2,23 @@
|
|||||||
{% load static %}
|
{% load static %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="mx-auto w-full p-8">
|
<div class="mx-auto w-full p-8">
|
||||||
<h1 class="h1 mb-4 text-3xl font-bold">
|
<h1 class="h1 mb-4 flex items-center gap-2 text-3xl font-bold">
|
||||||
Predict a Pathway
|
Predict a Pathway
|
||||||
|
|
||||||
<span class="text-base-content/50 text-xs"
|
<span class="inline-flex items-center gap-1 text-base-content/50 text-xs"
|
||||||
>in <strong>{{ meta.current_package.name|safe }}</strong>
|
>in <strong>{{ meta.current_package.name|safe }}</strong>{% if meta.url_contains_package %}
|
||||||
|
{% if meta.current_package.get_classification_level_display == "Restricted" %}
|
||||||
|
<img src="{% static 'images/restricted_mid.png' %}" width="100">
|
||||||
|
{% elif meta.current_package.get_classification_level_display == "Secret" %}
|
||||||
|
<img src="{% static 'images/secret_mid.png' %}" width="60">
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if meta.user.default_package.get_classification_level_display == "Restricted" %}
|
||||||
|
<img src="{% static 'images/restricted_mid.png' %}" width="100">
|
||||||
|
{% elif meta.user.default_package.get_classification_level_display == "Secret" %}
|
||||||
|
<img src="{% static 'images/secret_mid.png' %}" width="60">
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
</span>
|
</span>
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
|
|||||||
+33
-7
@@ -21,6 +21,8 @@ from epdb.models import (
|
|||||||
Compound,
|
Compound,
|
||||||
CompoundStructure,
|
CompoundStructure,
|
||||||
Edge,
|
Edge,
|
||||||
|
EnzymeLink,
|
||||||
|
Group,
|
||||||
License,
|
License,
|
||||||
Node,
|
Node,
|
||||||
ParallelRule,
|
ParallelRule,
|
||||||
@@ -150,6 +152,8 @@ class ReactionExportSchema(RefReactionExportSchema):
|
|||||||
# Rules #
|
# Rules #
|
||||||
#########
|
#########
|
||||||
class EnzymeExportSchema(RefEnzymeExportSchema):
|
class EnzymeExportSchema(RefEnzymeExportSchema):
|
||||||
|
name: str
|
||||||
|
description: str
|
||||||
ec_number: str
|
ec_number: str
|
||||||
classification_level: int
|
classification_level: int
|
||||||
linking_method: str
|
linking_method: str
|
||||||
@@ -162,12 +166,12 @@ class EnzymeRuleExportSchema(RefRuleExportSchema):
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_enzymes(obj):
|
def resolve_enzymes(obj):
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, EnzymeRuleExportSchema):
|
||||||
res = []
|
return obj.enzymes
|
||||||
for e in obj.get("enzymes", []):
|
elif isinstance(obj, dict):
|
||||||
res.append(EnzymeExportSchema.model_validate(e))
|
return obj.get("enzymes", [])
|
||||||
return res
|
else:
|
||||||
return obj.enzymelink_set.all()
|
return obj.enzymelink_set.all()
|
||||||
|
|
||||||
|
|
||||||
class RuleExportSchema(EnzymeRuleExportSchema):
|
class RuleExportSchema(EnzymeRuleExportSchema):
|
||||||
@@ -271,7 +275,7 @@ class PackageExportSchema(Schema):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_classification_level(obj):
|
def resolve_classification_level(obj):
|
||||||
if isinstance(obj, dict):
|
if isinstance(obj, dict):
|
||||||
return obj["classification_level"]
|
return obj.get("classification_level", "Internal")
|
||||||
return obj.Classification(obj.classification_level).name
|
return obj.Classification(obj.classification_level).name
|
||||||
|
|
||||||
|
|
||||||
@@ -679,6 +683,27 @@ class PackageImporter:
|
|||||||
for scen in elem.scenarios:
|
for scen in elem.scenarios:
|
||||||
elem_obj.scenarios.add(self._cache[scen.uuid])
|
elem_obj.scenarios.add(self._cache[scen.uuid])
|
||||||
|
|
||||||
|
def _import_enzyme(self, rule: Rule, enzyme: EnzymeExportSchema):
|
||||||
|
e = EnzymeLink()
|
||||||
|
e.uuid = str(uuid.uuid4()) if not self.preserve_uuids else enzyme.uuid
|
||||||
|
e.rule = rule
|
||||||
|
e.name = enzyme.name
|
||||||
|
e.description = enzyme.description
|
||||||
|
e.ec_number = enzyme.ec_number
|
||||||
|
e.classification_level = enzyme.classification_level
|
||||||
|
e.linking_method = enzyme.linking_method
|
||||||
|
e.save()
|
||||||
|
for reaction in enzyme.reaction_evidence:
|
||||||
|
e.reaction_evidence.add(self._cache[reaction.uuid])
|
||||||
|
for edge in enzyme.edge_evidence:
|
||||||
|
e.edge_evidence.add(self._cache[edge.uuid])
|
||||||
|
self._cache[enzyme.uuid] = e
|
||||||
|
|
||||||
|
def _import_and_link_enzymes(self, data: PackageExportSchema):
|
||||||
|
for rule in data.composite_rules:
|
||||||
|
for enzyme in rule.enzymes:
|
||||||
|
self._import_enzyme(self._cache[rule.uuid], enzyme)
|
||||||
|
|
||||||
def _import_package_from_json(
|
def _import_package_from_json(
|
||||||
self,
|
self,
|
||||||
) -> Package:
|
) -> Package:
|
||||||
@@ -717,6 +742,7 @@ class PackageImporter:
|
|||||||
self._import_scenarios(package, parsed.scenarios)
|
self._import_scenarios(package, parsed.scenarios)
|
||||||
self._import_additional_information(package, parsed.additional_information)
|
self._import_additional_information(package, parsed.additional_information)
|
||||||
self._link_scenarios_after_import(parsed)
|
self._link_scenarios_after_import(parsed)
|
||||||
|
self._import_and_link_enzymes(parsed)
|
||||||
|
|
||||||
return package
|
return package
|
||||||
|
|
||||||
|
|||||||
@@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
|
|||||||
[[package]]
|
[[package]]
|
||||||
name = "envipy-additional-information"
|
name = "envipy-additional-information"
|
||||||
version = "0.4.2"
|
version = "0.4.2"
|
||||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#ad825570480bbe2f1a35c04923fede756c450751" }
|
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?branch=develop#4909e35aac5a0a3969ac2a01f8d4ad8bdadae04c" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "pydantic" },
|
{ name = "pydantic" },
|
||||||
]
|
]
|
||||||
|
|||||||
Reference in New Issue
Block a user