Compare commits

...
18 Commits
Author SHA1 Message Date
Tim Lorsbach 493990566a adjusted bb4g for dev 2026-10-21 12:00:02 +02:00
Tim Lorsbach 7a6d8d7aec ... 2026-10-21 11:59:53 +02:00
Tim Lorsbach 2e2abf0287 ...
API CI / api-tests (pull_request) Failing after 32s
CI / test (pull_request) Failing after 33s
2026-10-21 11:59:11 +02:00
Tim Lorsbach 86c9ac887e Added news entries
API CI / api-tests (pull_request) Failing after 32s
CI / test (pull_request) Failing after 33s
2026-10-21 09:53:58 +02:00
Tim Lorsbach 5bbe316c24 ...
API CI / api-tests (pull_request) Failing after 0s
CI / test (pull_request) Failing after 35s
2026-10-07 15:00:19 +02:00
Tim Lorsbach d63c40e041 Machine users 2026-10-07 14:59:57 +02:00
Tim Lorsbach 8dc0e5e7a3 exclude groups
CI / test (pull_request) Failing after 28s
API CI / api-tests (pull_request) Failing after 35s
2026-09-23 20:08:35 +02:00
Tim Lorsbach ff37a17edb ...
CI / test (pull_request) Failing after 28s
API CI / api-tests (pull_request) Failing after 31s
2026-09-23 10:12:48 +02:00
Tim Lorsbach c60eac879e wip 2026-09-15 23:13:21 +02:00
Tim Lorsbach b269b7dfd8 ... 2026-09-08 21:49:54 +02:00
Tim Lorsbach b12c2dda69 wip
CI / test (pull_request) Failing after 27s
API CI / api-tests (pull_request) Failing after 31s
2026-09-08 15:11:47 +02:00
Tim Lorsbach 0b325a30b7 updated add if dep
CI / test (pull_request) Failing after 28s
API CI / api-tests (pull_request) Failing after 43s
2026-09-03 12:04:14 +02:00
Tim Lorsbach d8741c5375 wip
CI / test (pull_request) Failing after 29s
API CI / api-tests (pull_request) Failing after 42s
2026-09-02 21:34:26 +02:00
Tim Lorsbach 887de03a19 reactionSmarts 2026-09-02 21:30:53 +02:00
Tim Lorsbach b54c8eaab6 minor
CI / test (pull_request) Failing after 28s
API CI / api-tests (pull_request) Failing after 44s
2026-08-28 13:14:43 +02:00
Tim Lorsbach f885745127 Fix import
CI / test (pull_request) Failing after 27s
API CI / api-tests (pull_request) Failing after 41s
2026-08-27 11:46:08 +02:00
Tim Lorsbach 48d2bce52a minor 2026-08-20 09:28:23 +02:00
Tim Lorsbach 67aa3731cb PES Adjustments, Check is User is in enviPath Entra Group
CI / test (pull_request) Failing after 28s
API CI / api-tests (pull_request) Failing after 42s
2026-08-20 08:59:24 +02:00
34 changed files with 792 additions and 113 deletions
+1
View File
@@ -85,6 +85,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
libxext6 \
libfontconfig1 \
nano \
openjdk-21-jre-headless \
&& rm -rf /var/lib/apt/lists/*
RUN useradd -ms /bin/bash django
+6 -1
View File
@@ -3,7 +3,7 @@ from django.contrib import admin
# Register your models here.
from .models import (
PESCompound,
PESStructure
PESStructure, NewsEntry
)
@@ -15,5 +15,10 @@ class PESStructureAdmin(admin.ModelAdmin):
pass
class NewsEntryAdmin(admin.ModelAdmin):
pass
admin.site.register(PESCompound, PESCompoundAdmin)
admin.site.register(PESStructure, PESStructureAdmin)
admin.site.register(NewsEntry, NewsEntryAdmin)
+25
View File
@@ -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'),
),
]
+29
View File
@@ -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
View File
@@ -1,10 +1,12 @@
from typing import List
from uuid import uuid4
import urllib.parse
import nh3
from django.conf import settings as s
from django.db import models, transaction
from django.db.models import QuerySet
from django.urls import reverse
from django.utils import timezone
from epdb.models import (
EnviPathModel,
@@ -23,10 +25,11 @@ class Package(EnviPathModel):
license = models.ForeignKey(
"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):
INTERNAL = 0, "Internal"
RESTRICTED = 10 , "Restricted"
RESTRICTED = 10, "Restricted"
SECRET = 20, "Secret"
classification_level = models.IntegerField(
@@ -108,7 +111,7 @@ class PESCompound(Compound):
@staticmethod
@transaction.atomic
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":
pes_url = pes_data["pes_url"]
@@ -160,7 +163,6 @@ class PESCompound(Compound):
normalized_structure=True,
)
cs = PESStructure.create(
c,
pes_url,
@@ -217,13 +219,13 @@ class PESStructure(CompoundStructure):
@transaction.atomic
def add_structure(
self,
smiles: str,
name: str = None,
description: str = None,
default_structure: bool = False,
*args,
**kwargs,
self,
smiles: str,
name: str = None,
description: str = None,
default_structure: bool = False,
*args,
**kwargs,
) -> "CompoundStructure":
raise ValueError("Not supported!")
@@ -235,3 +237,23 @@ class PESStructure(CompoundStructure):
"image": f"{reverse('depict_pes')}?pesLink={urllib.parse.quote(self.pes_link)}",
"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 = () => {
this.pesVizHtml = `
<div class='alert alert-error' role='alert'>
<h4 class='alert-heading'>Could not render PES!</h4>
<p>Could not render PES - Do you have access?</p>
<h4 class='alert-heading'>Failed to fetch this PES!</h4>
<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>`;
};
},
+47
View File
@@ -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 %}
+1
View File
@@ -16,4 +16,5 @@ urlpatterns = [
v.create_pes_node,
name="create pes node",
),
re_path(rf"^news/(?P<news_entry_uuid>{UUID})$", v.news_detail, name="news detail"),
]
+110 -42
View File
@@ -4,12 +4,12 @@ import logging
import requests
from django.conf import settings as s
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.models import Pathway, Node
from epdb.views import _anonymous_or_real, error
from epdb.models import Pathway, Node, Group
from epdb.views import _anonymous_or_real, get_base_context, error
from utilities.decorators import package_permission_required
Package = s.GET_PACKAGE_MODEL()
@@ -18,6 +18,32 @@ Package = s.GET_PACKAGE_MODEL()
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()
def create_pes(request, package_uuid):
current_user = _anonymous_or_real(request)
@@ -38,7 +64,7 @@ def create_pes(request, package_uuid):
if pes_link:
try:
pes_data = fetch_pes(request, pes_link)
pes_data = fetch_pes(request, pes_link, current_user)
except ValueError as e:
return error(
request,
@@ -98,12 +124,12 @@ def create_pes_node(request, package_uuid, pathway_uuid):
if pes_link:
try:
pes_data = fetch_pes(request, pes_link)
pes_data = fetch_pes(request, pes_link, current_user)
except ValueError as e:
return error(
request,
"Could not fetch PES",
f"Could not fetch PES data for {pes_link}"
"Failed to fetch this PES",
"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", "")
@@ -157,53 +183,86 @@ def create_pes_node(request, package_uuid, pathway_uuid):
return HttpResponseNotAllowed(["POST"])
def fetch_pes(request, pes_url) -> dict:
from epauth.views import get_access_token_from_request
token = get_access_token_from_request(request)
def get_application_token(prod: bool) -> str:
scope = f"{s.PROD_PES_SCOPE if prod else s.NON_PROD_PES_SCOPE}/.default"
if token is None:
token = pes_url.split('/')[-1] == 'dummy'
url = f"https://login.microsoftonline.com/{s.MS_ENTRA_TENANT_ID}/oauth2/v2.0/token"
data = {
"grant_type": "client_credentials",
"client_id": s.MS_ENTRA_CLIENT_ID,
"client_secret": s.MS_ENTRA_CLIENT_SECRET,
"scope": scope,
}
if token:
for k, v in s.PES_API_MAPPING.items():
if pes_url.startswith(k):
pes_id = pes_url.split('/')[-1]
try:
response = requests.post(url, data=data)
response.raise_for_status()
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
res_data = json.load(open(s.BASE_DIR / "fixtures/pes.json"))
def fetch_pes(request, pes_url, user) -> dict:
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
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:
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.")
raise ValueError(f"Unknown URL {pes_url}")
def visualize_pes(request):
pes_link = request.GET.get('pesLink')
if pes_link:
pes_data = fetch_pes(request, pes_link)
pes_data = fetch_pes(request, pes_link, request.user)
representations = pes_data.get('representations')
@@ -211,3 +270,12 @@ def visualize_pes(request):
if rep.get('type') == 'color':
image_data = base64.b64decode(rep.get('base64').replace("data:image/png;base64,", ""))
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
View File
@@ -29,6 +29,8 @@ class SamplingAlgorithm(enum.Enum):
class BB4GConfig(EnviPyModel):
sampling_algorithm: SamplingAlgorithm = SamplingAlgorithm.EXACT
cutoff: int = -5
model: str = "rat"
mode: str | None = None
class UI:
title = "BB4G Configuration"
@@ -44,6 +46,18 @@ class BB4GConfig(EnviPyModel):
order=2,
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
@@ -127,7 +141,7 @@ class BB4G(Classifier):
def run(self, eP: EnviPyDTO, *args, **kwargs) -> RunResult:
# Ensure Service is running
self.start()
# self.start()
smiles = [c.smiles for c in eP.get_compounds()]
preds = self._post(smiles)
@@ -167,6 +181,8 @@ class BB4G(Classifier):
"smiles": smi,
"sampling_alg": self.config.sampling_algorithm.value,
"cutoff": self.config.cutoff,
"model": self.config.model,
"mode": self.config.mode,
}
retries = 0
+3
View File
@@ -114,6 +114,7 @@ TEMPLATES = [
"django.contrib.auth.context_processors.auth",
"django.contrib.messages.context_processors.messages",
"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_REDIRECT_URI = os.environ["MS_REDIRECT_URI"]
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
MATOMO_SITE_ID = os.environ.get("MATOMO_SITE_ID", "10")
+18 -3
View File
@@ -9,7 +9,7 @@ from django.shortcuts import redirect
from epdb.logic import UserManager, GroupManager
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")
@@ -72,6 +72,15 @@ def entra_callback(request):
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")
# preferred_username is a fallback for 2nd CWID
user_email = claims.get("emailaddress", claims.get("email", claims.get("preferred_username")))
@@ -93,7 +102,7 @@ def entra_callback(request):
else:
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
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 })")
else:
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)
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:
# #72 make package secret if user is part of a secret group
for id, name in s.ENTRA_SECRET_GROUPS.items():
+12
View File
@@ -6,11 +6,13 @@ from django.contrib import messages
from .models import (
AdditionalInformation,
AppDetails,
ClassifierPluginModel,
Compound,
CompoundStructure,
Edge,
EnviFormer,
EnzymeLink,
ExternalDatabase,
ExternalIdentifier,
Group,
@@ -39,6 +41,10 @@ class AdditionalInformationAdmin(admin.ModelAdmin):
pass
class AppDetailsAdmin(admin.ModelAdmin):
pass
class UserAdmin(admin.ModelAdmin):
list_display = [
"username",
@@ -212,6 +218,10 @@ class CompoundStructureAdmin(EPAdmin):
pass
class EnzymeLinkAdmin(EPAdmin):
pass
class SimpleAmbitRuleAdmin(EPAdmin):
pass
@@ -266,6 +276,7 @@ admin.site.register(License, LicenseAdmin)
admin.site.register(ClassifierPluginModel, ClassifierPluginModelAdmin)
admin.site.register(Compound, CompoundAdmin)
admin.site.register(CompoundStructure, CompoundStructureAdmin)
admin.site.register(EnzymeLink, EnzymeLinkAdmin)
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)
admin.site.register(ParallelRule, ParallelRuleAdmin)
admin.site.register(Reaction, ReactionAdmin)
@@ -276,3 +287,4 @@ admin.site.register(Setting, SettingAdmin)
admin.site.register(Scenario, ScenarioAdmin)
admin.site.register(ExternalDatabase, ExternalDatabaseAdmin)
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)
admin.site.register(AppDetails, AppDetailsAdmin)
+92 -5
View File
@@ -5,6 +5,7 @@ from typing import Any, Dict, List, Optional
import jwt
import nh3
import requests
from django.db import transaction
from django.conf import settings as s
from django.contrib.auth import get_user_model
from django.core.cache import cache
@@ -43,7 +44,7 @@ from .models import (
Scenario,
SimpleAmbitRule,
User,
UserPackagePermission,
UserPackagePermission, AppDetails,
)
from .views import delete_with_log, get_remote_address
@@ -122,7 +123,6 @@ def validate_token(token: str) -> dict:
class MSBearerTokenAuth(HttpBearer):
def authenticate(self, request, token):
auth_log.info(f"Authentication request by {get_remote_address(request)}")
if token is None:
@@ -130,6 +130,11 @@ class MSBearerTokenAuth(HttpBearer):
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():
auth_log.info(f"Authentication request by {get_remote_address(request)} failed!")
return None
@@ -564,7 +569,7 @@ class UpdatePackage(Schema):
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]):
try:
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)
else:
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)
return HttpResponse(status=200)
@@ -598,6 +606,9 @@ def update_package(request, package_uuid, pack: Form[UpdatePackage]):
grantee = GroupManager.get_group_lp(pack.ppsURI)
else:
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)
return HttpResponse(status=200)
@@ -889,7 +900,7 @@ def create_package_compound(
from bayer.models import PESCompound
try:
pes_data = fetch_pes(request, c.pesLink)
pes_data = fetch_pes(request, c.pesLink, request.user)
except ValueError as e:
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
try:
pes_data = fetch_pes(request, n.pesLink)
pes_data = fetch_pes(request, n.pesLink, request.user)
except ValueError as e:
return 400, {"message": f"Could not fetch PES data for {n.pesLink}"}
@@ -2473,3 +2484,79 @@ def export(request, q: Query[PackageExportInSchema]):
return 403, {
"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
View File
@@ -211,7 +211,7 @@ class UserManager(object):
# Create package
package_name = f"{u.username}{'’' if u.username[-1] in 'sxzß' else 's'} Package"
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.save()
@@ -220,11 +220,14 @@ class UserManager(object):
u.save()
if add_to_group:
g = Group.objects.get(public=True, name="enviPath Users")
g.user_member.add(u)
g.save()
u.default_group = g
u.save()
if Group.objects.filter(public=True, name="enviPath Users").exists():
g = Group.objects.get(public=True, name="enviPath Users")
g.user_member.add(u)
g.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
@@ -547,7 +550,7 @@ class PackageManager(object):
@staticmethod
@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()
# Clean for potential XSS
@@ -556,6 +559,9 @@ class PackageManager(object):
if description is not None and description.strip() != "":
p.description = nh3.clean(description.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
if "shareable" in kwargs:
p.shareable = kwargs["shareable"]
p.save()
up = UserPackagePermission()
@@ -578,6 +584,9 @@ class PackageManager(object):
if caller_perm != Permission.ALL[0] and not caller.is_superuser:
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 = {
"package": package,
}
@@ -623,6 +632,10 @@ class PackageManager(object):
def grant_write(caller: User, package: Package, grantee: Union[User, Group]):
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
@transaction.atomic
def import_legacy_package(
@@ -665,11 +678,11 @@ class PackageManager(object):
# EDIT START
if data.get("classification"):
if data["classification"] == "INTERNAL":
pack.classification = Package.Classification.RESTRICTED
pack.classification_level = Package.Classification.RESTRICTED
elif data["classification"] == "RESTRICTED":
pack.classification = Package.Classification.RESTRICTED
pack.classification_level = Package.Classification.RESTRICTED
elif data["classification"] == "SECRET":
pack.classification = Package.Classification.SECRET
pack.classification_level = Package.Classification.SECRET
if not "datapool" in data:
raise ValueError("Missing datapool in package")
@@ -821,7 +834,14 @@ class PackageManager(object):
r.name = rule["name"]
r.description = rule["description"]
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.product_filter_smarts = rule.get("productFilterSmarts", None)
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',
),
]
+19
View File
@@ -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),
),
]
+58
View File
@@ -84,6 +84,17 @@ class User(AbstractUser):
USERNAME_FIELD = "email"
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):
return self.username
@@ -103,6 +114,25 @@ class User(AbstractUser):
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):
"""
API authentication token for users.
@@ -2456,6 +2486,20 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
else:
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):
app_domain_data = self.get_app_domain_assessment_data()
@@ -2773,6 +2817,20 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
else:
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):
package = models.ForeignKey(
+26 -5
View File
@@ -1292,14 +1292,21 @@ def package(request, package_uuid):
"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)
groups = Group.objects.exclude(
id__in=GroupPackagePermission.objects.filter(package=current_package).values_list(
"group_id", flat=True
if current_package.classification_level == current_package.Classification.SECRET:
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["groups"] = groups
@@ -1324,6 +1331,12 @@ def package(request, package_uuid):
return redirect(s.SERVER_URL + "/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):
PackageManager.grant_read(current_user, current_package, g)
return redirect(current_package.url)
@@ -1376,6 +1389,9 @@ def package(request, package_uuid):
elif any([grantee_url, read, write, owner]):
if "user" in 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:
grantee = GroupManager.get_group_lp(grantee_url)
@@ -3314,6 +3330,11 @@ def jobs(request):
"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
res = dispatch(
+3 -1
View File
@@ -7,6 +7,7 @@
<i class="glyphicon glyphicon-edit"></i> Edit Package</a
>
</li>
{% if meta.current_package.shareable %}
<li>
<a
role="button"
@@ -15,6 +16,7 @@
<i class="glyphicon glyphicon-user"></i> Edit Permissions</a
>
</li>
{% endif %}
{% if meta.current_package.get_classification_level_display != "Secret" %}
<li>
<a
@@ -52,7 +54,7 @@
>
</li>
{% endif %}
{% if not meta.can_edit %}
{% if not meta.can_edit and meta.current_package.shareable %}
<li>
<a
role="button"
+2
View File
@@ -7,6 +7,7 @@
<i class="glyphicon glyphicon-edit"></i> Update</a
>
</li>
{% if 1 == 0 %}
<li>
<a
role="button"
@@ -15,6 +16,7 @@
<i class="glyphicon glyphicon-lock"></i> Update Password</a
>
</li>
{% endif %}
<li>
<a
role="button"
+59 -18
View File
@@ -180,12 +180,25 @@
>Aspartame</a
>
</div>
<a
class="absolute top-0 left-[calc(100%-5.4rem)]"
href="/predict"
>Advanced</a
>
</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
id="ketcher-container"
@@ -208,9 +221,24 @@
>
Predict!
</button>
<div class="mt-1 flex w-full justify-end">
<a class="label justify-end" href="/predict">Advanced</a>
</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>
<input
type="hidden"
@@ -232,15 +260,31 @@
</div>
<!-- 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">
<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">
<!-- News cards will be populated here -->
<div id="loading" class="flex w-full justify-center">
<span class="loading loading-spinner loading-lg"></span>
</div>
<div id="services-container" class="flex justify-center gap-4">
{% for ne in news_entries %}
<div class="card-body flex flex-col h-full justify-between p-5">
<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 class="mt-6 text-right">
@@ -252,9 +296,6 @@
Read More Announcements
</a>
</div>
<!-- Discourse API integration -->
<script src="{% static 'js/discourse-api.js' %}"></script>
</div>
</section>
@@ -31,6 +31,7 @@
{% csrf_token %}
<div class="form-control mb-3">
{% if 1 == 0 %}
<label class="label" for="default-package">
<span class="label-text">Default Package</span>
</label>
@@ -50,7 +51,7 @@
{% endfor %}
</select>
</div>
{% endif %}
<div class="form-control mb-3">
<label class="label" for="default-group">
<span class="label-text">Default Group</span>
+1 -1
View File
@@ -57,7 +57,7 @@
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<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>
{% if edge.aliases %}
+1 -1
View File
@@ -60,7 +60,7 @@
<div class="collapse-arrow bg-base-200 collapse">
<input type="checkbox" checked />
<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>
{% endif %}
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
+15 -3
View File
@@ -2,11 +2,23 @@
{% load static %}
{% block content %}
<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
<span class="text-base-content/50 text-xs"
>in <strong>{{ meta.current_package.name|safe }}</strong>
<span class="inline-flex items-center gap-1 text-base-content/50 text-xs"
>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>
</h1>
+33 -7
View File
@@ -21,6 +21,8 @@ from epdb.models import (
Compound,
CompoundStructure,
Edge,
EnzymeLink,
Group,
License,
Node,
ParallelRule,
@@ -150,6 +152,8 @@ class ReactionExportSchema(RefReactionExportSchema):
# Rules #
#########
class EnzymeExportSchema(RefEnzymeExportSchema):
name: str
description: str
ec_number: str
classification_level: int
linking_method: str
@@ -162,12 +166,12 @@ class EnzymeRuleExportSchema(RefRuleExportSchema):
@staticmethod
def resolve_enzymes(obj):
if isinstance(obj, dict):
res = []
for e in obj.get("enzymes", []):
res.append(EnzymeExportSchema.model_validate(e))
return res
return obj.enzymelink_set.all()
if isinstance(obj, EnzymeRuleExportSchema):
return obj.enzymes
elif isinstance(obj, dict):
return obj.get("enzymes", [])
else:
return obj.enzymelink_set.all()
class RuleExportSchema(EnzymeRuleExportSchema):
@@ -271,7 +275,7 @@ class PackageExportSchema(Schema):
@staticmethod
def resolve_classification_level(obj):
if isinstance(obj, dict):
return obj["classification_level"]
return obj.get("classification_level", "Internal")
return obj.Classification(obj.classification_level).name
@@ -679,6 +683,27 @@ class PackageImporter:
for scen in elem.scenarios:
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(
self,
) -> Package:
@@ -717,6 +742,7 @@ class PackageImporter:
self._import_scenarios(package, parsed.scenarios)
self._import_additional_information(package, parsed.additional_information)
self._link_scenarios_after_import(parsed)
self._import_and_link_enzymes(parsed)
return package
Generated
+1 -1
View File
@@ -894,7 +894,7 @@ provides-extras = ["ms-login", "dev", "pepper-plugin"]
[[package]]
name = "envipy-additional-information"
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 = [
{ name = "pydantic" },
]