34 Commits

Author SHA1 Message Date
a16035677c [Bugfix] Added Missing Paper in Cite Modal (#87)
Fixes #77

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#87
2025-09-05 08:21:46 +12:00
498a53ab3d [Feature] Move Login exempt urls to settings (#86)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#86
2025-09-05 08:04:20 +12:00
aa3b53e94b [Bugfix] Login required exempts auth for SSO Flow (#85)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#85
2025-09-05 07:59:14 +12:00
3c8f0e80cb [Feature] OAuth2 Provider (#84)
Fixes #74

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#84
2025-09-05 06:50:16 +12:00
4158bd36cb [Feature] Legacy API Layer (#80)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#80
2025-09-03 01:35:51 +12:00
4e02910c62 Add Link to Community in Navbar (#79)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#79
2025-09-02 08:28:37 +12:00
2babe7f7e2 [Feature] Scenario Creation (#78)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#78
2025-09-02 08:06:18 +12:00
7da3880a9b Log only unhandled or logged by 3rd party logger exceptions (#73)
Fixes #67

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#73
2025-09-01 00:16:59 +12:00
52931526c1 If Molecule contains a ~ load it as SMARTS (#71)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#71
2025-08-29 09:14:24 +12:00
dd7b28046c CSS Fix for Index Page (#69)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#69
2025-08-29 08:19:43 +12:00
8592cfae50 Change how List Pages are populated (#68)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#68
2025-08-29 08:09:57 +12:00
ec2b941a85 Fixes #60 (#66)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#66
2025-08-28 06:56:38 +12:00
02d84a9b29 Fix wrong Button Label in "Compound -> Edit" (#64)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#64
2025-08-28 06:32:42 +12:00
00d9188c0c Copy Objects between Packages (#59)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#59
2025-08-28 06:27:11 +12:00
13816ecaf3 Make URL a Field instead a property (#63)
This PR adds a new Field to all existing Models.
As its a data migrations the Migration is added.

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#63
2025-08-27 06:46:09 +12:00
6a4c8d96c3 Adjust Matomo Site ID (#57)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#57
2025-08-26 06:17:09 +12:00
97d0527565 Merge pull request 'fixed calls to create MLRelativeReasoning during bootstrap.py and test_model.py' (#56) from fix/mlrr_arguments into develop
Reviewed-on: enviPath/enviPy#56
2025-08-22 11:29:02 +12:00
b45c99f7d3 fixed calls to create MLRelativeReasoning during bootstrap.py and test_model.py 2025-08-22 11:28:01 +12:00
b95ec98a2f Added Shortcut to make Packages Public (#54)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#54
2025-08-22 07:31:08 +12:00
c79a1f2040 Fix "Impersonation" (#53)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#53
2025-08-22 07:01:49 +12:00
6e6b394289 Cleanup (#52)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#52
2025-08-22 06:36:22 +12:00
ec387cc12e Added UI elements to add/remove Scenarios to various objects (#51)
Fixes #23

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#51
2025-08-21 07:56:44 +12:00
a7637d046a External References (#50)
Fix for #24

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#50
2025-08-21 06:11:05 +12:00
fc8192fb0d Fix App Domain Bug when a Rule can be applied more than once (#49)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#49
2025-08-19 22:10:18 +12:00
c3c1d4f5cf App Domain Pathway Prediction (#47)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#47
2025-08-19 02:53:56 +12:00
3308d47071 Fix bond breaking (#46)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#46
2025-08-15 09:06:07 +12:00
1267ca8ace Enable App Domain Assessment on Model Page (#45)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#45
2025-08-12 09:02:11 +12:00
ec52b8872d Functional Group Calculation (#44)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#44
2025-08-11 09:07:07 +12:00
579cd519d0 Experimental App Domain (#43)
Backend App Domain done, Frontend missing

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#43
2025-08-08 20:52:21 +12:00
280ddc7205 Fixed UUID vs str comparison when getting User (#42)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#42
2025-07-31 09:11:52 +12:00
c9d6d8b024 Delete Stale Edges when removing a Node from a Pathway (#41)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#41
2025-07-31 07:50:50 +12:00
a1aebfa54d Model Building UI Flag (#39)
Fixes #8
Flag only disables UI Elements

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#39
2025-07-31 07:00:54 +12:00
79b4b1586c Download Pathway Functionality (#38)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#38
2025-07-31 01:30:16 +12:00
aec61151ce Correct Label assignment (#37)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#37
2025-07-31 00:43:28 +12:00
87 changed files with 7455 additions and 1364 deletions

View File

@ -1,7 +1,14 @@
from epdb.api import router as epdb_app_router
from epdb.legacy_api import router as epdb_legacy_app_router
from ninja import NinjaAPI
api = NinjaAPI()
api.add_router("/", epdb_app_router)
from ninja import NinjaAPI
api_v1 = NinjaAPI(title="API V1 Docs", urls_namespace="api-v1")
api_legacy = NinjaAPI(title="Legacy API Docs", urls_namespace="api-legacy")
# Add routers
api_v1.add_router("/", epdb_app_router)
api_legacy.add_router("/", epdb_legacy_app_router)

View File

@ -43,6 +43,7 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
# 3rd party
'django_extensions',
'oauth2_provider',
# Custom
'epdb',
'migration',
@ -60,8 +61,13 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'oauth2_provider.middleware.OAuth2TokenMiddleware',
]
OAUTH2_PROVIDER = {
"PKCE_REQUIRED": False, # Accept PKCE requests but dont require them
}
if os.environ.get('REGISTRATION_MANDATORY', False) == 'True':
MIDDLEWARE.append('epdb.middleware.login_required_middleware.LoginRequiredMiddleware')
@ -260,6 +266,8 @@ CELERY_RESULT_BACKEND = 'redis://localhost:6379/1'
CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = 'json'
MODEL_BUILDING_ENABLED = os.environ.get('MODEL_BUILDING_ENABLED', 'False') == 'True'
APPLICABILITY_DOMAIN_ENABLED = os.environ.get('APPLICABILITY_DOMAIN_ENABLED', 'False') == 'True'
DEFAULT_RF_MODEL_PARAMS = {
'base_clf': RandomForestClassifier(
n_estimators=100,
@ -273,14 +281,14 @@ DEFAULT_RF_MODEL_PARAMS = {
'num_chains': 10,
}
DEFAULT_DT_MODEL_PARAMS = {
DEFAULT_MODEL_PARAMS = {
'base_clf': DecisionTreeClassifier(
criterion='entropy',
max_depth=3,
min_samples_split=5,
min_samples_leaf=5,
# min_samples_leaf=5,
max_features='sqrt',
class_weight='balanced',
# class_weight='balanced',
random_state=42
),
'num_chains': 10,
@ -306,9 +314,36 @@ SENTRY_ENABLED = os.environ.get('SENTRY_ENABLED', 'False') == 'True'
if SENTRY_ENABLED:
import sentry_sdk
def before_send(event, hint):
# Check if was a handled exception by one of our loggers
if event.get('logger'):
for log_path in LOGGING.get('loggers').keys():
if event['logger'].startswith(log_path):
return None
return event
sentry_sdk.init(
dsn=os.environ.get('SENTRY_DSN'),
# Add data like request headers and IP for users,
# see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
send_default_pii=True,
environment=os.environ.get('SENTRY_ENVIRONMENT', 'development'),
before_send=before_send,
)
# compile into digestible flags
FLAGS = {
'MODEL_BUILDING': MODEL_BUILDING_ENABLED,
'CELERY': FLAG_CELERY_PRESENT,
'PLUGINS': PLUGINS_ENABLED,
'SENTRY': SENTRY_ENABLED,
'ENVIFORMER': ENVIFORMER_PRESENT,
'APPLICABILITY_DOMAIN': APPLICABILITY_DOMAIN_ENABLED,
}
LOGIN_EXEMPT_URLS = [
'/api/legacy/',
'/o/token/',
'/o/userinfo/',
]

View File

@ -17,11 +17,13 @@ Including another URLconf
from django.contrib import admin
from django.urls import include, path
from .api import api
from .api import api_v1, api_legacy
urlpatterns = [
path("", include("epdb.urls")),
path("", include("migration.urls")),
path("admin/", admin.site.urls),
path("api/", api.urls),
path("api/v1/", api_v1.urls),
path("api/legacy/", api_legacy.urls),
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
]

View File

@ -1,40 +1,105 @@
from django.contrib import admin
from .models import User, Group, UserPackagePermission, GroupPackagePermission, Setting, SimpleAmbitRule, Scenario
from .models import (
User,
UserPackagePermission,
Group,
GroupPackagePermission,
Package,
MLRelativeReasoning,
Compound,
CompoundStructure,
SimpleAmbitRule,
ParallelRule,
Reaction,
Pathway,
Node,
Edge,
Scenario,
Setting
)
class UserAdmin(admin.ModelAdmin):
pass
class GroupAdmin(admin.ModelAdmin):
pass
class UserPackagePermissionAdmin(admin.ModelAdmin):
pass
class GroupAdmin(admin.ModelAdmin):
pass
class GroupPackagePermissionAdmin(admin.ModelAdmin):
pass
class SettingAdmin(admin.ModelAdmin):
class EPAdmin(admin.ModelAdmin):
search_fields = ['name', 'description']
class PackageAdmin(EPAdmin):
pass
class MLRelativeReasoningAdmin(EPAdmin):
pass
class SimpleAmbitRuleAdmin(admin.ModelAdmin):
class CompoundAdmin(EPAdmin):
pass
class ScenarioAdmin(admin.ModelAdmin):
class CompoundStructureAdmin(EPAdmin):
pass
class SimpleAmbitRuleAdmin(EPAdmin):
pass
class ParallelRuleAdmin(EPAdmin):
pass
class ReactionAdmin(EPAdmin):
pass
class PathwayAdmin(EPAdmin):
pass
class NodeAdmin(EPAdmin):
pass
class EdgeAdmin(EPAdmin):
pass
class ScenarioAdmin(EPAdmin):
pass
class SettingAdmin(EPAdmin):
pass
admin.site.register(User, UserAdmin)
admin.site.register(Group, GroupAdmin)
admin.site.register(UserPackagePermission, UserPackagePermissionAdmin)
admin.site.register(Group, GroupAdmin)
admin.site.register(GroupPackagePermission, GroupPackagePermissionAdmin)
admin.site.register(Setting, SettingAdmin)
admin.site.register(Package, PackageAdmin)
admin.site.register(MLRelativeReasoning, MLRelativeReasoningAdmin)
admin.site.register(Compound, CompoundAdmin)
admin.site.register(CompoundStructure, CompoundStructureAdmin)
admin.site.register(SimpleAmbitRule, SimpleAmbitRuleAdmin)
admin.site.register(ParallelRule, ParallelRuleAdmin)
admin.site.register(Reaction, ReactionAdmin)
admin.site.register(Pathway, PathwayAdmin)
admin.site.register(Node, NodeAdmin)
admin.site.register(Edge, EdgeAdmin)
admin.site.register(Setting, SettingAdmin)
admin.site.register(Scenario, ScenarioAdmin)

View File

@ -4,3 +4,6 @@ from django.apps import AppConfig
class EPDBConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'epdb'
def ready(self):
import epdb.signals # noqa: F401

739
epdb/legacy_api.py Normal file
View File

@ -0,0 +1,739 @@
from typing import List, Dict, Optional, Any
from django.contrib.auth import get_user_model
from django.http import HttpResponse
from ninja import Router, Schema, Field, Form
from utilities.chem import FormatConverter
from .logic import PackageManager
from .models import Compound, CompoundStructure, Package, User, UserPackagePermission, Rule, Reaction, Scenario, Pathway
def _anonymous_or_real(request):
if request.user.is_authenticated and not request.user.is_anonymous:
return request.user
return get_user_model().objects.get(username='anonymous')
# router = Router(auth=SessionAuth())
router = Router()
class Error(Schema):
message: str
class SimpleObject(Schema):
id: str = Field(None, alias="url")
name: str = Field(None, alias="name")
reviewStatus: bool = Field(None, alias="package.reviewed")
################
# Login/Logout #
################
class SimpleUser(Schema):
id: str = Field(None, alias="url")
identifier: str = 'user'
name: str = Field(None, alias='username')
email: str = Field(None, alias='email')
@router.post("/", response={200: SimpleUser, 403: Error})
def login(request, loginusername: Form[str], loginpassword: Form[str], hiddenMethod: Form[str]):
from django.contrib.auth import authenticate
from django.contrib.auth import login
email = User.objects.get(username=loginusername).email
user = authenticate(username=email, password=loginpassword)
if user:
login(request, user)
return user
else:
return 403, {'message': 'Invalid username or password'}
class SimpleGroup(Schema):
id: str
identifier: str = 'group'
name: str
###########
# Package #
###########
class SimplePackage(SimpleObject):
identifier: str = 'package'
reviewStatus: bool = Field(None, alias="reviewed")
class PackageSchema(Schema):
description: str = Field(None, alias="description")
id: str = Field(None, alias="url")
links: List[Dict[str, List[str | int]]] = Field([], alias="links")
name: str = Field(None, alias="name")
primaryGroup: Optional[SimpleGroup] = None
readers: List[Dict[str, str]] = Field([], alias="readers")
reviewComment: str = Field(None, alias="review_comment")
reviewStatus: str = Field(None, alias="review_status")
writers: List[Dict[str, str]] = Field([], alias="writers")
@staticmethod
def resolve_links(obj: Package):
return [
{
'Pathways': [
f'{obj.url}/pathway', obj.pathways.count()
]
}, {
'Rules': [
f'{obj.url}/rule', obj.rules.count()
]
}, {
'Compounds': [
f'{obj.url}/compound', obj.compounds.count()
]
}, {
'Reactions': [
f'{obj.url}/reaction', obj.reactions.count()
]
}, {
'Relative Reasoning': [
f'{obj.url}/relative-reasoning', obj.models.count()
]
}, {
'Scenarios': [
f'{obj.url}/scenario', obj.scenarios.count()
]
}
]
@staticmethod
def resolve_readers(obj: Package):
users = User.objects.filter(
id__in=UserPackagePermission.objects.filter(
package=obj,
permission=UserPackagePermission.READ[0]
).values_list('user', flat=True)
).distinct()
return [{u.id: u.name} for u in users]
@staticmethod
def resolve_writers(obj: Package):
users = User.objects.filter(
id__in=UserPackagePermission.objects.filter(
package=obj,
permission=UserPackagePermission.WRITE[0]
).values_list('user', flat=True)
).distinct()
return [{u.id: u.name} for u in users]
@staticmethod
def resolve_review_comment(obj):
return ""
@staticmethod
def resolve_review_status(obj):
return 'reviewed' if obj.reviewed else 'unreviewed'
class PackageWrapper(Schema):
package: List['PackageSchema']
@router.get("/package", response={200: PackageWrapper, 403: Error})
def get_packages(request):
return {'package': PackageManager.get_all_readable_packages(request.user, include_reviewed=True)}
@router.get("/package/{uuid:package_uuid}", response={200: PackageSchema, 403: Error})
def get_package(request, package_uuid):
try:
return PackageManager.get_package_by_id(request.user, package_uuid)
except ValueError:
return 403, {
'message': f'Getting Package with id {package_uuid} failed due to insufficient rights!'}
################################
# Compound / CompoundStructure #
################################
class SimpleCompound(SimpleObject):
identifier: str = 'compound'
class CompoundPathwayScenario(Schema):
scenarioId: str
scenarioName: str
scenarioType: str
class CompoundSchema(Schema):
aliases: List[str] = Field([], alias="aliases")
description: str = Field(None, alias="description")
externalReferences: Dict[str, List[str]] = Field(None, alias="external_references")
id: str = Field(None, alias="url")
halflifes: List[Dict[str, str]] = Field([], alias="halflifes")
identifier: str = 'compound'
imageSize: int = 600
name: str = Field(None, alias="name")
pathwayScenarios: List[CompoundPathwayScenario] = Field([], alias="pathway_scenarios")
pathways: List['SimplePathway'] = Field([], alias="related_pathways")
pubchemCompoundReferences: List[str] = Field([], alias="pubchem_compound_references")
reactions: List['SimpleReaction'] = Field([], alias="related_reactions")
reviewStatus: str = Field(False, alias="review_status")
scenarios: List['SimpleScenario'] = Field([], alias="scenarios")
structures: List['CompoundStructureSchema'] = []
@staticmethod
def resolve_review_status(obj: CompoundStructure):
return 'reviewed' if obj.package.reviewed else 'unreviewed'
@staticmethod
def resolve_external_references(obj: Compound):
# TODO
return {}
@staticmethod
def resolve_structures(obj: Compound):
return CompoundStructure.objects.filter(compound=obj)
@staticmethod
def resolve_halflifes(obj: Compound):
return []
@staticmethod
def resolve_pubchem_compound_references(obj: Compound):
return []
@staticmethod
def resolve_pathway_scenarios(obj: Compound):
return [
{
'scenarioId': 'https://envipath.org/package/5882df9c-dae1-4d80-a40e-db4724271456/scenario/cd8350cd-4249-4111-ba9f-4e2209338501',
'scenarioName': 'Fritz, R. & Brauner, A. (1989) - (00004)',
'scenarioType': 'Soil'
}
]
class CompoundWrapper(Schema):
compound: List['SimpleCompound']
class SimpleCompoundStructure(SimpleObject):
identifier: str = 'structure'
reviewStatus: bool = Field(None, alias="compound.package.reviewed")
class CompoundStructureSchema(Schema):
InChI: str = Field(None, alias="inchi")
aliases: List[str] = Field([], alias="aliases")
canonicalSmiles: str = Field(None, alias="canonical_smiles")
charge: int = Field(None, alias="charge")
description: str = Field(None, alias="description")
externalReferences: Dict[str, List[str]] = Field(None, alias="external_references")
formula: str = Field(None, alias="formula")
halflifes: List[Dict[str, str]] = Field([], alias="halflifes")
id: str = Field(None, alias="url")
identifier: str = 'structure'
imageSize: int = 600
inchikey: str = Field(None, alias="inchikey")
isDefaultStructure: bool = Field(None, alias="is_default_structure")
mass: float = Field(None, alias="mass")
name: str = Field(None, alias="name")
pathways: List['SimplePathway'] = Field([], alias="related_pathways")
pubchemCompoundReferences: List[str] = Field([], alias="pubchem_compound_references")
reactions: List['SimpleReaction'] = Field([], alias="related_reactions")
reviewStatus: str = Field(None, alias="review_status")
scenarios: List['SimpleScenario'] = Field([], alias="scenarios")
smiles: str = Field(None, alias="smiles")
@staticmethod
def resolve_review_status(obj: CompoundStructure):
return 'reviewed' if obj.compound.package.reviewed else 'unreviewed'
@staticmethod
def resolve_inchi(obj: CompoundStructure):
return FormatConverter.InChI(obj.smiles)
@staticmethod
def resolve_charge(obj: CompoundStructure):
print(obj.smiles)
print(FormatConverter.charge(obj.smiles))
return FormatConverter.charge(obj.smiles)
@staticmethod
def resolve_formula(obj: CompoundStructure):
return FormatConverter.formula(obj.smiles)
@staticmethod
def resolve_mass(obj: CompoundStructure):
return FormatConverter.mass(obj.smiles)
@staticmethod
def resolve_external_references(obj: CompoundStructure):
# TODO
return {}
@staticmethod
def resolve_halflifes(obj: CompoundStructure):
return []
@staticmethod
def resolve_pubchem_compound_references(obj: CompoundStructure):
return []
@staticmethod
def resolve_pathway_scenarios(obj: CompoundStructure):
return [
{
'scenarioId': 'https://envipath.org/package/5882df9c-dae1-4d80-a40e-db4724271456/scenario/cd8350cd-4249-4111-ba9f-4e2209338501',
'scenarioName': 'Fritz, R. & Brauner, A. (1989) - (00004)',
'scenarioType': 'Soil'
}
]
class CompoundStructureWrapper(Schema):
structure: List['SimpleCompoundStructure']
@router.get("/compound", response={200: CompoundWrapper, 403: Error})
def get_compounds(request):
qs = Compound.objects.none()
for p in PackageManager.get_reviewed_packages():
qs |= Compound.objects.filter(package=p)
return {'compound': qs}
@router.get("/package/{uuid:package_uuid}/compound", response={200: CompoundWrapper, 403: Error})
def get_package_compounds(request, package_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return {'compound': Compound.objects.filter(package=p).prefetch_related('package')}
except ValueError:
return 403, {
'message': f'Getting Compounds for Package with id {package_uuid} failed due to insufficient rights!'}
@router.get("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}", response={200: CompoundSchema, 403: Error})
def get_package_compound(request, package_uuid, compound_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return Compound.objects.get(package=p, uuid=compound_uuid)
except ValueError:
return 403, {
'message': f'Getting Compound with id {compound_uuid} failed due to insufficient rights!'}
@router.get("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure",
response={200: CompoundStructureWrapper, 403: Error})
def get_package_compound_structures(request, package_uuid, compound_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return {'structure': Compound.objects.get(package=p, uuid=compound_uuid).structures.all()}
except ValueError:
return 403, {
'message': f'Getting CompoundStructures for Compound with id {compound_uuid} failed due to insufficient rights!'}
@router.get("/package/{uuid:package_uuid}/compound/{uuid:compound_uuid}/structure/{uuid:structure_uuid}",
response={200: CompoundStructureSchema, 403: Error})
def get_package_compound_structure(request, package_uuid, compound_uuid, structure_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return CompoundStructure.objects.get(uuid=structure_uuid,
compound=Compound.objects.get(package=p, uuid=compound_uuid))
except ValueError:
return 403, {
'message': f'Getting CompoundStructure with id {structure_uuid} failed due to insufficient rights!'}
#########
# Rules #
#########
class SimpleRule(SimpleObject):
identifier: str = 'rule'
@staticmethod
def resolve_url(obj: Rule):
return obj.url.replace('-ambit-', '-').replace('-rdkit-', '-')
class RuleWrapper(Schema):
rule: List['SimpleRule']
class SimpleRuleSchema(Schema):
aliases: List[str] = Field([], alias="aliases")
description: str = Field(None, alias="description")
ecNumbers: List[Dict[str, str]] = Field([], alias="ec_numbers")
engine: str = 'ambit'
id: str = Field(None, alias="url")
identifier: str = Field(None, alias="identifier")
isCompositeRule: bool = False
name: str = Field(None, alias="name")
pathways: List['SimplePathway'] = Field([], alias="related_pathways")
productFilterSmarts: str = Field("", alias="product_filter_smarts")
productSmarts: str = Field(None, alias="products_smarts")
reactantFilterSmarts: str = Field("", alias="reactant_filter_smarts")
reactantSmarts: str = Field(None, alias="reactants_smarts")
reactions: List['SimpleReaction'] = Field([], alias="related_reactions")
reviewStatus: str = Field(None, alias="review_status")
scenarios: List['SimpleScenario'] = Field([], alias="scenarios")
smirks: str = Field("", alias="smirks")
# TODO
transformations: str = Field("", alias="transformations")
@staticmethod
def resolve_url(obj: Rule):
return obj.url.replace('-ambit-', '-').replace('-rdkit-', '-')
@staticmethod
def resolve_identifier(obj: Rule):
if 'simple-rule' in obj.url:
return 'simple-rule'
if 'simple-ambit-rule' in obj.url:
return 'simple-rule'
elif 'parallel-rule' in obj.url:
return 'parallel-rule'
elif 'sequential-rule' in obj.url:
return 'sequential-rule'
else:
return None
@staticmethod
def resolve_review_status(obj: Rule):
return 'reviewed' if obj.package.reviewed else 'unreviewed'
@staticmethod
def resolve_product_filter_smarts(obj: Rule):
return obj.product_filter_smarts if obj.product_filter_smarts else ''
@staticmethod
def resolve_reactant_filter_smarts(obj: Rule):
return obj.reactant_filter_smarts if obj.reactant_filter_smarts else ''
class CompositeRuleSchema(Schema):
aliases: List[str] = Field([], alias="aliases")
description: str = Field(None, alias="description")
ecNumbers: List[Dict[str, str]] = Field([], alias="ec_numbers")
id: str = Field(None, alias="url")
identifier: str = Field(None, alias="identifier")
isCompositeRule: bool = True
name: str = Field(None, alias="name")
pathways: List['SimplePathway'] = Field([], alias="related_pathways")
productFilterSmarts: str = Field("", alias="product_filter_smarts")
reactantFilterSmarts: str = Field("", alias="reactant_filter_smarts")
reactions: List['SimpleReaction'] = Field([], alias="related_reactions")
reviewStatus: str = Field(None, alias="review_status")
scenarios: List['SimpleScenario'] = Field([], alias="scenarios")
simpleRules: List['SimpleRule'] = Field([], alias="simple_rules")
@staticmethod
def resolve_ec_numbers(obj: Rule):
return []
@staticmethod
def resolve_url(obj: Rule):
return obj.url.replace('-ambit-', '-').replace('-rdkit-', '-')
@staticmethod
def resolve_identifier(obj: Rule):
if 'simple-rule' in obj.url:
return 'simple-rule'
if 'simple-ambit-rule' in obj.url:
return 'simple-rule'
elif 'parallel-rule' in obj.url:
return 'parallel-rule'
elif 'sequential-rule' in obj.url:
return 'sequential-rule'
else:
return None
@staticmethod
def resolve_review_status(obj: Rule):
return 'reviewed' if obj.package.reviewed else 'unreviewed'
@staticmethod
def resolve_product_filter_smarts(obj: Rule):
return obj.product_filter_smarts if obj.product_filter_smarts else ''
@staticmethod
def resolve_reactant_filter_smarts(obj: Rule):
return obj.reactant_filter_smarts if obj.reactant_filter_smarts else ''
@router.get("/rule", response={200: RuleWrapper, 403: Error})
def get_rules(request):
qs = Rule.objects.none()
for p in PackageManager.get_reviewed_packages():
qs |= Rule.objects.filter(package=p)
return {'rule': qs}
@router.get("/package/{uuid:package_uuid}/rule", response={200: RuleWrapper, 403: Error})
def get_package_rules(request, package_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return {'rule': Rule.objects.filter(package=p).prefetch_related('package')}
except ValueError:
return 403, {
'message': f'Getting Rules for Package with id {package_uuid} failed due to insufficient rights!'}
@router.get("/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}",
response={200: SimpleRuleSchema | CompositeRuleSchema, 403: Error})
def get_package_rule(request, package_uuid, rule_uuid):
return _get_package_rule(request, package_uuid, rule_uuid)
@router.get("/package/{uuid:package_uuid}/simple-rule/{uuid:rule_uuid}",
response={200: SimpleRuleSchema | CompositeRuleSchema, 403: Error})
def get_package_simple_rule(request, package_uuid, rule_uuid):
return _get_package_rule(request, package_uuid, rule_uuid)
@router.get("/package/{uuid:package_uuid}/parallel-rule/{uuid:rule_uuid}",
response={200: SimpleRuleSchema | CompositeRuleSchema, 403: Error})
def get_package_parallel_rule(request, package_uuid, rule_uuid):
return _get_package_rule(request, package_uuid, rule_uuid)
def _get_package_rule(request, package_uuid, rule_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return Rule.objects.get(package=p, uuid=rule_uuid)
except ValueError:
return 403, {
'message': f'Getting Rule with id {rule_uuid} failed due to insufficient rights!'}
# POST
@router.post("/package/{uuid:package_uuid}/rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error})
def post_package_rule(request, package_uuid, rule_uuid, compound: Form[str] = None):
return _post_package_rule(request, package_uuid, rule_uuid, compound=compound)
@router.post("/package/{uuid:package_uuid}/simple-rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error})
def post_package_simple_rule(request, package_uuid, rule_uuid, compound: Form[str] = None):
return _post_package_rule(request, package_uuid, rule_uuid, compound=compound)
@router.post("/package/{uuid:package_uuid}/parallel-rule/{uuid:rule_uuid}", response={200: str | Any, 403: Error})
def post_package_parallel_rule(request, package_uuid, rule_uuid, compound: Form[str] = None):
return _post_package_rule(request, package_uuid, rule_uuid, compound=compound)
def _post_package_rule(request, package_uuid, rule_uuid, compound: Form[str]):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
r = Rule.objects.get(package=p, uuid=rule_uuid)
if compound is not None:
if not compound.split():
return 400, {'message': 'Compound is empty'}
product_sets = r.apply(compound)
res = []
for p_set in product_sets:
for product in p_set:
res.append(product)
return HttpResponse('\n'.join(res), content_type="text/plain")
return r
except ValueError:
return 403, {
'message': f'Getting Rule with id {rule_uuid} failed due to insufficient rights!'}
############
# Reaction #
############
class SimpleReaction(SimpleObject):
identifier: str = 'reaction'
class ReactionWrapper(Schema):
reaction: List['SimpleReaction']
class ReactionCompoundStructure(Schema):
compoundName: str = Field(None, alias="name")
id: str = Field(None, alias="url")
smiles: str = Field(None, alias="smiles")
class ReactionSchema(Schema):
aliases: List[str] = Field([], alias="aliases")
description: str = Field(None, alias="description")
ecNumbers: List[Dict[str, str]] = Field([], alias="ec_numbers")
educts: List['ReactionCompoundStructure'] = Field([], alias="educts")
id: str = Field(None, alias="url")
identifier: str = 'reaction'
medlineRefs: List[str] = Field([], alias="medline_references")
multistep: bool = Field(None, alias="multi_step")
name: str = Field(None, alias="name")
pathways: List['SimplePathway'] = Field([], alias="related_pathways")
products: List['ReactionCompoundStructure'] = Field([], alias="products")
references: List[Dict[str, List[str]]] = Field([], alias="references")
reviewStatus: str = Field(None, alias="review_status")
scenarios: List['SimpleScenario'] = Field([], alias="scenarios")
smirks: str = Field("", alias="smirks")
@staticmethod
def resolve_smirks(obj: Reaction):
return obj.smirks()
@staticmethod
def resolve_ec_numbers(obj: Reaction):
# TODO fetch via scenario EnzymeAI
return []
@staticmethod
def resolve_references(obj: Reaction):
# TODO
return []
@staticmethod
def resolve_medline_references(obj: Reaction):
# TODO
return []
@staticmethod
def resolve_review_status(obj: Rule):
return 'reviewed' if obj.package.reviewed else 'unreviewed'
@router.get("/reaction", response={200: ReactionWrapper, 403: Error})
def get_reactions(request):
qs = Reaction.objects.none()
for p in PackageManager.get_reviewed_packages():
qs |= Reaction.objects.filter(package=p)
return {'reaction': qs}
@router.get("/package/{uuid:package_uuid}/reaction", response={200: ReactionWrapper, 403: Error})
def get_package_reactions(request, package_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return {'reaction': Reaction.objects.filter(package=p).prefetch_related('package')}
except ValueError:
return 403, {
'message': f'Getting Reactions for Package with id {package_uuid} failed due to insufficient rights!'}
@router.get("/package/{uuid:package_uuid}/reaction/{uuid:reaction_uuid}", response={200: ReactionSchema, 403: Error})
def get_package_reaction(request, package_uuid, reaction_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return Reaction.objects.get(package=p, uuid=reaction_uuid)
except ValueError:
return 403, {
'message': f'Getting Reaction with id {reaction_uuid} failed due to insufficient rights!'}
############
# Scenario #
############
class SimpleScenario(SimpleObject):
identifier: str = 'scenario'
class ScenarioWrapper(Schema):
scenario: List['SimpleScenario']
class ScenarioSchema(Schema):
aliases: List[str] = Field([], alias="aliases")
collection: Dict['str', List[Dict[str, Any]]] = Field([], alias="collection")
collectionID: Optional[str] = None
description: str = Field(None, alias="description")
id: str = Field(None, alias="url")
identifier: str = 'scenario'
linkedTo: List[Dict[str, str]] = Field({}, alias="linked_to")
name: str = Field(None, alias="name")
pathways: List['SimplePathway'] = Field([], alias="related_pathways")
relatedScenarios: List[Dict[str, str]] = Field([], alias="related_scenarios")
reviewStatus: str = Field(None, alias="review_status")
scenarios: List['SimpleScenario'] = Field([], alias="scenarios")
type: str = Field(None, alias="scenario_type")
@staticmethod
def resolve_collection(obj: Scenario):
return obj.additional_information
@staticmethod
def resolve_review_status(obj: Rule):
return 'reviewed' if obj.package.reviewed else 'unreviewed'
@router.get("/scenario", response={200: ScenarioWrapper, 403: Error})
def get_scenarios(request):
qs = Scenario.objects.none()
for p in PackageManager.get_reviewed_packages():
qs |= Scenario.objects.filter(package=p)
return {'scenario': qs}
@router.get("/package/{uuid:package_uuid}/scenario", response={200: ScenarioWrapper, 403: Error})
def get_package_scenarios(request, package_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return {'scenario': Scenario.objects.filter(package=p).prefetch_related('package')}
except ValueError:
return 403, {
'message': f'Getting Scenarios for Package with id {package_uuid} failed due to insufficient rights!'}
@router.get("/package/{uuid:package_uuid}/scenario/{uuid:scenario_uuid}", response={200: ScenarioSchema, 403: Error})
def get_package_scenario(request, package_uuid, scenario_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return Scenario.objects.get(package=p, uuid=scenario_uuid)
except ValueError:
return 403, {
'message': f'Getting Scenario with id {scenario_uuid} failed due to insufficient rights!'}
###########
# Pathway #
###########
class SimplePathway(SimpleObject):
identifier: str = 'pathway'
class PathwayWrapper(Schema):
pathway: List['SimplePathway']
@router.get("/pathway", response={200: PathwayWrapper, 403: Error})
def get_pathways(request):
qs = Pathway.objects.none()
for p in PackageManager.get_reviewed_packages():
qs |= Pathway.objects.filter(package=p)
return {'pathway': qs}
@router.get("/package/{uuid:package_uuid}/pathway", response={200: PathwayWrapper, 403: Error})
def get_package_pathways(request, package_uuid):
try:
p = PackageManager.get_package_by_id(request.user, package_uuid)
return {'pathway': Pathway.objects.filter(package=p).prefetch_related('package')}
except ValueError:
return 403, {
'message': f'Getting Pathways for Package with id {package_uuid} failed due to insufficient rights!'}
# @router.get("/package/{uuid:package_uuid}/pathway/{uuid:pathway_uuid}", response={200: Pathway, 403: Error})
# def get_package_pathway(request, package_uuid, pathway_uuid):
# try:
# p = PackageManager.get_package_by_id(request.user, package_uuid)
# return Pathway.objects.get(package=p, uuid=pathway_uuid)
# except ValueError:
# return 403, {
# 'message': f'Getting Pathway with id {pathway_uuid} failed due to insufficient rights!'}

View File

@ -1,6 +1,7 @@
import re
import logging
from typing import Union, List, Optional, Set, Dict
import json
from typing import Union, List, Optional, Set, Dict, Any
from django.contrib.auth import get_user_model
from django.db import transaction
@ -13,6 +14,132 @@ from utilities.chem import FormatConverter
logger = logging.getLogger(__name__)
class EPDBURLParser:
UUID_PATTERN = r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
MODEL_PATTERNS = {
'epdb.User': re.compile(rf'^.*/user/{UUID_PATTERN}'),
'epdb.Group': re.compile(rf'^.*/group/{UUID_PATTERN}'),
'epdb.Package': re.compile(rf'^.*/package/{UUID_PATTERN}'),
'epdb.Compound': re.compile(rf'^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}'),
'epdb.CompoundStructure': re.compile(rf'^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}/structure/{UUID_PATTERN}'),
'epdb.Rule': re.compile(rf'^.*/package/{UUID_PATTERN}/(?:simple-ambit-rule|simple-rdkit-rule|parallel-rule|sequential-rule|rule)/{UUID_PATTERN}'),
'epdb.Reaction': re.compile(rf'^.*/package/{UUID_PATTERN}/reaction/{UUID_PATTERN}$'),
'epdb.Pathway': re.compile(rf'^.*/package/{UUID_PATTERN}/pathway/{UUID_PATTERN}'),
'epdb.Node': re.compile(rf'^.*/package/{UUID_PATTERN}/pathway/{UUID_PATTERN}/node/{UUID_PATTERN}'),
'epdb.Edge': re.compile(rf'^.*/package/{UUID_PATTERN}/pathway/{UUID_PATTERN}/edge/{UUID_PATTERN}'),
'epdb.Scenario': re.compile(rf'^.*/package/{UUID_PATTERN}/scenario/{UUID_PATTERN}'),
'epdb.EPModel': re.compile(rf'^.*/package/{UUID_PATTERN}/model/{UUID_PATTERN}'),
'epdb.Setting': re.compile(rf'^.*/setting/{UUID_PATTERN}'),
}
def __init__(self, url: str):
self.url = url
self._matches = {}
self._analyze_url()
def _analyze_url(self):
for model_path, pattern in self.MODEL_PATTERNS.items():
match = pattern.findall(self.url)
if match:
self._matches[model_path] = match[0]
def _get_model_class(self, model_path: str):
try:
from django.apps import apps
app_label, model_name = model_path.split('.')[-2:]
return apps.get_model(app_label, model_name)
except (ImportError, LookupError, ValueError):
raise ValueError(f"Model {model_path} does not exist!")
def _get_object_by_url(self, model_path: str, url: str):
model_class = self._get_model_class(model_path)
return model_class.objects.get(url=url)
def is_package_url(self) -> bool:
return bool(re.compile(rf'^.*/package/{self.UUID_PATTERN}$').findall(self.url))
def contains_package_url(self):
return bool(self.MODEL_PATTERNS['epdb.Package'].findall(self.url)) and not self.is_package_url()
def is_user_url(self) -> bool:
return bool(self.MODEL_PATTERNS['epdb.User'].findall(self.url))
def is_group_url(self) -> bool:
return bool(self.MODEL_PATTERNS['epdb.Group'].findall(self.url))
def is_setting_url(self) -> bool:
return bool(self.MODEL_PATTERNS['epdb.Setting'].findall(self.url))
def get_object(self) -> Optional[Any]:
# Define priority order from most specific to least specific
priority_order = [
# 3rd level
'epdb.CompoundStructure',
'epdb.Node',
'epdb.Edge',
# 2nd level
'epdb.Compound',
'epdb.Rule',
'epdb.Reaction',
'epdb.Scenario',
'epdb.EPModel',
'epdb.Pathway',
# 1st level
'epdb.Package',
'epdb.Setting',
'epdb.Group',
'epdb.User',
]
for model_path in priority_order:
if model_path in self._matches:
url = self._matches[model_path]
return self._get_object_by_url(model_path, url)
raise ValueError(f"No object found for URL {self.url}")
def get_objects(self) -> List[Any]:
"""
Get all Django model objects along the URL path in hierarchical order.
Returns objects from parent to child (e.g., Package -> Compound -> Structure).
"""
objects = []
hierarchy_order = [
# 1st level
'epdb.Package',
'epdb.Setting',
'epdb.Group',
'epdb.User',
# 2nd level
'epdb.Compound',
'epdb.Rule',
'epdb.Reaction',
'epdb.Scenario',
'epdb.EPModel',
'epdb.Pathway',
# 3rd level
'epdb.CompoundStructure',
'epdb.Node',
'epdb.Edge',
]
for model_path in hierarchy_order:
if model_path in self._matches:
url = self._matches[model_path]
objects.append(self._get_object_by_url(model_path, url))
return objects
def __str__(self) -> str:
return f"EPDBURLParser(url='{self.url}')"
def __repr__(self) -> str:
return f"EPDBURLParser(url='{self.url}', matches={list(self._matches.keys())})"
class UserManager(object):
user_pattern = re.compile(r".*/user/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")
@ -62,7 +189,7 @@ class UserManager(object):
@staticmethod
def get_user_by_id(user, user_uuid: str):
if user.uuid != user_uuid and not user.is_superuser:
if str(user.uuid) != user_uuid and not user.is_superuser:
raise ValueError("Getting user failed!")
return get_user_model().objects.get(uuid=user_uuid)
@ -183,6 +310,25 @@ class PackageManager(object):
return True
return False
@staticmethod
def administrable(user, package):
if UserPackagePermission.objects.filter(package=package, user=user, permission=Permission.ALL[0]).exists() or \
GroupPackagePermission.objects.filter(package=package, group__in=GroupManager.get_groups(user), permission=Permission.ALL[0]).exists() or \
user.is_superuser:
return True
return False
# @staticmethod
# def get_package_permission(user: 'User', package: Union[str, 'Package']):
# if PackageManager.administrable(user, package):
# return Permission.ALL[0]
# elif PackageManager.writable(user, package):
# return Permission.WRITE[0]
# elif PackageManager.readable(user, package):
# return Permission.READ[0]
# else:
# return None
@staticmethod
def has_package_permission(user: 'User', package: Union[str, 'Package'], permission: str):
@ -339,7 +485,7 @@ class PackageManager(object):
@staticmethod
@transaction.atomic
def import_package(data: dict, owner: User, keep_ids=False):
def import_package(data: dict, owner: User, keep_ids=False, add_import_timestamp=True):
from uuid import UUID, uuid4
from datetime import datetime
from collections import defaultdict
@ -349,7 +495,12 @@ class PackageManager(object):
pack = Package()
pack.uuid = UUID(data['id'].split('/')[-1]) if keep_ids else uuid4()
if add_import_timestamp:
pack.name = '{} - {}'.format(data['name'], datetime.now().strftime('%Y-%m-%d %H:%M'))
else:
pack.name = data['name']
pack.reviewed = True if data['reviewStatus'] == 'reviewed' else False
pack.description = data['description']
pack.save()
@ -402,11 +553,13 @@ class PackageManager(object):
try:
res = AdditionalInformationConverter.convert(name, addinf_data)
res_cls_name = res.__class__.__name__
ai_data = json.loads(res.model_dump_json())
ai_data['uuid'] = f"{uuid4()}"
new_add_inf[res_cls_name].append(ai_data)
except:
logger.error(f"Failed to convert {name} with {addinf_data}")
new_add_inf[name].append(res.model_dump_json())
scen.additional_information = new_add_inf
scen.save()
@ -890,9 +1043,10 @@ class SearchManager(object):
class SNode(object):
def __init__(self, smiles: str, depth: int):
def __init__(self, smiles: str, depth: int, app_domain_assessment: dict = None):
self.smiles = smiles
self.depth = depth
self.app_domain_assessment = app_domain_assessment
def __hash__(self):
return hash(self.smiles)
@ -1035,7 +1189,7 @@ class SPathway(object):
def depth(self):
return max([v.depth for v in self.smiles_to_node.values()])
def _get_nodes_for_depth(self, depth: int):
def _get_nodes_for_depth(self, depth: int) -> List[SNode]:
if depth == 0:
return self.root_nodes
@ -1046,7 +1200,7 @@ class SPathway(object):
return sorted(res, key=lambda x: x.smiles)
def _get_edges_for_depth(self, depth: int):
def _get_edges_for_depth(self, depth: int) -> List[SEdge]:
res = []
for e in self.edges:
for n in e.educts:
@ -1071,15 +1225,44 @@ class SPathway(object):
new_tp = False
if substrates:
for sub in substrates:
if sub.app_domain_assessment is None:
if self.prediction_setting.model:
if self.prediction_setting.model.app_domain:
app_domain_assessment = self.prediction_setting.model.app_domain.assess(sub.smiles)[0]
if self.persist is not None:
n = self.snode_persist_lookup[sub]
assert n.id is not None, "Node has no id! Should have been saved already... aborting!"
node_data = n.simple_json()
node_data['image'] = f"{n.url}?image=svg"
app_domain_assessment['assessment']['node'] = node_data
n.kv['app_domain_assessment'] = app_domain_assessment
n.save()
sub.app_domain_assessment = app_domain_assessment
candidates = self.prediction_setting.expand(self, sub)
# candidates is a List of PredictionResult. The length of the List is equal to the number of rules
for cand_set in candidates:
if cand_set:
new_tp = True
# cand_set is a PredictionResult object that can consist of multiple candidate reactions
for cand in cand_set:
cand_nodes = []
# candidate reactions can have multiple fragments
for c in cand:
if c not in self.smiles_to_node:
self.smiles_to_node[c] = SNode(c, sub.depth + 1)
# For new nodes do an AppDomain Assessment if an AppDomain is attached
app_domain_assessment = None
if self.prediction_setting.model:
if self.prediction_setting.model.app_domain:
app_domain_assessment = self.prediction_setting.model.app_domain.assess(c)[0]
self.smiles_to_node[c] = SNode(c, sub.depth + 1, app_domain_assessment)
node = self.smiles_to_node[c]
cand_nodes.append(node)
@ -1092,18 +1275,30 @@ class SPathway(object):
if len(substrates) == 0 or from_node is not None:
self.done = True
# Check if we need to write back data to database
# Check if we need to write back data to the database
if new_tp and self.persist:
self._sync_to_pathway()
# call save to update internal modified field
# call save to update the internal modified field
self.persist.save()
def _sync_to_pathway(self):
def _sync_to_pathway(self) -> None:
logger.info("Updating Pathway with SPathway")
for snode in self.smiles_to_node.values():
if snode not in self.snode_persist_lookup:
n = Node.create(self.persist, snode.smiles, snode.depth)
if snode.app_domain_assessment is not None:
app_domain_assessment = snode.app_domain_assessment
assert n.id is not None, "Node has no id! Should have been saved already... aborting!"
node_data = n.simple_json()
node_data['image'] = f"{n.url}?image=svg"
app_domain_assessment['assessment']['node'] = node_data
n.kv['app_domain_assessment'] = app_domain_assessment
n.save()
self.snode_persist_lookup[snode] = n
for sedge in self.edges:
@ -1125,7 +1320,6 @@ class SPathway(object):
self.sedge_persist_lookup[sedge] = e
logger.info("Update done!")
pass
def to_json(self):
nodes = []

View File

@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand
from django.db import transaction
from epdb.logic import UserManager, GroupManager, PackageManager, SettingManager
from epdb.models import UserSettingPermission, MLRelativeReasoning, EnviFormer, Permission, User
from epdb.models import UserSettingPermission, MLRelativeReasoning, EnviFormer, Permission, User, ExternalDatabase
class Command(BaseCommand):
@ -58,7 +58,7 @@ class Command(BaseCommand):
return anon, admin, g, jebus
def import_package(self, data, owner):
return PackageManager.import_package(data, owner, keep_ids=True)
return PackageManager.import_package(data, owner, keep_ids=True, add_import_timestamp=False)
def create_default_setting(self, owner, packages):
s = SettingManager.create_setting(
@ -74,6 +74,76 @@ class Command(BaseCommand):
return s
def populate_common_external_databases(self):
"""
Helper function to populate common external databases.
This can be called from a Django management command.
"""
databases = [
{
'name': 'PubChem Compound',
'full_name': 'PubChem Compound Database',
'description': 'Chemical database of small organic molecules',
'base_url': 'https://pubchem.ncbi.nlm.nih.gov',
'url_pattern': 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'
},
{
'name': 'PubChem Substance',
'full_name': 'PubChem Substance Database',
'description': 'Database of chemical substances',
'base_url': 'https://pubchem.ncbi.nlm.nih.gov',
'url_pattern': 'https://pubchem.ncbi.nlm.nih.gov/substance/{id}'
},
{
'name': 'ChEBI',
'full_name': 'Chemical Entities of Biological Interest',
'description': 'Dictionary of molecular entities',
'base_url': 'https://www.ebi.ac.uk/chebi',
'url_pattern': 'https://www.ebi.ac.uk/chebi/searchId.do?chebiId=CHEBI:{id}'
},
{
'name': 'RHEA',
'full_name': 'RHEA Reaction Database',
'description': 'Comprehensive resource of biochemical reactions',
'base_url': 'https://www.rhea-db.org',
'url_pattern': 'https://www.rhea-db.org/rhea/{id}'
},
{
'name': 'CAS',
'full_name': 'Chemical Abstracts Service Registry',
'description': 'Registry of chemical substances',
'base_url': 'https://www.cas.org',
'url_pattern': None # CAS doesn't have a free public URL pattern
},
{
'name': 'KEGG Reaction',
'full_name': 'KEGG Reaction Database',
'description': 'Database of biochemical reactions',
'base_url': 'https://www.genome.jp',
'url_pattern': 'https://www.genome.jp/entry/reaction+{id}'
},
{
'name': 'MetaCyc',
'full_name': 'MetaCyc Metabolic Pathway Database',
'description': 'Database of metabolic pathways and enzymes',
'base_url': 'https://metacyc.org',
'url_pattern': None
},
{
'name': 'UniProt',
'full_name': 'MetaCyc Metabolic Pathway Database',
'description': 'UniProt is a freely accessible database of protein sequence and functional information',
'base_url': 'https://www.uniprot.org',
'url_pattern': 'https://www.uniprot.org/uniprotkb?query="{id}"'
}
]
for db_info in databases:
ExternalDatabase.objects.get_or_create(
name=db_info['name'],
defaults=db_info
)
@transaction.atomic
def handle(self, *args, **options):
# Create users
@ -117,17 +187,17 @@ class Command(BaseCommand):
# Create RR
ml_model = MLRelativeReasoning.create(
pack,
'ECC - BBD - T0.5',
'ML Relative Reasoning',
[mapping['EAWAG-BBD']],
[mapping['EAWAG-BBD']],
[],
0.5
package=pack,
rule_packages=[mapping['EAWAG-BBD']],
data_packages=[mapping['EAWAG-BBD']],
eval_packages=[],
threshold=0.5,
name='ECC - BBD - T0.5',
description='ML Relative Reasoning',
)
X, y = ml_model.build_dataset()
ml_model.build_model(X, y)
ml_model.build_dataset()
ml_model.build_model()
# ml_model.evaluate_model()
# If available create EnviFormerModel

View File

@ -0,0 +1,50 @@
from django.apps import apps
from django.core.management.base import BaseCommand
from django.db.models import F, Value
from django.db.models.functions import Replace
class Command(BaseCommand):
def add_arguments(self, parser):
parser.add_argument(
'--old',
type=str,
help='Old Host, most likely https://envipath.org/',
required=True,
)
parser.add_argument(
'--new',
type=str,
help='New Host, most likely http://localhost:8000/',
required=True,
)
def handle(self, *args, **options):
MODELS = [
'User',
'Group',
'Package',
'Compound',
'CompoundStructure',
'Pathway',
'Edge',
'Node',
'Reaction',
'SimpleAmbitRule',
'SimpleRDKitRule',
'ParallelRule',
'SequentialRule',
'Scenario',
'Setting',
'MLRelativeReasoning',
'EnviFormer',
'ApplicabilityDomain',
]
for model in MODELS:
obj_cls = apps.get_model("epdb", model)
print(f"Localizing urls for {model}")
obj_cls.objects.update(
url=Replace(F('url'), Value(options['old']), Value(options['new']))
)

View File

@ -1,7 +1,7 @@
from django.conf import settings
from django.shortcuts import redirect
from django.urls import reverse
from urllib.parse import quote
class LoginRequiredMiddleware:
def __init__(self, get_response):
@ -17,5 +17,8 @@ class LoginRequiredMiddleware:
if not request.user.is_authenticated:
path = request.path_info
if not any(path.startswith(url) for url in self.exempt_urls):
if request.method == 'GET':
if request.get_full_path() and request.get_full_path() != '/':
return redirect(f"{settings.LOGIN_URL}?next={quote(request.get_full_path())}")
return redirect(settings.LOGIN_URL)
return self.get_response(request)

View File

@ -0,0 +1,594 @@
# Generated by Django 5.2.1 on 2025-07-22 20:58
import datetime
import django.contrib.auth.models
import django.contrib.auth.validators
import django.contrib.postgres.fields
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0012_alter_user_first_name_max_length'),
('contenttypes', '0002_remove_content_type_name'),
]
operations = [
migrations.CreateModel(
name='Compound',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
],
),
migrations.CreateModel(
name='EPModel',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='Permission',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('permission', models.CharField(choices=[('read', 'Read'), ('write', 'Write'), ('all', 'All')], max_length=32)),
],
),
migrations.CreateModel(
name='License',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('link', models.URLField(verbose_name='link')),
('image_link', models.URLField(verbose_name='Image link')),
],
),
migrations.CreateModel(
name='Rule',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='User',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('email', models.EmailField(max_length=254, unique=True)),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'user',
'verbose_name_plural': 'users',
'abstract': False,
},
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
migrations.CreateModel(
name='APIToken',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('hashed_key', models.CharField(max_length=128, unique=True)),
('created', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField(blank=True, default=datetime.datetime(2025, 10, 20, 20, 58, 48, 351675, tzinfo=datetime.timezone.utc), null=True)),
('name', models.CharField(blank=True, help_text='Optional name for the token', max_length=100)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.CreateModel(
name='CompoundStructure',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('smiles', models.TextField(verbose_name='SMILES')),
('canonical_smiles', models.TextField(verbose_name='Canonical SMILES')),
('inchikey', models.TextField(max_length=27, verbose_name='InChIKey')),
('normalized_structure', models.BooleanField(default=False)),
('compound', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.compound')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='compound',
name='default_structure',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='compound_default_structure', to='epdb.compoundstructure', verbose_name='Default Structure'),
),
migrations.CreateModel(
name='Edge',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('polymorphic_ctype', models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
),
migrations.CreateModel(
name='EnviFormer',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='PluginModel',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='RuleBaseRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='Group',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(verbose_name='Group name')),
('public', models.BooleanField(default=False, verbose_name='Public Group')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('group_member', models.ManyToManyField(related_name='groups_in_group', to='epdb.group', verbose_name='Group member')),
('owner', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Group Owner')),
('user_member', models.ManyToManyField(related_name='users_in_group', to=settings.AUTH_USER_MODEL, verbose_name='User members')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='user',
name='default_group',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='default_group', to='epdb.group', verbose_name='Default Group'),
),
migrations.CreateModel(
name='Node',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('depth', models.IntegerField(verbose_name='Node depth')),
('default_node_label', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='default_node_structure', to='epdb.compoundstructure', verbose_name='Default Node Label')),
('node_labels', models.ManyToManyField(related_name='node_structures', to='epdb.compoundstructure', verbose_name='All Node Labels')),
('out_edges', models.ManyToManyField(to='epdb.edge', verbose_name='Outgoing Edges')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='edge',
name='end_nodes',
field=models.ManyToManyField(related_name='edge_products', to='epdb.node', verbose_name='End Nodes'),
),
migrations.AddField(
model_name='edge',
name='start_nodes',
field=models.ManyToManyField(related_name='edge_educts', to='epdb.node', verbose_name='Start Nodes'),
),
migrations.CreateModel(
name='Package',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('reviewed', models.BooleanField(default=False, verbose_name='Reviewstatus')),
('license', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.license', verbose_name='License')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='epmodel',
name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
),
migrations.AddField(
model_name='compound',
name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
),
migrations.AddField(
model_name='user',
name='default_package',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.package', verbose_name='Default Package'),
),
migrations.CreateModel(
name='SequentialRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.CreateModel(
name='SimpleRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.AddField(
model_name='rule',
name='package',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package'),
),
migrations.AddField(
model_name='rule',
name='polymorphic_ctype',
field=models.ForeignKey(editable=False, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='polymorphic_%(app_label)s.%(class)s_set+', to='contenttypes.contenttype'),
),
migrations.CreateModel(
name='Pathway',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='node',
name='pathway',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
),
migrations.AddField(
model_name='edge',
name='pathway',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.pathway', verbose_name='belongs to'),
),
migrations.CreateModel(
name='Reaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('aliases', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), default=list, size=None, verbose_name='Aliases')),
('multi_step', models.BooleanField(verbose_name='Multistep Reaction')),
('medline_references', django.contrib.postgres.fields.ArrayField(base_field=models.TextField(), null=True, size=None, verbose_name='Medline References')),
('educts', models.ManyToManyField(related_name='reaction_educts', to='epdb.compoundstructure', verbose_name='Educts')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
('products', models.ManyToManyField(related_name='reaction_products', to='epdb.compoundstructure', verbose_name='Products')),
('rules', models.ManyToManyField(related_name='reaction_rule', to='epdb.rule', verbose_name='Rule')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='edge',
name='edge_label',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.reaction', verbose_name='Edge label'),
),
migrations.CreateModel(
name='Scenario',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('scenario_date', models.CharField(default='No date', max_length=256)),
('scenario_type', models.CharField(default='Not specified', max_length=256)),
('additional_information', models.JSONField(verbose_name='Additional Information')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Package')),
('parent', models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.scenario')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='rule',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='reaction',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='pathway',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='node',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='edge',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='compoundstructure',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.AddField(
model_name='compound',
name='scenarios',
field=models.ManyToManyField(to='epdb.scenario', verbose_name='Attached Scenarios'),
),
migrations.CreateModel(
name='Setting',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('public', models.BooleanField(default=False)),
('global_default', models.BooleanField(default=False)),
('max_depth', models.IntegerField(default=5, verbose_name='Setting Max Depth')),
('max_nodes', models.IntegerField(default=30, verbose_name='Setting Max Number of Nodes')),
('model_threshold', models.FloatField(blank=True, default=0.25, null=True, verbose_name='Setting Model Threshold')),
('model', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.epmodel', verbose_name='Setting EPModel')),
('rule_packages', models.ManyToManyField(blank=True, related_name='setting_rule_packages', to='epdb.package', verbose_name='Setting Rule Packages')),
],
options={
'abstract': False,
},
),
migrations.AddField(
model_name='pathway',
name='setting',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Setting'),
),
migrations.AddField(
model_name='user',
name='default_setting',
field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.setting', verbose_name='The users default settings'),
),
migrations.CreateModel(
name='MLRelativeReasoning',
fields=[
('epmodel_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.epmodel')),
('threshold', models.FloatField(default=0.5)),
('model_status', models.CharField(choices=[('INITIAL', 'Initial'), ('INITIALIZING', 'Model is initializing.'), ('BUILDING', 'Model is building.'), ('BUILT_NOT_EVALUATED', 'Model is built and can be used for predictions, Model is not evaluated yet.'), ('EVALUATING', 'Model is evaluating'), ('FINISHED', 'Model has finished building and evaluation.'), ('ERROR', 'Model has failed.')], default='INITIAL')),
('eval_results', models.JSONField(blank=True, default=dict, null=True)),
('data_packages', models.ManyToManyField(related_name='data_packages', to='epdb.package', verbose_name='Data Packages')),
('eval_packages', models.ManyToManyField(related_name='eval_packages', to='epdb.package', verbose_name='Evaluation Packages')),
('rule_packages', models.ManyToManyField(related_name='rule_packages', to='epdb.package', verbose_name='Rule Packages')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.epmodel',),
),
migrations.CreateModel(
name='ApplicabilityDomain',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, unique=True, verbose_name='UUID of this object')),
('name', models.TextField(default='no name', verbose_name='Name')),
('description', models.TextField(default='no description', verbose_name='Descriptions')),
('kv', models.JSONField(blank=True, default=dict, null=True)),
('num_neighbours', models.FloatField(default=5)),
('reliability_threshold', models.FloatField(default=0.5)),
('local_compatibilty_threshold', models.FloatField(default=0.5)),
('model', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.mlrelativereasoning')),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='SimpleAmbitRule',
fields=[
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
('smirks', models.TextField(verbose_name='SMIRKS')),
('reactant_filter_smarts', models.TextField(null=True, verbose_name='Reactant Filter SMARTS')),
('product_filter_smarts', models.TextField(null=True, verbose_name='Product Filter SMARTS')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.simplerule',),
),
migrations.CreateModel(
name='SimpleRDKitRule',
fields=[
('simplerule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.simplerule')),
('reaction_smarts', models.TextField(verbose_name='SMIRKS')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.simplerule',),
),
migrations.CreateModel(
name='SequentialRuleOrdering',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order_index', models.IntegerField()),
('sequential_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.sequentialrule')),
('simple_rule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.simplerule')),
],
),
migrations.AddField(
model_name='sequentialrule',
name='simple_rules',
field=models.ManyToManyField(through='epdb.SequentialRuleOrdering', to='epdb.simplerule', verbose_name='Simple rules'),
),
migrations.CreateModel(
name='ParallelRule',
fields=[
('rule_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='epdb.rule')),
('simple_rules', models.ManyToManyField(to='epdb.simplerule', verbose_name='Simple rules')),
],
options={
'abstract': False,
'base_manager_name': 'objects',
},
bases=('epdb.rule',),
),
migrations.AlterUniqueTogether(
name='compound',
unique_together={('uuid', 'package')},
),
migrations.CreateModel(
name='GroupPackagePermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.group', verbose_name='Permission to')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
],
options={
'unique_together': {('package', 'group')},
},
bases=('epdb.permission',),
),
migrations.CreateModel(
name='UserPackagePermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('package', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.package', verbose_name='Permission on')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
],
options={
'unique_together': {('package', 'user')},
},
bases=('epdb.permission',),
),
migrations.CreateModel(
name='UserSettingPermission',
fields=[
('permission_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, to='epdb.permission')),
('uuid', models.UUIDField(default=uuid.uuid4, primary_key=True, serialize=False, verbose_name='UUID of this object')),
('setting', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.setting', verbose_name='Permission on')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL, verbose_name='Permission to')),
],
options={
'unique_together': {('setting', 'user')},
},
bases=('epdb.permission',),
),
]

View File

@ -0,0 +1,128 @@
# Generated by Django 5.2.1 on 2025-08-25 18:07
import django.db.models.deletion
import django.utils.timezone
import model_utils.fields
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('contenttypes', '0002_remove_content_type_name'),
('epdb', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ExternalDatabase',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('name', models.CharField(max_length=100, unique=True, verbose_name='Database Name')),
('full_name', models.CharField(blank=True, max_length=255, verbose_name='Full Database Name')),
('description', models.TextField(blank=True, verbose_name='Description')),
('base_url', models.URLField(blank=True, null=True, verbose_name='Base URL')),
('url_pattern', models.CharField(blank=True, help_text="URL pattern with {id} placeholder, e.g., 'https://pubchem.ncbi.nlm.nih.gov/compound/{id}'", max_length=500, verbose_name='URL Pattern')),
('is_active', models.BooleanField(default=True, verbose_name='Is Active')),
],
options={
'verbose_name': 'External Database',
'verbose_name_plural': 'External Databases',
'db_table': 'epdb_external_database',
'ordering': ['name'],
},
),
migrations.AlterModelOptions(
name='apitoken',
options={'ordering': ['-created'], 'verbose_name': 'API Token', 'verbose_name_plural': 'API Tokens'},
),
migrations.AlterModelOptions(
name='edge',
options={},
),
migrations.RemoveField(
model_name='edge',
name='polymorphic_ctype',
),
migrations.AddField(
model_name='apitoken',
name='is_active',
field=models.BooleanField(default=True, help_text='Whether this token is active'),
),
migrations.AddField(
model_name='apitoken',
name='modified',
field=model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified'),
),
migrations.AddField(
model_name='applicabilitydomain',
name='functional_groups',
field=models.JSONField(blank=True, default=dict, null=True),
),
migrations.AddField(
model_name='mlrelativereasoning',
name='app_domain',
field=models.ForeignKey(blank=True, default=None, null=True, on_delete=django.db.models.deletion.SET_NULL, to='epdb.applicabilitydomain'),
),
migrations.AlterField(
model_name='apitoken',
name='created',
field=model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created'),
),
migrations.AlterField(
model_name='apitoken',
name='expires_at',
field=models.DateTimeField(blank=True, help_text='Token expiration time (null for no expiration)', null=True),
),
migrations.AlterField(
model_name='apitoken',
name='hashed_key',
field=models.CharField(help_text='SHA-256 hash of the token key', max_length=128, unique=True),
),
migrations.AlterField(
model_name='apitoken',
name='name',
field=models.CharField(help_text='Descriptive name for this token', max_length=100),
),
migrations.AlterField(
model_name='apitoken',
name='user',
field=models.ForeignKey(help_text='User who owns this token', on_delete=django.db.models.deletion.CASCADE, related_name='api_tokens', to=settings.AUTH_USER_MODEL),
),
migrations.AlterField(
model_name='applicabilitydomain',
name='num_neighbours',
field=models.IntegerField(default=5),
),
migrations.AlterModelTable(
name='apitoken',
table='epdb_api_token',
),
migrations.CreateModel(
name='ExternalIdentifier',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', model_utils.fields.AutoCreatedField(default=django.utils.timezone.now, editable=False, verbose_name='created')),
('modified', model_utils.fields.AutoLastModifiedField(default=django.utils.timezone.now, editable=False, verbose_name='modified')),
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
('object_id', models.IntegerField()),
('identifier_value', models.CharField(max_length=255, verbose_name='Identifier Value')),
('url', models.URLField(blank=True, null=True, verbose_name='Direct URL')),
('is_primary', models.BooleanField(default=False, help_text='Mark this as the primary identifier for this database', verbose_name='Is Primary')),
('content_type', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='contenttypes.contenttype')),
('database', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='epdb.externaldatabase', verbose_name='External Database')),
],
options={
'verbose_name': 'External Identifier',
'verbose_name_plural': 'External Identifiers',
'db_table': 'epdb_external_identifier',
'indexes': [models.Index(fields=['content_type', 'object_id'], name='epdb_extern_content_b76813_idx'), models.Index(fields=['database', 'identifier_value'], name='epdb_extern_databas_486422_idx')],
'unique_together': {('content_type', 'object_id', 'database', 'identifier_value')},
},
),
]

View File

@ -0,0 +1,228 @@
# Generated by Django 5.2.1 on 2025-08-26 17:05
from django.db import migrations, models
def populate_url(apps, schema_editor):
MODELS = [
'User',
'Group',
'Package',
'Compound',
'CompoundStructure',
'Pathway',
'Edge',
'Node',
'Reaction',
'SimpleAmbitRule',
'SimpleRDKitRule',
'ParallelRule',
'SequentialRule',
'Scenario',
'Setting',
'MLRelativeReasoning',
'EnviFormer',
'ApplicabilityDomain',
]
for model in MODELS:
obj_cls = apps.get_model("epdb", model)
for obj in obj_cls.objects.all():
obj.url = assemble_url(obj)
if obj.url is None:
raise ValueError(f"Could not assemble url for {obj}")
obj.save()
def assemble_url(obj):
from django.conf import settings as s
match obj.__class__.__name__:
case 'User':
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
case 'Group':
return '{}/group/{}'.format(s.SERVER_URL, obj.uuid)
case 'Package':
return '{}/package/{}'.format(s.SERVER_URL, obj.uuid)
case 'Compound':
return '{}/compound/{}'.format(obj.package.url, obj.uuid)
case 'CompoundStructure':
return '{}/structure/{}'.format(obj.compound.url, obj.uuid)
case 'SimpleAmbitRule':
return '{}/simple-ambit-rule/{}'.format(obj.package.url, obj.uuid)
case 'SimpleRDKitRule':
return '{}/simple-rdkit-rule/{}'.format(obj.package.url, obj.uuid)
case 'ParallelRule':
return '{}/parallel-rule/{}'.format(obj.package.url, obj.uuid)
case 'SequentialRule':
return '{}/sequential-rule/{}'.format(obj.compound.url, obj.uuid)
case 'Reaction':
return '{}/reaction/{}'.format(obj.package.url, obj.uuid)
case 'Pathway':
return '{}/pathway/{}'.format(obj.package.url, obj.uuid)
case 'Node':
return '{}/node/{}'.format(obj.pathway.url, obj.uuid)
case 'Edge':
return '{}/edge/{}'.format(obj.pathway.url, obj.uuid)
case 'MLRelativeReasoning':
return '{}/model/{}'.format(obj.package.url, obj.uuid)
case 'EnviFormer':
return '{}/model/{}'.format(obj.package.url, obj.uuid)
case 'ApplicabilityDomain':
return '{}/model/{}/applicability-domain/{}'.format(obj.model.package.url, obj.model.uuid, obj.uuid)
case 'Scenario':
return '{}/scenario/{}'.format(obj.package.url, obj.uuid)
case 'Setting':
return '{}/setting/{}'.format(s.SERVER_URL, obj.uuid)
case _:
raise ValueError(f"Unknown model {obj.__class__.__name__}")
class Migration(migrations.Migration):
dependencies = [
('epdb', '0002_externaldatabase_alter_apitoken_options_and_more'),
]
operations = [
migrations.AddField(
model_name='applicabilitydomain',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='compound',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='compoundstructure',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='edge',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='epmodel',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='group',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='node',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='package',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='pathway',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='reaction',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='rule',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='scenario',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='setting',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.AddField(
model_name='user',
name='url',
field=models.TextField(null=True, unique=False, verbose_name='URL'),
),
migrations.RunPython(populate_url, reverse_code=migrations.RunPython.noop),
migrations.AlterField(
model_name='applicabilitydomain',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='compound',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='compoundstructure',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='edge',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='epmodel',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='group',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='node',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='package',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='pathway',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='reaction',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='rule',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='scenario',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='setting',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
migrations.AlterField(
model_name='user',
name='url',
field=models.TextField(null=True, unique=True, verbose_name='URL'),
),
]

File diff suppressed because it is too large Load Diff

20
epdb/signals.py Normal file
View File

@ -0,0 +1,20 @@
from django.db import transaction
from django.db.models.signals import pre_delete
from django.dispatch import receiver
from epdb.models import Node, Edge
@receiver(pre_delete, sender=Node)
@transaction.atomic
def delete_orphan_edges(sender, instance, **kwargs):
# check if the node that is about to be deleted is the only start node
for edge in Edge.objects.filter(start_nodes=instance):
if edge.start_nodes.count() == 1:
edge.delete()
# same for end_nodes
for edge in Edge.objects.filter(end_nodes=instance):
# check if the node that is about to be deleted is the only start node
if edge.end_nodes.count() == 1:
edge.delete()

View File

@ -31,8 +31,8 @@ def send_registration_mail(user_pk: int):
@shared_task(queue='model')
def build_model(model_pk: int):
mod = EPModel.objects.get(id=model_pk)
X, y = mod.build_dataset()
mod.build_model(X, y)
mod.build_dataset()
mod.build_model()
@shared_task(queue='model')
@ -58,7 +58,7 @@ def predict(pw_pk: int, pred_setting_pk: int, limit: Optional[int] = None, node_
spw.predict_step(from_depth=level)
level += 1
# break in case we are in incremental model
# break in case we are in incremental mode
if limit != -1:
if level >= limit:
break

View File

@ -76,5 +76,7 @@ urlpatterns = [
re_path(r'^indigo/dearomatize$', v.dearomatize, name='indigo_dearomatize'),
re_path(r'^indigo/layout$', v.layout, name='indigo_layout'),
re_path(r'^depict$', v.depict, name='depict')
re_path(r'^depict$', v.depict, name='depict'),
path("o/userinfo/", v.userinfo, name="oauth_userinfo"),
]

View File

@ -7,10 +7,13 @@ from django.contrib.auth import get_user_model
from django.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest
from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt
from envipy_additional_information import NAME_MAPPING
from oauth2_provider.decorators import protected_resource
from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager
from utilities.misc import HTMLGenerator
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager, EPDBURLParser
from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \
EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \
UserPackagePermission, Permission, License, User, Edge
@ -44,6 +47,7 @@ def login(request):
if request.method == 'GET':
context['title'] = 'enviPath'
context['next'] = request.GET.get('next', '')
return render(request, 'login.html', context)
elif request.method == 'POST':
@ -57,7 +61,7 @@ def login(request):
username = request.POST.get('username')
password = request.POST.get('password')
# Get email for username and check if account is active
# Get email for username and check if the account is active
try:
temp_user = get_user_model().objects.get(username=username)
@ -77,6 +81,10 @@ def login(request):
if user is not None:
login(request, user)
if next := request.POST.get('next'):
return redirect(next)
return redirect(s.SERVER_URL)
else:
context['message'] = "Login failed!"
@ -103,7 +111,10 @@ def login(request):
else:
context['message'] = "Account has been created! You'll receive a mail to activate your account shortly."
return render(request, 'login.html', context)
else:
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(['GET', 'POST'])
def logout(request):
if request.method == 'POST':
@ -136,7 +147,7 @@ def editable(request, user):
f"{s.SERVER_URL}/group", f"{s.SERVER_URL}/search"]:
return True
else:
print(f"Unknown url: {url}")
logger.debug(f"Unknown url: {url}")
return False
@ -144,6 +155,12 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
current_user = _anonymous_or_real(request)
can_edit = editable(request, current_user)
parser = EPDBURLParser(request.build_absolute_uri(request.path))
url_contains_package = False
if parser.contains_package_url() or parser.is_package_url():
url_contains_package = True
if for_user:
current_user = for_user
@ -154,11 +171,12 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
'server_url': s.SERVER_URL,
'user': current_user,
'can_edit': can_edit,
'url_contains_package': url_contains_package,
'readable_packages': PackageManager.get_all_readable_packages(current_user, include_reviewed=True),
'writeable_packages': PackageManager.get_all_writeable_packages(current_user),
'available_groups': GroupManager.get_groups(current_user),
'available_settings': SettingManager.get_all_settings(current_user),
'enabled_features': [],
'enabled_features': s.FLAGS,
'debug': s.DEBUG,
},
}
@ -196,6 +214,41 @@ def breadcrumbs(first_level_object=None, second_level_namespace=None, second_lev
return bread
def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
scens = []
for scenario_url in scenario_urls:
package = PackageManager.get_package_by_url(current_user, scenario_url)
scen = Scenario.objects.get(package=package, uuid=scenario_url.split('/')[-1])
scens.append(scen)
attach_object.set_scenarios(scens)
def copy_object(current_user, target_package: 'Package', source_object_url: str):
# Ensures that source is readable
source_package = PackageManager.get_package_by_url(current_user, source_object_url)
parser = EPDBURLParser(source_object_url)
# if the url won't contain a package or is a plain package
if not parser.contains_package_url():
raise ValueError(f"Object {source_object_url} can't be copied!")
# Gets the most specific object
source_object = parser.get_object()
if hasattr(source_object, 'copy'):
mapping = dict()
copy = source_object.copy(target_package, mapping)
if s.DEBUG:
for k, v in mapping.items():
logger.debug(f"Mapping {k.url} to {v.url}")
return copy
raise ValueError(f"Object {source_object} can't be copied!")
def index(request):
context = get_base_context(request)
context['title'] = 'enviPath - Home'
@ -424,8 +477,7 @@ def scenarios(request):
if request.GET.get('all'):
return JsonResponse({
"objects": [
{"name": pw.name, "url": pw.url, "reviewed": True}
for pw in reviewed_scenario_qs
{"name": s.name, "url": s.url, "reviewed": True} for s in reviewed_scenario_qs
]
})
@ -505,6 +557,7 @@ def search(request):
packages = PackageManager.get_reviewed_packages()
search_result = SearchManager.search(packages, searchterm, mode)
return JsonResponse(search_result, safe=False)
context = get_base_context(request)
@ -530,6 +583,7 @@ def search(request):
packages = PackageManager.get_reviewed_packages()
context['search_result'] = SearchManager.search(packages, searchterm, mode)
context['search_result']['searchterm'] = searchterm
return render(request, 'search.html', context)
@ -572,15 +626,21 @@ def package_models(request, package_uuid):
context['model_types'] = {
'ML Relative Reasoning': 'ml-relative-reasoning',
'Rule Based Relative Reasoning': 'rule-based-relative-reasoning',
'EnviFormer': 'enviformer',
}
if s.FLAGS.get('ENVIFORMER', False):
context['model_types']['EnviFormer'] = 'enviformer'
if s.FLAGS.get('PLUGINS', False):
for k, v in s.CLASSIFIER_PLUGINS.items():
context['model_types'][v.display()] = k
return render(request, 'collections/objects_list.html', context)
elif request.method == 'POST':
log_post_params(request)
name = request.POST.get('model-name')
description = request.POST.get('model-description')
@ -603,14 +663,25 @@ def package_models(request, package_uuid):
data_package_objs = [PackageManager.get_package_by_url(current_user, p) for p in data_packages]
eval_packages_objs = [PackageManager.get_package_by_url(current_user, p) for p in eval_packages]
# App Domain related parameters
build_ad = request.POST.get('build-app-domain', False) == 'on'
num_neighbors = request.POST.get('num-neighbors', 5)
reliability_threshold = request.POST.get('reliability-threshold', 0.5)
local_compatibility_threshold = request.POST.get('local-compatibility-threshold', 0.5)
mod = MLRelativeReasoning.create(
current_package,
name,
description,
rule_package_objs,
data_package_objs,
eval_packages_objs,
threshold
package=current_package,
name=name,
description=description,
rule_packages=rule_package_objs,
data_packages=data_package_objs,
eval_packages=eval_packages_objs,
threshold=threshold,
# fingerprinter=fingerprinter,
build_app_domain=build_ad,
app_domain_num_neighbours=num_neighbors,
app_domain_reliability_threshold=reliability_threshold,
app_domain_local_compatibility_threshold=local_compatibility_threshold,
)
from .tasks import build_model
@ -646,7 +717,7 @@ def package_model(request, package_uuid, model_uuid):
if len(pr) > 0:
products = []
for prod_set in pr.product_sets:
print(f"Checking {prod_set}")
logger.debug(f"Checking {prod_set}")
products.append(tuple([x for x in prod_set]))
res.append({
@ -657,6 +728,12 @@ def package_model(request, package_uuid, model_uuid):
return JsonResponse(res, safe=False)
elif request.GET.get('app-domain-assessment', False):
smiles = request.GET['smiles']
stand_smiles = FormatConverter.standardize(smiles)
app_domain_assessment = current_model.app_domain.assess(stand_smiles)[0]
return JsonResponse(app_domain_assessment, safe=False)
context = get_base_context(request)
context['title'] = f'enviPath - {current_package.name} - {current_model.name}'
@ -665,12 +742,13 @@ def package_model(request, package_uuid, model_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'model', current_model)
context['model'] = current_model
context['current_object'] = current_model
return render(request, 'objects/model.html', context)
elif request.method == 'POST':
if hidden := request.POST.get('hidden', None):
if hidden == 'delete-model':
if hidden == 'delete':
current_model.delete()
return redirect(current_package.url + '/model')
else:
@ -696,8 +774,6 @@ def package(request, package_uuid):
context['breadcrumbs'] = breadcrumbs(current_package)
context['package'] = current_package
# context['package_group'] = GroupPackagePermission.objects.filter(package=current_package,
# permission=GroupPackagePermission.ALL)
user_perms = UserPackagePermission.objects.filter(package=current_package)
users = get_user_model().objects.exclude(
@ -716,14 +792,24 @@ def package(request, package_uuid):
elif request.method == 'POST':
if s.DEBUG:
for k, v in request.POST.items():
logger.debug(f"{k}\t{v}")
log_post_params(request)
if hidden := request.POST.get('hidden', None):
if hidden == 'delete-package':
if hidden == 'delete':
logger.debug(current_package.delete())
return redirect(s.SERVER_URL + '/package')
elif hidden == 'publish-package':
for g in Group.objects.filter(public=True):
PackageManager.update_permissions(current_user, current_package, g, Permission.READ[0])
return redirect(current_package.url)
elif hidden == 'copy':
object_to_copy = request.POST.get('object_to_copy')
if not object_to_copy:
return error(request, 'Invalid target package.', 'Please select a target package.')
copied_object = copy_object(current_user, current_package, object_to_copy)
return JsonResponse({'success': copied_object.url})
else:
return HttpResponseBadRequest()
@ -855,17 +941,24 @@ def package_compound(request, package_uuid, compound_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'compound', current_compound)
context['compound'] = current_compound
context['current_object'] = current_compound
return render(request, 'objects/compound.html', context)
elif request.method == 'POST':
if hidden := request.POST.get('hidden', None):
if hidden == 'delete-compound':
if hidden == 'delete':
current_compound.delete()
return redirect(current_package.url + '/compound')
else:
return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios:
set_scenarios(current_user, current_compound, selected_scenarios)
return redirect(current_compound.url)
new_compound_name = request.POST.get('compound-name')
new_compound_description = request.POST.get('compound-description')
@ -897,6 +990,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
context['meta']['current_package'] = current_package
context['object_type'] = 'structure'
context['breadcrumbs'] = breadcrumbs(current_package, 'compound', current_compound, 'structure')
reviewed_compound_structure_qs = CompoundStructure.objects.none()
unreviewed_compound_structure_qs = CompoundStructure.objects.none()
@ -936,12 +1030,45 @@ def package_compound_structure(request, package_uuid, compound_uuid, structure_u
context['title'] = f'enviPath - {current_package.name} - {current_compound.name} - {current_structure.name}'
context['meta']['current_package'] = current_package
context['object_type'] = 'compound'
context['object_type'] = 'structure'
context['compound_structure'] = current_structure
context['current_object'] = current_structure
context['breadcrumbs'] = breadcrumbs(current_package, 'compound', current_compound, 'structure', current_structure)
return render(request, 'objects/compound_structure.html', context)
elif request.method == 'POST':
if hidden := request.POST.get('hidden', None):
if hidden == 'delete':
# Check if we have to delete the compound as no structure is left
if len(current_structure.compound.structures.all()) == 1:
# This will delete the structure as well
current_compound.delete()
return redirect(current_package.url + '/compound')
else:
if current_structure.normalized_structure:
current_compound.delete()
return redirect(current_package.url + '/compound')
else:
if current_compound.default_structure == current_structure:
current_structure.delete()
current_compound.default_structure = current_compound.structures.all().first()
return redirect(current_compound.url + '/structure')
else:
current_structure.delete()
return redirect(current_compound.url + '/structure')
else:
return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios:
set_scenarios(current_user, current_structure, selected_scenarios)
return redirect(current_structure.url)
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(['GET', ])
@ -1022,6 +1149,24 @@ def package_rule(request, package_uuid, rule_uuid):
if request.method == 'GET':
context = get_base_context(request)
if smiles := request.GET.get('smiles', False):
stand_smiles = FormatConverter.standardize(smiles)
res = current_rule.apply(stand_smiles)
if len(res) > 1:
logger.info(f"Rule {current_rule.uuid} returned multiple product sets on {smiles}, picking the first one.")
smirks = f"{stand_smiles}>>{'.'.join(sorted(res[0]))}"
# Usually the functional groups are a mapping of fg -> count
# As we are doing it on the fly here fake a high count to ensure that its properly highlighted
educt_functional_groups = {x: 1000 for x in current_rule.reactants_smarts}
product_functional_groups = {x: 1000 for x in current_rule.products_smarts}
return HttpResponse(
IndigoUtils.smirks_to_svg(smirks, False, 0, 0,
educt_functional_groups=educt_functional_groups,
product_functional_groups=product_functional_groups),
content_type='image/svg+xml')
context['title'] = f'enviPath - {current_package.name} - {current_rule.name}'
context['meta']['current_package'] = current_package
@ -1029,6 +1174,8 @@ def package_rule(request, package_uuid, rule_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'rule', current_rule)
context['rule'] = current_rule
context['current_object'] = current_rule
if isinstance(current_rule, SimpleAmbitRule):
return render(request, 'objects/simple_rule.html', context)
else: # isinstance(current_rule, ParallelRule) or isinstance(current_rule, SequentialRule):
@ -1036,12 +1183,18 @@ def package_rule(request, package_uuid, rule_uuid):
elif request.method == 'POST':
if hidden := request.POST.get('hidden', None):
if hidden == 'delete-rule':
if hidden == 'delete':
current_rule.delete()
return redirect(current_package.url + '/rule')
else:
return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios:
set_scenarios(current_user, current_rule, selected_scenarios)
return redirect(current_rule.url)
rule_name = request.POST.get('rule-name', '').strip()
rule_description = request.POST.get('rule-description', '').strip()
@ -1127,17 +1280,24 @@ def package_reaction(request, package_uuid, reaction_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'reaction', current_reaction)
context['reaction'] = current_reaction
context['current_object'] = current_reaction
return render(request, 'objects/reaction.html', context)
elif request.method == 'POST':
if hidden := request.POST.get('hidden', None):
if hidden == 'delete-reaction':
if hidden == 'delete':
current_reaction.delete()
return redirect(current_package.url + '/reaction')
else:
return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios:
set_scenarios(current_user, current_reaction, selected_scenarios)
return redirect(current_reaction.url)
new_reaction_name = request.POST.get('reaction-name')
new_reaction_description = request.POST.get('reaction-description')
@ -1195,8 +1355,8 @@ def package_pathways(request, package_uuid):
log_post_params(request)
name = request.POST.get('name', 'Pathway ' + str(Pathway.objects.filter(package=current_package).count()))
description = request.POST.get('description', s.DEFAULT_VALUES['description'])
name = request.POST.get('name')
description = request.POST.get('description')
pw_mode = request.POST.get('predict', 'predict')
smiles = request.POST.get('smiles')
@ -1217,7 +1377,14 @@ def package_pathways(request, package_uuid):
return error(request, "Pathway prediction failed!",
f'Pathway prediction failed as received mode "{pw_mode}" is none of {modes}')
prediction_setting = request.POST.get('prediction-setting', None)
if prediction_setting:
prediction_setting = SettingManager.get_setting_by_url(current_user, prediction_setting)
else:
prediction_setting = current_user.prediction_settings()
pw = Pathway.create(current_package, stand_smiles, name=name, description=description)
# set mode
pw.kv.update({'mode': pw_mode})
pw.save()
@ -1230,12 +1397,11 @@ def package_pathways(request, package_uuid):
if pw_mode == 'incremental':
limit = 1
pred_setting = current_user.prediction_settings()
pw.setting = pred_setting
pw.setting = prediction_setting
pw.save()
from .tasks import predict
predict.delay(pw.pk, pred_setting.pk, limit=limit)
predict.delay(pw.pk, prediction_setting.pk, limit=limit)
return redirect(pw.url)
@ -1254,6 +1420,14 @@ def package_pathway(request, package_uuid, pathway_uuid):
if request.GET.get("last_modified", False):
return JsonResponse({'modified': current_pathway.modified.strftime('%Y-%m-%d %H:%M:%S')})
if request.GET.get("download", False) == "true":
filename = f"{current_pathway.name.replace(' ', '_')}_{current_pathway.uuid}.csv"
csv_pw = current_pathway.to_csv()
response = HttpResponse(csv_pw, content_type='text/csv')
response['Content-Disposition'] = f'attachment; filename="{filename}"'
return response
context = get_base_context(request)
context['title'] = f'enviPath - {current_package.name} - {current_pathway.name}'
@ -1262,6 +1436,7 @@ def package_pathway(request, package_uuid, pathway_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'pathway', current_pathway)
context['pathway'] = current_pathway
context['current_object'] = current_pathway
context['breadcrumbs'] = [
{'Home': s.SERVER_URL},
@ -1276,12 +1451,18 @@ def package_pathway(request, package_uuid, pathway_uuid):
elif request.method == 'POST':
if hidden := request.POST.get('hidden', None):
if hidden == 'delete-pathway':
if hidden == 'delete':
current_pathway.delete()
return redirect(current_package.url + '/pathway')
else:
return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios:
set_scenarios(current_user, current_pathway, selected_scenarios)
return redirect(current_pathway.url)
pathway_name = request.POST.get('pathway-name')
pathway_description = request.POST.get('pathway-description')
@ -1402,19 +1583,33 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
]
context['node'] = current_node
context['current_object'] = current_node
context['app_domain_assessment_data'] = json.dumps(current_node.get_app_domain_assessment_data())
return render(request, 'objects/node.html', context)
elif request.method == 'POST':
if s.DEBUG:
for k, v in request.POST.items():
print(k, v)
log_post_params(request)
if hidden := request.POST.get('hidden', None):
if hidden == 'delete-node':
current_node.delete()
return redirect(current_pathway.url)
if hidden == 'delete':
# pre_delete signal will take care of edge deletion
current_node.delete()
return redirect(current_pathway.url)
else:
return HttpResponseBadRequest()
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios:
set_scenarios(current_user, current_node, selected_scenarios)
return redirect(current_node.url)
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(['GET', 'POST'])
@ -1463,6 +1658,8 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
elif request.method == 'POST':
log_post_params(request)
edge_name = request.POST.get('edge-name')
edge_description = request.POST.get('edge-description')
edge_substrates = request.POST.getlist('edge-substrates')
@ -1499,22 +1696,30 @@ def package_pathway_edge(request, package_uuid, pathway_uuid, edge_uuid):
'title'] = f'enviPath - {current_package.name} - {current_pathway.name} - {current_edge.edge_label.name}'
context['meta']['current_package'] = current_package
context['object_type'] = 'reaction'
context['object_type'] = 'edge'
context['breadcrumbs'] = breadcrumbs(current_package, 'pathway', current_pathway, 'edge', current_edge)
context['edge'] = current_edge
context['current_object'] = current_edge
return render(request, 'objects/edge.html', context)
elif request.method == 'POST':
if s.DEBUG:
for k, v in request.POST.items():
print(k, v)
log_post_params(request)
if hidden := request.POST.get('hidden', None):
if hidden == 'delete-edge':
if hidden == 'delete':
current_edge.delete()
return redirect(current_pathway.url)
selected_scenarios = request.POST.getlist('selected-scenarios')
if selected_scenarios:
set_scenarios(current_user, current_edge, selected_scenarios)
return redirect(current_edge.url)
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(['GET', 'POST'])
@ -1525,12 +1730,18 @@ def package_scenarios(request, package_uuid):
current_package = PackageManager.get_package_by_id(current_user, package_uuid)
if request.method == 'GET':
if 'application/json' in request.META.get('HTTP_ACCEPT') and not request.GET.get('all', False):
scens = Scenario.objects.filter(package=current_package).order_by('name')
res = [{'name': s.name, 'url': s.url, 'uuid': s.uuid} for s in scens]
return JsonResponse(res, safe=False)
context = get_base_context(request)
context['title'] = f'enviPath - {current_package.name} - Scenarios'
context['meta']['current_package'] = current_package
context['object_type'] = 'scenario'
context['breadcrumbs'] = breadcrumbs(current_package, 'pathway')
context['breadcrumbs'] = breadcrumbs(current_package, 'scenario')
reviewed_scenario_qs = Scenario.objects.none()
unreviewed_scenario_qs = Scenario.objects.none()
@ -1551,8 +1762,57 @@ def package_scenarios(request, package_uuid):
context['reviewed_objects'] = reviewed_scenario_qs
context['unreviewed_objects'] = unreviewed_scenario_qs
return render(request, 'collections/objects_list.html', context)
from envipy_additional_information import SLUDGE_ADDITIONAL_INFORMATION, SOIL_ADDITIONAL_INFORMATION, \
SEDIMENT_ADDITIONAL_INFORMATION
context['scenario_types'] = {
'Soil Data': {
'name': 'soil',
'widgets': [HTMLGenerator.generate_html(ai, prefix=f'soil_{0}') for ai in
[x for s in SOIL_ADDITIONAL_INFORMATION.values() for x in s]]
},
'Sludge Data': {
'name': 'sludge',
'widgets': [HTMLGenerator.generate_html(ai, prefix=f'sludge_{0}') for ai in
[x for s in SLUDGE_ADDITIONAL_INFORMATION.values() for x in s]]
},
'Water-Sediment System Data': {
'name': 'sediment',
'widgets': [HTMLGenerator.generate_html(ai, prefix=f'sediment_{0}') for ai in
[x for s in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in s]]
}
}
context['sludge_additional_information'] = SLUDGE_ADDITIONAL_INFORMATION
context['soil_additional_information'] = SOIL_ADDITIONAL_INFORMATION
context['sediment_additional_information'] = SEDIMENT_ADDITIONAL_INFORMATION
return render(request, 'collections/objects_list.html', context)
elif request.method == 'POST':
log_post_params(request)
scenario_name = request.POST.get('scenario-name')
scenario_description = request.POST.get('scenario-description')
scenario_date_year = request.POST.get('scenario-date-year')
scenario_date_month = request.POST.get('scenario-date-month')
scenario_date_day = request.POST.get('scenario-date-day')
scenario_date = scenario_date_year
if scenario_date_month is not None and scenario_date_month.strip() != '':
scenario_date += f'-{int(scenario_date_month):02d}'
if scenario_date_day is not None and scenario_date_day.strip() != '':
scenario_date += f'-{int(scenario_date_day):02d}'
scenario_type = request.POST.get('scenario-type')
additional_information = HTMLGenerator.build_models(request.POST.dict())
additional_information = [x for s in additional_information.values() for x in s]
s = Scenario.create(current_package, name=scenario_name, description=scenario_description,
scenario_date=scenario_date, scenario_type=scenario_type,
additional_information=additional_information)
return redirect(s.url)
else:
return HttpResponseNotAllowed(['GET', ])
@ -1573,10 +1833,63 @@ def package_scenario(request, package_uuid, scenario_uuid):
context['scenario'] = current_scenario
available_add_infs = []
for add_inf in NAME_MAPPING.values():
available_add_infs.append({
'display_name': add_inf.property_name(None),
'name': add_inf.__name__,
'widget': HTMLGenerator.generate_html(add_inf, prefix=f'{0}')
})
context['available_additional_information'] = available_add_infs
context['update_widgets'] = [HTMLGenerator.generate_html(ai, prefix=f'{i}') for i, ai in enumerate(current_scenario.get_additional_information())]
return render(request, 'objects/scenario.html', context)
elif request.method == 'POST':
log_post_params(request)
if hidden := request.POST.get('hidden', None):
if hidden == 'delete':
current_scenario.delete()
return redirect(current_package.url + '/scenario')
elif hidden == 'delete-additional-information':
uuid = request.POST.get('uuid')
current_scenario.remove_additional_information(uuid)
return redirect(current_scenario.url)
elif hidden == 'delete-all-additional-information':
current_scenario.additional_information = dict()
current_scenario.save()
return redirect(current_scenario.url)
elif hidden == 'set-additional-information':
ais = HTMLGenerator.build_models(request.POST.dict())
if s.DEBUG:
logger.info(ais)
current_scenario.set_additional_information(ais)
return redirect(current_scenario.url)
elif hidden == 'add-additional-information':
ais = HTMLGenerator.build_models(request.POST.dict())
if len(ais.keys()) != 1:
raise ValueError('Only one additional information field can be added at a time.')
ai = list(ais.values())[0][0]
if s.DEBUG:
logger.info(ais)
current_scenario.add_additional_information(ai)
return redirect(current_scenario.url)
else:
return HttpResponseNotAllowed(['GET', ])
return HttpResponseBadRequest()
else:
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(['GET', 'POST'])
##############
@ -1587,7 +1900,6 @@ def users(request):
context = get_base_context(request)
context['title'] = f'enviPath - Users'
context['meta']['current_package'] = context['meta']['user'].default_package
context['object_type'] = 'user'
context['breadcrumbs'] = [
{'Home': s.SERVER_URL},
@ -1611,27 +1923,27 @@ def user(request, user_uuid):
if str(current_user.uuid) != user_uuid and not current_user.is_superuser:
return HttpResponseBadRequest()
user = UserManager.get_user_by_id(current_user, user_uuid)
requested_user = UserManager.get_user_by_id(current_user, user_uuid)
context = get_base_context(request)
context = get_base_context(request, for_user=requested_user)
context['title'] = f'enviPath - User'
context['object_type'] = 'user'
context['breadcrumbs'] = [
{'Home': s.SERVER_URL},
{'User': s.SERVER_URL + '/user'},
{current_user.username: current_user.url}
{current_user.username: requested_user.url}
]
context['user'] = user
context['user'] = requested_user
model_qs = EPModel.objects.none()
for p in PackageManager.get_all_readable_packages(current_user, include_reviewed=True):
for p in PackageManager.get_all_readable_packages(requested_user, include_reviewed=True):
model_qs |= p.models
context['models'] = model_qs
context['tokens'] = APIToken.objects.filter(user=current_user)
context['tokens'] = APIToken.objects.filter(user=requested_user)
return render(request, 'objects/user.html', context)
@ -1700,8 +2012,6 @@ def user(request, user_uuid):
}
}
print(setting)
return HttpResponseBadRequest()
else:
@ -1715,7 +2025,6 @@ def groups(request):
context = get_base_context(request)
context['title'] = f'enviPath - Groups'
context['meta']['current_package'] = context['meta']['user'].default_package
context['object_type'] = 'group'
context['breadcrumbs'] = [
{'Home': s.SERVER_URL},
@ -1764,12 +2073,10 @@ def group(request, group_uuid):
elif request.method == 'POST':
if s.DEBUG:
for k, v in request.POST.items():
print(k, v)
log_post_params(request)
if hidden := request.POST.get('hidden', None):
if hidden == 'delete-group':
if hidden == 'delete':
current_group.delete()
return redirect(s.SERVER_URL + '/group')
else:
@ -1912,3 +2219,16 @@ def depict(request):
return HttpResponse(IndigoUtils.smirks_to_svg(smirks, query_smirks), content_type='image/svg+xml')
else:
return HttpResponseBadRequest()
@protected_resource()
def userinfo(request):
user = request.resource_owner
res = {
"sub": str(user.uuid),
"email": user.email,
"username": user.username,
"name": user.get_full_name() or user.username,
"email_verified": user.is_active,
}
return JsonResponse(res)

File diff suppressed because one or more lines are too long

View File

@ -38,10 +38,8 @@ def migration(request):
res = True
for comp, ambit_prod in zip(bt_rule['compounds'], bt_rule['products']):
# if comp['smiles'] != 'CC1=C(C(=C(C=N1)CO)C=O)O':
# continue
products = FormatConverter.apply(comp['smiles'], smirks, preprocess_smiles=True, bracketize=False)
products = FormatConverter.apply(comp['smiles'], smirks)
all_rdkit_prods = []
for ps in products:
@ -130,7 +128,7 @@ def migration_detail(request, package_uuid, rule_uuid):
# if comp['smiles'] != 'CC1=C(C(=C(C=N1)CO)C=O)O':
# continue
products = FormatConverter.apply(comp['smiles'], smirks, preprocess_smiles=True, bracketize=False)
products = FormatConverter.apply(comp['smiles'], smirks)
all_rdkit_prods = []
for ps in products:

View File

@ -10,6 +10,7 @@ dependencies = [
"django-extensions>=4.1",
"django-model-utils>=5.0.0",
"django-ninja>=1.4.1",
"django-oauth-toolkit>=3.0.1",
"django-polymorphic>=4.1.0",
"enviformer",
"envipy-additional-information",
@ -29,4 +30,4 @@ dependencies = [
[tool.uv.sources]
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.0" }
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git" }
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.4"}

View File

@ -638,3 +638,150 @@ function fillPRCurve(modelUri, onclick){
});
}
function handleAssessmentResponse(depict_url, data) {
var inside_app_domain = "<a class='list-group-item'>This compound is " + (data["assessment"]["inside_app_domain"] ? "inside" : "outside") + " the Applicability Domain derived from the chemical (PCA) space constructed using the training data." + "</a>";
var functionalGroupsImgSrc = null;
var reactivityCentersImgSrc = null;
if (data['assessment']['node'] !== undefined) {
functionalGroupsImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "'>";
reactivityCentersImgSrc = "<img width='400' src='" + data['assessment']['node']['image'] + "'>"
} else {
functionalGroupsImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">";
reactivityCentersImgSrc = "<img width='400' src=\"" + depict_url + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "\">"
}
tpl = `<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="app-domain-assessment-functional-groups-link" data-toggle="collapse" data-parent="#app-domain-assessment" href="#app-domain-assessment-functional-groups">Functional Groups Covered by Model</a>
</h4>
</div>
<div id="app-domain-assessment-functional-groups" class="panel-collapse collapse">
<div class="panel-body list-group-item">
${inside_app_domain}
<p></p>
<div id="image-div" align="center">
${functionalGroupsImgSrc}
</div>
</div>
</div>
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="app-domain-assessment-reactivity-centers-link" data-toggle="collapse" data-parent="#app-domain-assessment" href="#app-domain-assessment-reactivity-centers">Reactivity Centers</a>
</h4>
</div>
<div id="app-domain-assessment-reactivity-centers" class="panel-collapse collapse">
<div class="panel-body list-group-item">
<div id="image-div" align="center">
${reactivityCentersImgSrc}
</div>
</div>
</div>`
var transformations = '';
for (t in data['assessment']['transformations']) {
transObj = data['assessment']['transformations'][t];
var neighbors = '';
for (n in transObj['neighbors']) {
neighObj = transObj['neighbors'][n];
var neighImg = "<img width='100%' src='" + transObj['rule']['url'] + "?smiles=" + encodeURIComponent(neighObj['smiles']) + "'>";
var objLink = `<a class='list-group-item' href="${neighObj['url']}">${neighObj['name']}</a>`
var neighPredProb = "<a class='list-group-item'>Predicted probability: " + neighObj['probability'].toFixed(2) + "</a>";
var pwLinks = '';
for (pw in neighObj['related_pathways']) {
var pwObj = neighObj['related_pathways'][pw];
pwLinks += "<a class='list-group-item' href=" + pwObj['url'] + ">" + pwObj['name'] + "</a>";
}
var expPathways = `
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="transformation-${t}-neighbor-${n}-exp-pathway-link" data-toggle="collapse" data-parent="#transformation-${t}-neighbor-${n}" href="#transformation-${t}-neighbor-${n}-exp-pathway">Experimental Pathways</a>
</h4>
</div>
<div id="transformation-${t}-neighbor-${n}-exp-pathway" class="panel-collapse collapse">
<div class="panel-body list-group-item">
${pwLinks}
</div>
</div>
`
if (pwLinks === '') {
expPathways = ''
}
neighbors += `
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="transformation-${t}-neighbor-${n}-link" data-toggle="collapse" data-parent="#transformation-${t}" href="#transformation-${t}-neighbor-${n}">Analog Transformation on ${neighObj['name']}</a>
</h4>
</div>
<div id="transformation-${t}-neighbor-${n}" class="panel-collapse collapse">
<div class="panel-body list-group-item">
${objLink}
${neighPredProb}
${expPathways}
<p></p>
<div id="image-div" align="center">
${neighImg}
</div>
</div>
</div>
`
}
var panelName = null;
var objLink = null;
if (transObj['is_predicted']) {
panelName = `Predicted Transformation by ${transObj['rule']['name']}`;
for (e in transObj['edges']) {
objLink = `<a class='list-group-item' href="${transObj['edges'][e]['url']}">${transObj['edges'][e]['name']}</a>`
break;
}
} else {
panelName = `Potential Transformation by applying ${transObj['rule']['name']}`;
objLink = `<a class='list-group-item' href="${transObj['rule']['url']}">${transObj['rule']['name']}</a>`
}
var predProb = "<a class='list-group-item'>Predicted probability: " + transObj['probability'].toFixed(2) + "</a>";
var timesTriggered = "<a class='list-group-item'>This rule has triggered " + transObj['times_triggered'] + " times in the training set</a>";
var reliability = "<a class='list-group-item'>Reliability: " + transObj['reliability'].toFixed(2) + " (" + (transObj['reliability'] > data['ad_params']['reliability_threshold'] ? "&gt" : "&lt") + " Reliability Threshold of " + data['ad_params']['reliability_threshold'] + ") </a>";
var localCompatibility = "<a class='list-group-item'>Local Compatibility: " + transObj['local_compatibility'].toFixed(2) + " (" + (transObj['local_compatibility'] > data['ad_params']['local_compatibilty_threshold'] ? "&gt" : "&lt") + " Local Compatibility Threshold of " + data['ad_params']['local_compatibilty_threshold'] + ")</a>";
var transImg = "<img width='100%' src='" + transObj['rule']['url'] + "?smiles=" + encodeURIComponent(data['assessment']['smiles']) + "'>";
var transformation = `
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="transformation-${t}-link" data-toggle="collapse" data-parent="#transformation-${t}" href="#transformation-${t}">${panelName}</a>
</h4>
</div>
<div id="transformation-${t}" class="panel-collapse collapse">
<div class="panel-body list-group-item">
${objLink}
${predProb}
${timesTriggered}
${reliability}
${localCompatibility}
<p></p>
<div id="image-div" align="center">
${transImg}
</div>
<p></p>
${neighbors}
</div>
</div>
`
transformations += transformation;
}
res = tpl + transformations;
$("#appDomainAssessmentResultTable").append(res);
}

View File

@ -1,5 +1,4 @@
console.log("loaded")
console.log("loaded pw.js")
function predictFromNode(url) {
$.post("", {node: url})
@ -28,61 +27,164 @@ function draw(pathway, elem) {
const horizontalSpacing = 75; // horizontal space between nodes
const depthMap = new Map();
nodes.forEach(node => {
// Sort nodes by depth first to minimize crossings
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
sortedNodes.forEach(node => {
if (!depthMap.has(node.depth)) {
depthMap.set(node.depth, 0);
}
const nodesInLevel = nodes.filter(n => n.depth === node.depth).length;
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
node.fy = node.depth * levelSpacing + 50;
// For pseudo nodes, try to position them to minimize crossings
if (node.pseudo) {
const parentLinks = links.filter(l => l.target.id === node.id);
const childLinks = links.filter(l => l.source.id === node.id);
if (parentLinks.length > 0 && childLinks.length > 0) {
const parentX = parentLinks[0].source.x || (width / 2);
const childrenX = childLinks.map(l => l.target.x || (width / 2));
const avgChildX = childrenX.reduce((sum, x) => sum + x, 0) / childrenX.length;
// Position pseudo node between parent and average child position
node.fx = (parentX + avgChildX) / 2;
} else {
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
}
} else {
node.fx = width / 2 + depthMap.get(node.depth) * horizontalSpacing - ((nodesInLevel - 1) * horizontalSpacing) / 2;
}
node.fy = node.depth * levelSpacing + 50;
depthMap.set(node.depth, depthMap.get(node.depth) + 1);
});
}
// Funktion für das Update der Positionen
// Function to update pseudo node positions based on connected nodes
function updatePseudoNodePositions() {
nodes.forEach(node => {
if (node.pseudo && !node.isDragging) { // Don't auto-update if being dragged
const parentLinks = links.filter(l => l.target.id === node.id);
const childLinks = links.filter(l => l.source.id === node.id);
if (parentLinks.length > 0 && childLinks.length > 0) {
const parent = parentLinks[0].source;
const children = childLinks.map(l => l.target);
// Calculate optimal position to minimize crossing
const parentX = parent.x;
const parentY = parent.y;
const childrenX = children.map(c => c.x);
const childrenY = children.map(c => c.y);
const avgChildX = d3.mean(childrenX);
const avgChildY = d3.mean(childrenY);
// Position pseudo node between parent and average child position
node.fx = (parentX + avgChildX) / 2;
node.fy = (parentY + avgChildY) / 2; // Allow vertical movement too
}
}
});
}
// Enhanced ticked function
function ticked() {
// Update pseudo node positions first
updatePseudoNodePositions();
link.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("transform", d => `translate(${d.x},${d.y})`);
nodes.forEach(n => {
if (n.pseudo) {
// Alle Kinder dieses Pseudonodes finden
const childLinks = links.filter(l => l.source.id === n.id);
const childNodes = childLinks.map(l => l.target);
if (childNodes.length > 0) {
// Durchschnitt der Kinderpositionen berechnen
const avgX = d3.mean(childNodes, d => d.x);
const avgY = d3.mean(childNodes, d => d.y);
n.fx = avgX;
// keep level as is
n.fy = n.y;
}
}
});
//simulation.alpha(0.3).restart();
}
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x; // Setzt die Fixierung auf die aktuelle Position
d.fx = d.x;
d.fy = d.y;
// Mark if this node is being dragged
d.isDragging = true;
// If dragging a non-pseudo node, mark connected pseudo nodes for update
if (!d.pseudo) {
markConnectedPseudoNodes(d);
}
}
function dragged(event, d) {
d.fx = event.x; // Position direkt an Maus anpassen
d.fx = event.x;
d.fy = event.y;
// Update connected pseudo nodes in real-time
if (!d.pseudo) {
updateConnectedPseudoNodes(d);
}
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
// Knoten bleibt an der neuen Position und wird nicht zurückgezogen
// Mark that dragging has ended
d.isDragging = false;
// Final update of connected pseudo nodes
if (!d.pseudo) {
updateConnectedPseudoNodes(d);
}
}
// Helper function to mark connected pseudo nodes
function markConnectedPseudoNodes(draggedNode) {
// Find pseudo nodes connected to this node
const connectedPseudos = new Set();
// Check as parent of pseudo nodes
links.filter(l => l.source.id === draggedNode.id && l.target.pseudo)
.forEach(l => connectedPseudos.add(l.target));
// Check as child of pseudo nodes
links.filter(l => l.target.id === draggedNode.id && l.source.pseudo)
.forEach(l => connectedPseudos.add(l.source));
return connectedPseudos;
}
// Helper function to update connected pseudo nodes
function updateConnectedPseudoNodes(draggedNode) {
const connectedPseudos = markConnectedPseudoNodes(draggedNode);
connectedPseudos.forEach(pseudoNode => {
if (!pseudoNode.isDragging) { // Don't update if pseudo node is being dragged
const parentLinks = links.filter(l => l.target.id === pseudoNode.id);
const childLinks = links.filter(l => l.source.id === pseudoNode.id);
if (parentLinks.length > 0 && childLinks.length > 0) {
const parent = parentLinks[0].source;
const children = childLinks.map(l => l.target);
const parentX = parent.fx || parent.x;
const parentY = parent.fy || parent.y;
const childrenX = children.map(c => c.fx || c.x);
const childrenY = children.map(c => c.fy || c.y);
const avgChildX = d3.mean(childrenX);
const avgChildY = d3.mean(childrenY);
// Update pseudo node position - allow both X and Y movement
pseudoNode.fx = (parentX + avgChildX) / 2;
pseudoNode.fy = (parentY + avgChildY) / 2;
}
}
});
// Restart simulation with lower alpha to smooth the transition
simulation.alpha(0.1).restart();
}
// t -> ref to "this" from d3
function nodeClick(event, node, t) {
@ -140,10 +242,19 @@ function draw(pathway, elem) {
});
}
function node_popup(n) {
popupContent = "<a href='" + n.url + "'>" + n.name + "</a><br>";
popupContent += "Depth " + n.depth + "<br>"
if (appDomainViewEnabled) {
if (n.app_domain != null) {
popupContent += "This compound is " + (n.app_domain['inside_app_domain'] ? "inside" : "outside") + " the Applicability Domain derived from the chemical (PCA) space constructed using the training data." + "<br>"
if (n.app_domain['uncovered_functional_groups']) {
popupContent += "Compound contains functional groups not covered by the training set <br>"
}
}
}
popupContent += "<img src='" + n.image + "' width='" + 20 * nodeRadius + "'><br>"
if (n.scenarios.length > 0) {
popupContent += '<b>Half-lives and related scenarios:</b><br>'
@ -162,12 +273,23 @@ function draw(pathway, elem) {
function edge_popup(e) {
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br>";
if (e.app_domain) {
adcontent = "<p>";
if (e.app_domain["times_triggered"]) {
adcontent += "This rule triggered " + e.app_domain["times_triggered"] + " times in the training set<br>";
}
adcontent += "Reliability " + e.app_domain["reliability"].toFixed(2) + " (" + (e.app_domain["reliability"] > e.app_domain["reliability_threshold"] ? "&gt" : "&lt") + " Reliability Threshold of " + e.app_domain["reliability_threshold"] + ")<br>";
adcontent += "Local Compatibility " + e.app_domain["local_compatibility"].toFixed(2) + " (" + (e.app_domain["local_compatibility"] > e.app_domain["local_compatibility_threshold"] ? "&gt" : "&lt") + " Local Compatibility Threshold of " + e.app_domain["local_compatibility_threshold"] + ")<br>";
adcontent += "</p>";
popupContent += adcontent;
}
popupContent += "<img src='" + e.image + "' width='" + 20 * nodeRadius + "'><br>"
if (e.reaction_probability) {
popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>';
}
if (e.scenarios.length > 0) {
popupContent += '<b>Half-lives and related scenarios:</b><br>'
for (var s of e.scenarios) {
@ -233,13 +355,12 @@ function draw(pathway, elem) {
.enter().append("line")
// Check if target is pseudo and draw marker only if not pseudo
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
// .on("mouseover", (event, d) => {
// tooltip.style("visibility", "visible")
// .text(`Link: ${d.source.id} → ${d.target.id}`)
// .style("top", `${event.pageY + 5}px`)
// .style("left", `${event.pageX + 5}px`);
// })
// .on("mouseout", () => tooltip.style("visibility", "hidden"));
.attr("marker-end", d => d.target.pseudo ? '' : 'url(#arrow)')
// add element to links array
link.each(function (d) {
d.el = this; // attach the DOM element to the data object
});
pop_add(link, "Reaction", edge_popup);
@ -255,20 +376,10 @@ function draw(pathway, elem) {
.on("click", function (event, d) {
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
})
// .on("mouseover", (event, d) => {
// if (d.pseudo) {
// return
// }
// tooltip.style("visibility", "visible")
// .text(`Node: ${d.id} Depth: ${d.depth}`)
// .style("top", `${event.pageY + 5}px`)
// .style("left", `${event.pageX + 5}px`);
// })
// .on("mouseout", () => tooltip.style("visibility", "hidden"));
// Kreise für die Knoten hinzufügen
node.append("circle")
// make radius "invisible"
// make radius "invisible" for pseudo nodes
.attr("r", d => d.pseudo ? 0.01 : nodeRadius)
.style("fill", "#e8e8e8");
@ -280,5 +391,10 @@ function draw(pathway, elem) {
.attr("width", nodeRadius * 2)
.attr("height", nodeRadius * 2);
// add element to nodes array
node.each(function (d) {
d.el = this; // attach the DOM element to the data object
});
pop_add(node, "Compound", node_popup);
}

View File

@ -1,4 +1,4 @@
{% if meta.can_edit %}
{% if meta.can_edit and meta.enabled_features.MODEL_BUILDING %}
<li>
<a role="button" data-toggle="modal" data-target="#new_model_modal">
<span class="glyphicon glyphicon-plus"></span> New Model</a>

View File

@ -1,6 +1,6 @@
{% if meta.can_edit %}
<li>
<a role="button" data-toggle="modal" data-target="#new_pathway_modal">
<a role="button" data-toggle="modal" data-target="#new_scenario_modal">
<span class="glyphicon glyphicon-plus"></span> New Scenario</a>
</li>
{% endif %}

View File

@ -8,7 +8,17 @@
<i class="glyphicon glyphicon-plus"></i> Add Structure</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#delete_compound_modal">
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
{% endif %}
<li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
</li>
{% if meta.can_edit %}
<li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
</li>
{% endif %}

View File

@ -4,7 +4,11 @@
<i class="glyphicon glyphicon-edit"></i> Edit Compound Structure</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#delete_compound_structure_modal">
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Compound Structure</a>
</li>
{% endif %}

View File

@ -0,0 +1,10 @@
{% if meta.can_edit %}
<li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Edge</a>
</li>
{% endif %}

View File

@ -1,10 +1,10 @@
{% if meta.can_edit %}
<li>
<a role="button" data-toggle="modal" data-target="#delete_group_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Group</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#edit_group_member_modal">
<i class="glyphicon glyphicon-trash"></i> Add/Remove Member</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Group</a>
</li>
{% endif %}

View File

@ -1,6 +1,6 @@
{% if meta.can_edit %}
<li>
<a class="button" data-toggle="modal" data-target="#delete_model_modal">
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Model</a>
</li>
{% endif %}

View File

@ -4,7 +4,11 @@
<i class="glyphicon glyphicon-edit"></i> Edit Node</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#delete_node_modal">
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Node</a>
</li>
{% endif %}

View File

@ -7,12 +7,16 @@
<a role="button" data-toggle="modal" data-target="#edit_package_permissions_modal">
<i class="glyphicon glyphicon-user"></i> Edit Permissions</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#publish_package_modal">
<i class="glyphicon glyphicon-bullhorn"></i> Publish Package</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_license_modal">
<i class="glyphicon glyphicon-duplicate"></i> License</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#delete_package_modal">
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Package</a>
</li>
{% endif %}

View File

@ -8,9 +8,24 @@
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a>
</li>
<li role="separator" class="divider"></li>
{% endif %}
<li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#download_pathway_modal">
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway</a>
</li>
{% if meta.can_edit %}
<li role="separator" class="divider"></li>
<li>
<a class="button" data-toggle="modal" data-target="#edit_pathway_modal">
<i class="glyphicon glyphicon-plus"></i> Edit Pathway</a>
<i class="glyphicon glyphicon-edit"></i> Edit Pathway</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal">#}
@ -26,7 +41,7 @@
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#delete_pathway_modal">
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Pathway</a>
</li>
{% endif %}

View File

@ -4,7 +4,17 @@
<i class="glyphicon glyphicon-edit"></i> Edit Reaction</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#delete_reaction_modal">
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
{% endif %}
<li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
</li>
{% if meta.can_edit %}
<li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
</li>
{% endif %}

View File

@ -3,4 +3,18 @@
<a role="button" data-toggle="modal" data-target="#edit_rule_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Rule</a>
</li>
<li>
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li>
{% endif %}
<li>
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
</li>
{% if meta.can_edit %}
<li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Rule</a>
</li>
{% endif %}

View File

@ -1,2 +1,14 @@
{% if meta.can_edit %}
<li>
<a class="button" data-toggle="modal" data-target="#add_additional_information_modal">
<i class="glyphicon glyphicon-trash"></i> Add Additional Information</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#update_scenario_additional_information_modal">
<i class="glyphicon glyphicon-trash"></i> Set Additional Information</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Scenario</a>
</li>
{% endif %}

View File

@ -16,7 +16,7 @@
{# <i class="glyphicon glyphicon-console"></i> Manage API Token</a>#}
{# </li>#}
<li>
<a role="button" data-toggle="modal" data-target="#delete_user_modal">
<a role="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Account</a>
</li>
{% endif %}

View File

@ -2,14 +2,8 @@
{% load static %}
{% block content %}
{% if reviewed_objects.count > 50 or unreviewed_objects.count > 50 %}
{% if object_type != 'package' %}
<div id="load-remaining-button-div">
<button class="btn btn-secondary btn-lg btn-block" type="button" id="load-remaining">Load all {% if reviewed_objects.count > 0 %} {{ reviewed_objects.count }} {% else %} {{ unreviewed_objects.count }} {% endif %} {{ object_type }}s
</button>
<p></p>
<div id="load-all-loading"></div>
<p></p>
<div>
<div id="load-all-error" style="display: none;">
<div class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
@ -18,11 +12,11 @@
</div>
</div>
<input type="text" id="object-search" class="form-control" placeholder="Search by name" style="display: none;">
<input type="text" id="object-search" class="form-control" placeholder="Search by name"
style="display: none;">
<p></p>
</div>
{% endif %}
{% endif %}
{% block action_modals %}
{% if object_type == 'package' %}
@ -248,21 +242,34 @@
</ul>
{% endif %}
</div>
<style>
.spinner-widget {
position: fixed; /* stays in place on scroll */
bottom: 20px; /* distance from bottom */
right: 20px; /* distance from right */
z-index: 9999; /* above most elements */
width: 60px; /* adjust to gif size */
height: 60px;
}
.spinner-widget img {
width: 100%;
height: auto;
}
</style>
{% if object_type != 'package' %}
<div id="load-all-loading" class="spinner-widget">
<img id="loading-gif" src="{% static '/images/wait.gif' %}" alt="Loading...">
</div>
{% endif %}
</div>
<script>
$(function () {
$('#modal-form-delete-submit').on('click', function (e) {
e.preventDefault();
$('#modal-form-delete').submit();
});
$('#object-search').show();
if ($('#load-remaining').length) {
$('#load-remaining').on('click', function () {
makeLoadingGif("#load-all-loading", "{% static '/images/wait.gif' %}");
{% if object_type != 'package' %}
setTimeout(function () {
$('#load-all-error').hide();
$.getJSON('?all=true', function (resp) {
@ -272,20 +279,26 @@
for (o in resp.objects) {
obj = resp.objects[o];
if (obj.reviewed) {
$('#ReviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + '</a>');
$('#ReviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + ' <span class="glyphicon glyphicon-star" aria-hidden="true" style="float:right" data-toggle="tooltip" data-placement="top" title="" data-original-title="Reviewed"></span></a>');
} else {
$('#UnreviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + '</a>');
}
}
$('#load-all-loading').empty();
$('#load-all-loading').hide();
$('#load-remaining').hide();
}).fail(function (resp) {
$('#load-all-loading').empty();
$('#load-all-loading').hide();
$('#load-all-error').show();
});
}, 2500);
{% endif %}
$('#modal-form-delete-submit').on('click', function (e) {
e.preventDefault();
$('#modal-form-delete').submit();
});
}
$('#object-search').on('keyup', function () {
let query = $(this).val().toLowerCase();

View File

@ -3,7 +3,12 @@
{% load static %}
<head>
<title>{{ title }}</title>
<style>
html, body {
height: 100%; /* ensure body fills viewport */
overflow-x: hidden; /* prevent horizontal scroll */
}
</style>
{# TODO use bundles from bootstrap 3.3.7 #}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
@ -51,7 +56,7 @@
(function () {
var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']);
_paq.push(['setSiteId', '7']);
_paq.push(['setSiteId', '10']);
var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true;
g.src = u + 'matomo.js';
@ -83,21 +88,26 @@
<!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse collapse-framework navbar-collapse-framework" id="navbarCollapse">
<ul class="nav navbar-nav navbar-nav-framework">
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Predict Pathway<b class="caret"></b></a>
<ul role="menu" class="dropdown-menu">
<li>
<a class="button" data-toggle="modal" data-target="#predict_modal">
<i class=" glyphicon glyphicon-tag"></i> Predict Pathway
Predict Pathway
</a>
</li>
<li>
<a class="button" data-toggle="modal" data-target="#batch_predict_modal">
<i class=" glyphicon glyphicon-tags"></i> Batch Prediction
</a>
</li>
</ul>
</li>
{# <li class="dropdown">#}
{# <a data-toggle="dropdown" class="dropdown-toggle" href="#">Predict Pathway<b class="caret"></b></a>#}
{# <ul role="menu" class="dropdown-menu">#}
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#predict_modal">#}
{# <i class=" glyphicon glyphicon-tag"></i> Predict Pathway#}
{# </a>#}
{# </li>#}
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#batch_predict_modal">#}
{# <i class=" glyphicon glyphicon-tags"></i> Batch Prediction#}
{# </a>#}
{# </li>#}
{# </ul>#}
{# </li>#}
<li><a href="{{ meta.server_url }}/package" id="packageLink">Package</a></li>
<li><a href="{{ meta.server_url }}/search" id="searchLink">Search</a></li>
<li><a href="{{ meta.server_url }}/model" id="modelLink">Modelling</a></li>
@ -119,7 +129,7 @@
</ul>
<ul class="nav navbar-nav navbar-right navbar-nav-framework navbar-right-framework">
{# <li><a href="{{ meta.server_url }}/search" id="searchLink">Search</a></li>#}
<li><a href="https://community.envipath.org/" id="communityLink">Community</a></li>
<li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Info <b class="caret"></b></a>
<ul role="menu" class="dropdown-menu">
@ -192,6 +202,23 @@
{% endif %}
{% block content %}
{% endblock content %}
{% if meta.url_contains_package and meta.current_package.license %}
<p></p>
<div class="panel-group" id="license_accordion">
<div class="panel panel-default list-group-item" style="background-color:#f5f5f5">
<div class="panel-title">
<a data-toggle="collapse" data-parent="#licence_accordion" href="#license">License</a>
</div>
</div>
<div id="license" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<a target="_blank" href="{{ meta.current_package.license.link }}">
<img src="{{ meta.current_package.license.image_link }}">
</a>
</div>
</div>
</div>
{% endif %}
</div>
<!-- FOOTER -->

View File

@ -143,6 +143,14 @@
}
$(function () {
$('#index-form').on("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
goButtonClicked();
}
});
// Code that should be executed once DOM is ready goes here
$('#dropdown-predict').on('click', actionDropdownClicked);
$('#dropdown-search').on('click', actionDropdownClicked);

View File

@ -30,7 +30,7 @@
.center-button {
position: absolute;
top: 50%;
top: 70%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
@ -38,7 +38,7 @@
.center-message {
position: absolute;
top: 40%;
top: 35%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 1;
@ -124,6 +124,7 @@
</button>
</div>
</div>
<input type="hidden" name="next" value="{{ next }}" />
</fieldset>
</form>
</div>

View File

@ -3,13 +3,25 @@
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4>How to cite enviPath</h4>
<h3>How to cite enviPath</h3>
</div>
<div class="modal-body">
<p>
enviPathThe environmental contaminant biotransformation pathway resource. J Wicker, T Lorsbach, M
Gütlein, E Schmid, D Latino, S Kramer, K Fenner. Nucleic Acids Research, gkv1229
</p>
<ol class="list-group list-group-numbered">
<li class="list-group-item">
Hafner, J., Lorsbach, T., Schmidt, S. <em>et al.</em>
<cite>Advancements in biotransformation pathway prediction: enhancements, datasets, and novel
functionalities in enviPath.</cite>
<a href="https://doi.org/10.1186/s13321-024-00881-6" target="_blank">J Cheminform 16, 93
(2024)</a>
</li>
<li class="list-group-item">
Wicker, J., Lorsbach, T., Gütlein, M., Schmid, E., Latino, D., Kramer, S., Fenner, K.
<cite>enviPath - The environmental contaminant biotransformation pathway resource</cite>
<a href="https://doi.org/10.1093/nar/gkv1229" target="_blank">
Nucleic Acids Research, Volume 44, Issue D1, 4 January 2016, Pages D502-D508
</a>
</li>
</ol>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>

View File

@ -16,14 +16,14 @@
<div class="jumbotron">Create a new Model to
limit the number of degradation products in the
prediction. You just need to set a name and the packages
you want the object to be based on. If you want to use the
default options suggested by us, simply click Submit,
otherwise click Advanced Options.
you want the object to be based on. There are multiple types of models available.
For additional information have a look at our
<a target="_blank" href="https://wiki.envipath.org/index.php/relative-reasoning" role="button">wiki &gt;&gt;</a>
</div>
<label for="name">Name</label>
<input id="name" name="model-name" class="form-control" placeholder="Name"/>
<label for="description">Description</label>
<input id="description" name="model-description" class="form-control"
<label for="model-name">Name</label>
<input id="model-name" name="model-name" class="form-control" placeholder="Name"/>
<label for="model-description">Description</label>
<input id="model-description" name="model-description" class="form-control"
placeholder="Description"/>
<label for="model-type">Model Type</label>
<select id="model-type" name="model-type" class="form-control" data-width='100%'>
@ -35,7 +35,7 @@
<!-- ML Based Form-->
<div id="ml-relative-reasoning-specific-form">
<!-- Rule Packages -->
<label>Rule Packages</label><br>
<label for="ml-relative-reasoning-rule-packages">Rule Packages</label>
<select id="ml-relative-reasoning-rule-packages" name="ml-relative-reasoning-rule-packages"
data-actions-box='true' class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option>
@ -53,7 +53,7 @@
{% endfor %}
</select>
<!-- Data Packages -->
<label>Data Packages</label><br>
<label for="ml-relative-reasoning-data-packages" >Data Packages</label>
<select id="ml-relative-reasoning-data-packages" name="ml-relative-reasoning-data-packages"
data-actions-box='true' class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option>
@ -77,22 +77,24 @@
class="form-control">
<option value="MACCS" selected>MACCS Fingerprinter</option>
</select>
{% if 'plugins' in meta.enabled_features %}
{% if meta.enabled_features.PLUGINS and additional_descriptors %}
<!-- Property Plugins go here -->
<label for="ml-relative-reasoning-additional-fingerprinter">Fingerprinter</label>
<select id="ml-relative-reasoning-additional-fingerprinter"
name="ml-relative-reasoning-additional-fingerprinter"
class="form-control">
<label for="ml-relative-reasoning-additional-fingerprinter">Additional Fingerprinter / Descriptors</label>
<select id="ml-relative-reasoning-additional-fingerprinter" name="ml-relative-reasoning-additional-fingerprinter" class="form-control">
<option disabled selected>Select Additional Fingerprinter / Descriptor</option>
{% for k, v in additional_descriptors.items %}
<option value="{{ v }}">{{ k }}</option>
{% endfor %}
</select>
{% endif %}
<label for="ml-relative-reasoning-threshold">Threshold</label>
<input type="number" min="0" , max="1" step="0.05" value="0.5"
<input type="number" min="0" max="1" step="0.05" value="0.5"
id="ml-relative-reasoning-threshold"
name="ml-relative-reasoning-threshold" class="form-control">
<!-- Evaluation -->
<label>Evaluation Packages</label><br>
<label for="ml-relative-reasoning-evaluation-packages">Evaluation Packages</label>
<select id="ml-relative-reasoning-evaluation-packages" name="ml-relative-reasoning-evaluation-packages"
data-actions-box='true' class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option>
@ -110,6 +112,26 @@
{% endfor %}
</select>
{% if meta.enabled_features.APPLICABILITY_DOMAIN %}
<!-- Build AD? -->
<div class="checkbox">
<label>
<input type="checkbox" id="build-app-domain" name="build-app-domain">Also build an Applicability Domain?
</label>
</div>
<!-- Num Neighbors -->
<label for="num-neighbors">Number of Neighbors</label>
<input id="num-neighbors" name="num-neighbors" type="number" class="form-control" value="5"
step="1" min="0" max="10">
<!-- Local Compatibility -->
<label for="local-compatibility-threshold">Local Compatibility Threshold</label>
<input id="local-compatibility-threshold" name="local-compatibility-threshold" type="number"
class="form-control" value="0.5" step="0.01" min="0" max="1">
<!-- Reliability -->
<label for="reliability-threshold">Reliability Threshold</label>
<input id="reliability-threshold" name="reliability-threshold" type="number"
class="form-control" value="0.5" step="0.01" min="0" max="1">
{% endif %}
</div>
<!-- Rule Based Based Form-->
<div id="rule-based-relative-reasoning-specific-form">
@ -118,47 +140,9 @@
<!-- EnviFormer-->
<div id="enviformer-specific-form">
<label for="enviformer-threshold">Threshold</label>
<input type="number" min="0" , max="1" step="0.05" value="0.5" id="enviformer-threshold"
<input type="number" min="0" max="1" step="0.05" value="0.5" id="enviformer-threshold"
name="enviformer-threshold" class="form-control">
</div>
{% if 'applicability_domain' in enabled_features %}
<div class="modal-body hide" data-step="3" data-title="Advanced Options II">
<div class="jumbotron">Selection of parameter values for the Applicability Domain process.
Number of Neighbours refers to a requirement on the minimum number of compounds from the
training
dataset that has at least one triggered transformation rule that is common with the compound
being
analyzed.
Reliability Threshold is a requirement on the average tanimoto distance to the set number of
"nearest neighbours" (Number of neighbours with the smallest tanimoto distances).
Local Compatibility Threshold is a requirement on the average F1 score determined from the
number of
nearest neighbours, using their respective precision and recall values computed from the
agreement
between their observed and triggered rules.
You can learn more about it in our wiki!
</div>
<!-- Use AD? -->
<div class="checkbox">
<label>
<input type="checkbox" id="buildAD" name="buildAD">Also build an Applicability Domain?
</label>
</div>
<!-- Num Neighbours -->
<label for="adK">Number of Neighbours</label>
<input id="adK" name="adK" type="number" class="form-control" value="5" step="1" min="0"
max="10">
<!-- F1 Threshold -->
<label for="localCompatibilityThreshold">Local Compatibility Threshold</label>
<input id="localCompatibilityThreshold" name="localCompatibilityThreshold" type="number"
class="form-control" value="0.5" step="0.01" min="0" max="1">
<!-- Percentile Threshold -->
<label for="reliabilityThreshold">Reliability Threshold</label>
<input id="reliabilityThreshold" name="reliabilityThreshold" type="number" class="form-control"
value="0.5" step="0.01" min="0" max="1">
</div>
{% endif %}
</form>
</div>
<div class="modal-footer">
@ -179,6 +163,9 @@ $(function() {
$("#ml-relative-reasoning-rule-packages").selectpicker();
$("#ml-relative-reasoning-data-packages").selectpicker();
$("#ml-relative-reasoning-evaluation-packages").selectpicker();
if ($('#ml-relative-reasoning-additional-fingerprinter').length > 0) {
$("#ml-relative-reasoning-additional-fingerprinter").selectpicker();
}
// On change hide all and show only selected
$("#model-type").change(function() {

View File

@ -1,115 +1,95 @@
<div class="modal fade" tabindex="-1" id="new_scenario_modal" role="dialog" aria-labelledby="newScenGenMod"
<div class="modal fade" tabindex="-1" id="new_scenario_modal" role="dialog" aria-labelledby="new_scenario_modal"
aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> <span
class="sr-only">Close</span>
<span aria-hidden="true">&times;</span>
<span class="sr-only">Close</span>
</button>
<h4 class="js-title-step"></h4>
<h4 class="modal-title">New Scenario</h4>
</div>
<form id="base-scenario-form" accept-charset="UTF-8" action="" data-remote="true" method="POST">
<div class="modal-body hide" data-step="1" data-title="New Scenario - Step 1">
<div class="jumbotron">Please enter name, description,
and date of scenario. Date should be associated to the
data, not the current date. For example, this could
reflect the publishing date of a study. You can leave
all fields but the name empty and fill them in
later.
<div class="modal-body">
<form id="new_scenario_form" accept-charset="UTF-8" action="{{ meta.current_package.url }}/scenario"
data-remote="true" method="post">
{% csrf_token %}
<div class="jumbotron">Please enter name, description, and date of scenario. Date should be
associated to the data, not the current date. For example, this could reflect the publishing
date of a study. You can leave all fields but the name empty and fill them in later.
<a target="_blank" href="https://wiki.envipath.org/index.php/scenario" role="button">wiki
&gt;&gt;</a>
</div>
<label for="name">Name</label>
<input id="name" name="studyname" placeholder="Name" class="form-control"/>
<label for="name">Description</label>
<input id="description" name="studydescription" placeholder="Description" class="form-control"/>
<label for="scenario-name">Name</label>
<input id="scenario-name" name="scenario-name" class="form-control" placeholder="Name"/>
<label for="scenario-description">Description</label>
<input id="scenario-description" name="scenario-description" class="form-control"
placeholder="Description"/>
<label id="dateField" for="dateYear">Date</label>
<table>
<tr>
<th>
<input type="number" id="dateYear" name="dateYear" class="form-control" placeholder="YYYY">
<input type="number" id="dateYear" name="scenario-date-year" class="form-control"
placeholder="YYYY">
</th>
<th>
<input type="number" id="dateMonth" name="dateMonth" min="1" max="12"
<input type="number" id="dateMonth" name="scenario-date-month" min="1" max="12"
class="form-control" placeholder="MM" align="">
</th>
<th>
<input type="number" id="dateDay" name="dateDay" min="1" max="31" class="form-control"
<input type="number" id="dateDay" name="scenario-date-day" min="1" max="31" class="form-control"
placeholder="DD">
</th>
</tr>
</table>
<label for="scenario-type">Scenario Type</label>
<select id="scenario-type" name="scenario-type" class="form-control" data-width='100%'>
<option value="empty" selected>Empty Scenario</option>
{% for k, v in scenario_types.items %}
<option value="{{ v.name }}">{{ k }}</option>
{% endfor %}
</select>
{% for type in scenario_types.values %}
<div id="{{ type.name }}-specific-inputs">
{% for widget in type.widgets %}
{{ widget|safe }}
{% endfor %}
</div>
<div class="modal-body hide" data-step="2" data-title="New Scenario - Step 2">
<div class="jumbotron">
Do you want to create an empty scenario and fill it
with your own set of attributes, or fill in a
pre-defined set of attributes for soil, sludge or sediment
data?
</div>
<div class="radio">
<label>
<input type="radio" name="type" id="radioEmpty" checked>Empty Scenario
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="type" id="radioSoil" value="soil" >Soil Data
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="type" id="radioSludge" value="sludge">Sludge Data
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="type" id="radioSediment" value="sediment">Water-Sediment System Data
</label>
</div>
</div>
<div class="modal-body hide" data-step="3" data-title="New Scenario - Step 3">
<div class="jumbotron" id="finaljumbo">
All done! Click Submit!
</div>
<div style="float: left"><button
id="addColumnButton" type="button"
class="btn btn-default">Add
another Scenario
</button></div>
<input type="hidden" name="fullScenario" value="true"/>
{% include "tables/scenario.html" %}
</div>
{% endfor %}
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default js-btn-step pull-left" data-orientation="cancel" data-dismiss="modal"></button>
<button type="button" class="btn btn-default js-btn-step" data-orientation="previous"></button>
<button type="button" class="btn btn-default js-btn-step"
data-orientation="next" id="nextbutton"></button>
<a id="new_scenario_modal_form_submit" class="btn btn-primary" href="#">Submit</a>
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
<p></p>
<div id="scenariocontent"></div>
<!--Template index -->
<script language="javascript">
<script>
$(function () {
// Hide additional columns per default
$('.col-2').hide();
$('.col-3').hide();
//TODO just to see modal
$('#new_scenario_modal').modalSteps({
btnCancelHtml: 'Cancel',
btnPreviousHtml: 'Previous',
btnNextHtml: 'Next',
btnLastStepHtml: 'Submit',
disableNextButton: false,
// Initially hide all "specific" forms
$("div[id$='-specific-inputs']").each(function () {
$(this).hide();
});
// On change hide all and show only selected
$("#scenario-type").change(function () {
$("div[id$='-specific-inputs']").each(function () {
$(this).hide();
});
val = $('option:selected', this).val();
$("#" + val + "-specific-inputs").show();
});
$('#new_scenario_modal_form_submit').on('click', function (e) {
e.preventDefault();
$('#new_scenario_form').submit();
});
});
</script>

View File

@ -0,0 +1,61 @@
{% load static %}
<!-- Add Additional Information-->
<div id="add_additional_information_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h3 class="modal-title">Add Additional Information</h3>
</div>
<div class="modal-body">
<select id="select-additional-information-type" data-actions-box='true' class="form-control" data-width='100%'>
<option selected disabled>Select the type to add</option>
{% for add_inf in available_additional_information %}
<option value="{{ add_inf.name }}">{{ add_inf.display_name }}</option>
{% endfor %}
</select>
{% for add_inf in available_additional_information %}
<div class="aiform {{ add_inf.name }}" style="display: none;">
<form id="add_{{ add_inf.name }}_add-additional-information-modal-form" accept-charset="UTF-8"
action="" data-remote="true" method="post">
{% csrf_token %}
{{ add_inf.widget|safe }}
<input type="hidden" name="hidden" value="add-additional-information">
</form>
</div>
{% endfor %}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="add-additional-information-modal-submit">Add
</button>
</div>
</div>
</div>
</div>
<script>
$(function() {
$('#select-additional-information-type').change(function(e){
var selectedType = $("#select-additional-information-type :selected").val();
$('.aiform').hide();
$('.' + selectedType).show();
})
$('#add-additional-information-modal-submit').click(function(e){
e.preventDefault();
var selectedType = $("#select-additional-information-type :selected").val();
console.log(selectedType);
if (selectedType !== null && selectedType !== undefined && selectedType !== '') {
$('.' + selectedType + ' >form').submit();
}
});
});
</script>

View File

@ -1,38 +0,0 @@
{% load static %}
<!-- Delete Group -->
<div id="delete_group_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete Group</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="alert alert-danger">
Clicking "Delete" will <strong>permanently</strong> delete the Group.
This action can't be undone!
</div>
<form id="delete-group-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %}
<input type="hidden" name="hidden" value="delete-group">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" id="delete-group-modal-submit">Delete</button>
</div>
</div>
</div>
</div>
<script>
$(function() {
$('#delete-group-modal-submit').click(function(e){
e.preventDefault();
$('#delete-group-modal-form').submit();
});
})
</script>

View File

@ -1,35 +0,0 @@
{% load static %}
<!-- Delete Model -->
<div id="delete_model_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete Model</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Deletes the Model.
<form id="delete-model-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %}
<input type="hidden" id="hidden" name="hidden" value="delete-model"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="delete-model-modal-submit">Delete</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#delete-model-modal-submit').click(function (e) {
e.preventDefault();
$('#delete-model-modal-form').submit();
});
})
</script>

View File

@ -1,35 +0,0 @@
{% load static %}
<!-- Delete Node -->
<div id="delete_node_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete Node</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Deletes the Node as well as ingoing and outgoing edges.
<form id="delete-node-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %}
<input type="hidden" id="hidden" name="hidden" value="delete-node"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="delete-node-modal-submit">Delete</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#delete-node-modal-submit').click(function (e) {
e.preventDefault();
$('#delete-node-modal-form').submit();
});
})
</script>

View File

@ -1,36 +0,0 @@
{% load static %}
<!-- Delete Package -->
<div id="delete_package_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete Package</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Deleting a Package deletes the very Package
as well as all Objects stored in the Package.
<form id="delete-package-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %}
<input type="hidden" id="hidden" name="hidden" value="delete-package"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="delete-package-modal-submit">Delete</button>
</div>
</div>
</div>
</div>
<script>
$(function() {
$('#delete-package-modal-submit').click(function(e){
e.preventDefault();
$('#delete-package-modal-form').submit();
});
})
</script>

View File

@ -22,7 +22,7 @@
<option value="{{ e.url }}">{{ e.edge_label.name }}</option>
{% endfor %}
</select>
<input type="hidden" id="hidden" name="hidden" value="delete-edge"/>
<input type="hidden" id="hidden" name="hidden" value="delete"/>
</form>
<p></p>
<div id="delete_pathway_edge_image"></div>

View File

@ -22,7 +22,7 @@
<option value="{{ n.url }}">{{ n.default_node_label.name }}</option>
{% endfor %}
</select>
<input type="hidden" id="hidden" name="hidden" value="delete-node"/>
<input type="hidden" id="hidden" name="hidden" value="delete"/>
</form>
<p></p>
<div id="delete_pathway_node_image"></div>

View File

@ -1,35 +0,0 @@
{% load static %}
<!-- Delete Reaction -->
<div id="delete_reaction_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete Reaction</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Deletes the Reaction.
<form id="delete-reaction-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %}
<input type="hidden" id="hidden" name="hidden" value="delete-reaction"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="delete-reaction-modal-submit">Delete</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#delete-reaction-modal-submit').click(function (e) {
e.preventDefault();
$('#delete-reaction-modal-form').submit();
});
})
</script>

View File

@ -1,38 +0,0 @@
{% load static %}
<!-- Delete User -->
<div id="delete_user_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete User</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="alert alert-danger">
Clicking "Delete" will <strong>permanently</strong> delete the User and associated data.
This action can't be undone!
</div>
<form id="delete-user-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %}
<input type="hidden" name="hidden" value="delete-user">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-danger" id="delete-user-modal-submit">Delete</button>
</div>
</div>
</div>
</div>
<script>
$(function() {
$('#delete-user-modal-submit').click(function(e){
e.preventDefault();
$('#delete-user-modal-form').submit();
});
})
</script>

View File

@ -1,24 +1,24 @@
{% load static %}
<!-- Delete Pathway -->
<div id="delete_pathway_modal" class="modal" tabindex="-1">
<!-- Download Pathway -->
<div id="download_pathway_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete Pathway</h3>
<h3 class="modal-title">Download Pathway</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Deletes the Pathway together with all Nodes and Edges.
<form id="delete-pathway-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %}
<input type="hidden" id="hidden" name="hidden" value="delete-pathway"/>
By clicking on Download the Pathway will be converted into a CSV and directly downloaded.
<form id="download-pathway-modal-form" accept-charset="UTF-8" action="{{ pathway.url }}"
data-remote="true" method="GET">
<input type="hidden" name="download" value="true"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="delete-pathway-modal-submit">Delete</button>
<button type="button" class="btn btn-primary" id="download-pathway-modal-submit">Download</button>
</div>
</div>
</div>
@ -26,9 +26,10 @@
<script>
$(function () {
$('#delete-pathway-modal-submit').click(function (e) {
$('#download-pathway-modal-submit').click(function (e) {
e.preventDefault();
$('#delete-pathway-modal-form').submit();
$('#download-pathway-modal-form').submit();
$('#download_pathway_modal').modal('hide');
});
})

View File

@ -27,7 +27,7 @@
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="edit-compound-modal-submit">Create</button>
<button type="button" class="btn btn-primary" id="edit-compound-modal-submit">Update</button>
</div>
</div>
</div>

View File

@ -0,0 +1,61 @@
{% load static %}
<!-- Copy Object -->
<div id="generic_copy_object_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Copy {{ object_type|capfirst }}</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<form id="generic-copy-object-modal-form" accept-charset="UTF-8" data-remote="true" method="post">
{% csrf_token %}
<label for="target-package">Select the Target Package you want to copy this {{ object_type }}
into</label>
<select id="target-package" name="target-package" data-actions-box='true' class="form-control"
data-width='100%'>
<option disabled selected>Select Target Package</option>
{% for p in meta.writeable_packages %}
<option value="{{ p.url }}">{{ p.name }}</option>`
{% endfor %}
</select>
<input type="hidden" name="hidden" value="copy">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="generic-copy-object-modal-form-submit">
Copy
</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#generic-copy-object-modal-form-submit').click(function (e) {
e.preventDefault();
const packageUrl = $('#target-package').find(":selected").val();
if (packageUrl === 'Select Target Package' || packageUrl === '' || packageUrl === null || packageUrl === undefined) {
return;
}
const formData = {
hidden: 'copy',
object_to_copy: '{{ current_object.url }}',
}
$.post(packageUrl, formData, function (response) {
if (response.success) {
window.location.href = response.success;
}
});
});
})
</script>

View File

@ -0,0 +1,42 @@
{% load static %}
<!-- Delete Object -->
<div id="generic_delete_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete {{ object_type|capfirst }}</h3>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
{% if object_type == 'user' %}
Clicking "Delete" will <strong>permanently</strong> delete the User and associated data.
This action can't be undone!
{% else %}
Deletes the {{ object_type|capfirst }}. Related objects that depend on this {{ object_type|capfirst }}
will be deleted as well.
{% endif %}
<form id="generic-delete-modal-form" accept-charset="UTF-8" action="{{ current_object.url }}"
data-remote="true" method="post">
{% csrf_token %}
<input type="hidden" id="hidden" name="hidden" value="delete"/>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="generic-delete-modal-form-submit">Delete</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
$('#generic-delete-modal-form-submit').click(function (e) {
e.preventDefault();
$('#generic-delete-modal-form').submit();
});
})
</script>

View File

@ -0,0 +1,72 @@
{% load static %}
<div class="modal fade bs-modal-lg" id="set_scenario_modal" tabindex="-1" aria-labelledby="set_scenario_modal"
aria-modal="true" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
<h4 class="modal-title">Set Scenarios for {{ current_object.name }}</h4>
</div>
<div class="modal-body">
<div id="loading_scenario_div" class="text-center"></div>
<form id="set_scenario_modal_form" accept-charset="UTF-8" action="{{ current_object.url }}"
data-remote="true" method="post">
{% csrf_token %}
<label for="scenario-select">Scenarios</label>
<select id="scenario-select" name="selected-scenarios" data-actions-box='true' class="form-control"
multiple data-width='100%'>
<option disabled>Select Scenarios</option>
</select>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary pull-left" data-dismiss="modal">Close
</button>
<button type="button" class="btn btn-primary" id="set_scenario_modal_form_submit">Submit</button>
</div>
</div>
</div>
</div>
<script>
$(function () {
var loaded = false;
var attachedScenarios = []
{% if current_object.scenarios.all %}
{% for scen in current_object.scenarios.all %}
attachedScenarios.push('{{ scen.url }}')
{% endfor %}
{% endif %}
$('#scenario-select').selectpicker();
$('#set_scenario_modal').on('shown.bs.modal', function () {
if (!loaded) {
makeLoadingGif("#loading_scenario_div", "{% static '/images/wait.gif' %}");
$('#loading_scenario_div').append("<p></p><div class='alert alert-info'>Loading Scenarios...</div>");
$.getJSON("{% url 'package scenario list' meta.current_package.uuid %}").then(function (data) {
for(s in data) {
scenario = data[s]
var selected = attachedScenarios.includes(scenario.url);
$('#scenario-select').append(`<option value="${scenario.url}" ${selected ? 'selected' : ''}>${scenario.name}</option>`);
}
$('#scenario-select').selectpicker('refresh');
$("#loading_scenario_div").empty();
});
loaded = true;
}
$('#set_scenario_modal_form_submit').on('click', function (e) {
e.preventDefault();
$('#set_scenario_modal_form').submit();
});
});
});
</script>

View File

@ -1,24 +1,24 @@
{% load static %}
<!-- Delete Compound -->
<div id="delete_compound_modal" class="modal" tabindex="-1">
<!-- Publish a Package -->
<div id="publish_package_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h3 class="modal-title">Delete Compound</h3>
<h5 class="modal-title">Publish Package</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
Deletes the Compound and associated Structures.
<form id="delete-compound-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
<p>Clicking on Publish will make this Package publicly available!</p>
<form id="publish-package-modal-form" accept-charset="UTF-8" action="{{ current_package.url }}" data-remote="true" method="post">
{% csrf_token %}
<input type="hidden" id="hidden" name="hidden" value="delete-compound"/>
<input type="hidden" name="hidden" value="publish-package">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="delete-compound-modal-submit">Delete</button>
<button type="button" class="btn btn-primary" id="publish-package-modal-form-submit">Publish</button>
</div>
</div>
</div>
@ -26,9 +26,9 @@
<script>
$(function() {
$('#delete-compound-modal-submit').click(function(e){
$('#publish-package-modal-form-submit').click(function(e){
e.preventDefault();
$('#delete-compound-modal-form').submit();
$('#publish-package-modal-form').submit();
});
})

View File

@ -0,0 +1,39 @@
{% load static %}
<!-- Update Scenario Additional Information-->
<div id="update_scenario_additional_information_modal" class="modal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h3 class="modal-title">Update Additional Information</h3>
</div>
<div class="modal-body">
<form id="edit-scenario-additional-information-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post">
{% csrf_token %}
{% for widget in update_widgets %}
{{ widget|safe }}
{% endfor %}
<input type="hidden" name="hidden" value="set-additional-information">
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary" id="edit-scenario-additional-information-modal-submit">Update</button>
</div>
</div>
</div>
</div>
<script>
$(function() {
$('#edit-scenario-additional-information-modal-submit').click(function(e){
e.preventDefault();
$('#edit-scenario-additional-information-modal-form').submit();
});
});
</script>

View File

@ -49,6 +49,18 @@
<iframe id="predict-modal-ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%"
height="510"></iframe>
</div>
<label for="prediction-setting">Default Prediction Setting</label>
<select id="prediction-setting" name="prediction-setting" class="form-control"
data-width='100%'>
<option disabled>Select a Setting</option>
{% for s in meta.available_settings %}
<option value="{{ s.url }}"{% if s.id == meta.user.default_setting.id %}selected{% endif %}>
{{ s.name }}{% if s.id == meta.user.default_setting.id %} <i>(User default)</i>{% endif %}
</option>
{% endfor %}
</select>
</form>
</div>
<div class="modal-footer">

View File

@ -4,6 +4,9 @@
{% block action_modals %}
{% include "modals/objects/edit_rule_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="rule-detail">
@ -49,7 +52,22 @@
</div>
</div>
<!-- Scenarios -->
{% if rule.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="rule-scenario-link" data-toggle="collapse" data-parent="#rule-detail"
href="#rule-scenario">Scenarios</a>
</h4>
</div>
<div id="rule-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for s in rule.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- EC Numbers -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">

View File

@ -5,7 +5,9 @@
{% block action_modals %}
{% include "modals/objects/edit_compound_modal.html" %}
{% include "modals/objects/add_structure_modal.html" %}
{% include "modals/objects/delete_compound_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="compound-detail">
@ -134,7 +136,69 @@
</div>
</div>
{% endif %}
<!-- Scenarios -->
{% if compound.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound-scenario-link" data-toggle="collapse" data-parent="#compound-detail"
href="#compound-scenario">Scenarios</a>
</h4>
</div>
<div id="compound-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for s in compound.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- External Identifiers -->
{% if compound.get_external_identifiers %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound-external-identifier-link" data-toggle="collapse" data-parent="#compound-detail"
href="#compound-external-identifier">External Identifier</a>
</h4>
</div>
<div id="compound-external-identifier" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% if compound.get_pubchem_identifiers %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">
<h4 class="panel-title">
<a id="compound-pubchem-identifier-link" data-toggle="collapse"
data-parent="#compound-external-identifier"
href="#compound-pubchem-identifier">PubChem Compound Identifier</a>
</h4>
</div>
<div id="compound-pubchem-identifier" class="panel-collapse collapse in">
{% for eid in compound.get_pubchem_identifiers %}
<a class="list-group-item"
href="{{ eid.external_url }}">CID{{ eid.identifier_value }}</a>
{% endfor %}
</div>
{% endif %}
{% if compound.get_chebi_identifiers %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">
<h4 class="panel-title">
<a id="compound-chebi-identifier-link" data-toggle="collapse"
data-parent="#compound-external-identifier"
href="#compound-chebi-identifier">ChEBI Identifier</a>
</h4>
</div>
<div id="compound-chebi-identifier" class="panel-collapse collapse in">
{% for eid in compound.get_chebi_identifiers %}
<a class="list-group-item"
href="{{ eid.external_url }}">CHEBI:{{ eid.identifier_value }}</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>

View File

@ -4,6 +4,8 @@
{% block action_modals %}
{% include "modals/objects/edit_compound_structure_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="compound-structure-detail">
@ -54,6 +56,22 @@
</div>
</div>
{% if compound_structure.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="compound_structure-scenario-link" data-toggle="collapse" data-parent="#compound-structure-detail"
href="#compound-structure-scenario">Scenarios</a>
</h4>
</div>
<div id="compound-structure-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for s in compound_structure.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- Reactions -->
<!-- Pathways -->

View File

@ -4,7 +4,8 @@
{% block action_modals %}
{# {% include "modals/objects/edit_edge_modal.html" %}#}
{# {% include "modals/objects/delete_edge_modal.html" %}#}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="edge-detail">
@ -19,7 +20,7 @@
style="padding-right:1em"></span></a>
<ul id="actionsList" class="dropdown-menu">
{% block actions %}
{# {% include "actions/objects/edge.html" %}#}
{% include "actions/objects/edge.html" %}
{% endblock %}
</ul>
</div>
@ -103,6 +104,21 @@
</div>
{% endif %}
{% if edge.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="edge-scenario-link" data-toggle="collapse" data-parent="#edge-detail"
href="#edge-scenario">Scenarios</a>
</h4>
</div>
<div id="edge-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for s in edge.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
</div>
{% endblock content %}

View File

@ -5,7 +5,7 @@
{% block action_modals %}
{% include "modals/objects/edit_group_modal.html" %}
{% include "modals/objects/edit_group_member_modal.html" %}
{% include "modals/objects/delete_group_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="package-detail">

View File

@ -4,7 +4,7 @@
{% block content %}
{% block action_modals %}
{% include "modals/objects/delete_model_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<!-- Include required libs -->
@ -90,6 +90,7 @@
</div>
</div>
{% endif %}
{% if model.ready_for_prediction %}
<!-- Predict Panel -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
@ -106,11 +107,36 @@
<button class="btn btn-default" type="submit" id="predict-button">Predict!</button>
</span>
</div>
<div id="loading"></div>
<div id="predictLoading"></div>
<div id="predictResultTable"></div>
</div>
</div>
<!-- End Predict Panel -->
{% endif %}
{% if model.app_domain %}
<!-- App Domain -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="app-domain-assessment-link" data-toggle="collapse" data-parent="#model-detail"
href="#app-domain-assessment">Applicability Domain Assessment</a>
</h4>
</div>
<div id="app-domain-assessment" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<div class="input-group">
<input id="smiles-to-assess" type="text" class="form-control" placeholder="CCN(CC)C(=O)C1=CC(=CC=C1)C">
<span class="input-group-btn">
<button class="btn btn-default" type="submit" id="assess-button">Assess!</button>
</span>
</div>
<div id="appDomainLoading"></div>
<div id="appDomainAssessmentResultTable"></div>
</div>
</div>
<!-- End App Domain -->
{% endif %}
{% if model.model_status == 'FINISHED' %}
<!-- Single Gen Curve Panel -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
@ -243,7 +269,7 @@
<script>
function handleResponse(data) {
function handlePredictionResponse(data) {
res = "<table class='table table-striped'>"
res += "<thead>"
res += "<th scope='col'>#</th>"
@ -277,9 +303,9 @@
$("#predictResultTable").append(res);
}
function clear() {
$("#predictResultTable").removeClass("alert alert-danger");
$("#predictResultTable").empty();
function clear(divid) {
$("#" + divid).removeClass("alert alert-danger");
$("#" + divid).empty();
}
if ($('#predict-button').length > 0) {
@ -291,32 +317,70 @@
"classify": "ILikeCats!"
}
clear();
clear("predictResultTable");
makeLoadingGif("#loading", "{% static '/images/wait.gif' %}");
makeLoadingGif("#predictLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: 'get',
data: data,
url: '',
success: function (data, textStatus) {
try {
$("#loading").empty();
handleResponse(data);
$("#predictLoading").empty();
handlePredictionResponse(data);
} catch (error) {
console.log("Error");
$("#loading").empty();
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append("Error while processing request :/");
}
},
error: function (jqXHR, textStatus, errorThrown) {
$("#loading").empty();
$("#predictLoading").empty();
$("#predictResultTable").addClass("alert alert-danger");
$("#predictResultTable").append("Error while processing request :/");
}
});
});
}
if ($('#assess-button').length > 0) {
$("#assess-button").on("click", function (e) {
e.preventDefault();
data = {
"smiles": $("#smiles-to-assess").val(),
"app-domain-assessment": "ILikeCats!"
}
clear("appDomainAssessmentResultTable");
makeLoadingGif("#appDomainLoading", "{% static '/images/wait.gif' %}");
$.ajax({
type: 'get',
data: data,
url: '',
success: function (data, textStatus) {
try {
$("#appDomainLoading").empty();
handleAssessmentResponse("{% url 'depict' %}", data);
console.log(data);
} catch (error) {
console.log("Error");
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Error while processing request :/");
}
},
error: function (jqXHR, textStatus, errorThrown) {
$("#appDomainLoading").empty();
$("#appDomainAssessmentResultTable").addClass("alert alert-danger");
$("#appDomainAssessmentResultTable").append("Error while processing request :/");
}
});
});
}
</script>
{% endblock content %}

View File

@ -4,7 +4,8 @@
{% block action_modals %}
{% include "modals/objects/edit_node_modal.html" %}
{% include "modals/objects/delete_node_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="node-detail">
@ -69,7 +70,34 @@
</div>
</div>
{% if node.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="node-scenario-link" data-toggle="collapse" data-parent="#node-detail"
href="#node-scenario">Scenarios</a>
</h4>
</div>
<div id="node-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for s in node.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
{% if app_domain_assessment_data %}
<div id="appDomainAssessmentResultTable"></div>
<script>
$(document).ready(function () {
handleAssessmentResponse("{% url 'depict' %}", {{ app_domain_assessment_data|safe }})
})
</script>
{% endif %}
</div>
</div>
{% endblock content %}

View File

@ -5,8 +5,9 @@
{% block action_modals %}
{% include "modals/objects/edit_package_modal.html" %}
{% include "modals/objects/edit_package_permissions_modal.html" %}
{% include "modals/objects/publish_package_modal.html" %}
{% include "modals/objects/set_license_modal.html" %}
{% include "modals/objects/delete_package_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="package-detail">
@ -52,23 +53,5 @@
</div>
{% if package.license %}
<p></p>
<div class="panel-group" id="license_accordion">
<div class="panel panel-default list-group-item" style="background-color:#f5f5f5">
<div class="panel-title">
<a data-toggle="collapse" data-parent="#licence_accordion" href="#license">License</a>
</div>
</div>
<div id="license" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
<a target="_blank" href="{{ package.license.link }}">
<img src="{{ package.license.image_link }}">
</a>
</div>
</div>
</div>
{% endif %}
</div>
{% endblock content %}

View File

@ -4,16 +4,22 @@
<script src="https://d3js.org/d3.v7.min.js"></script>
<style>
svg {
#vizdiv {
width: 100%;
height: 600px;
background: white;
}
#pwsvg {
width: 100%;
height: 100%;
color: red;
}
.link {
stroke: #999;
stroke-opacity: 0.6;
marker-end: url(#arrow);
//marker-end: url(#arrow);
}
.link_no_arrow {
@ -31,6 +37,31 @@
stroke-width: 1.5px;
}
.inside_app_domain {
fill: green;
stroke: green;
stroke-width: 1.5px;
}
.outside_app_domain {
fill: red;
stroke: red;
stroke-width: 1.5px;
}
.passes_app_domain {
stroke: green;
stroke-width: 1.5px;
stroke-opacity: 0.6;
}
.fails_app_domain {
stroke: red;
stroke-width: 1.5px;
stroke-opacity: 0.6;
}
.highlighted {
stroke: red;
stroke-width: 3px;
@ -50,10 +81,13 @@
{% block action_modals %}
{% include "modals/objects/add_pathway_node_modal.html" %}
{% include "modals/objects/add_pathway_edge_modal.html" %}
{% include "modals/objects/download_pathway_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/edit_pathway_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/delete_pathway_node_modal.html" %}
{% include "modals/objects/delete_pathway_edge_modal.html" %}
{% include "modals/objects/delete_pathway_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<p></p>
@ -79,8 +113,7 @@
<ul class="nav navbar-nav">
<li class="dropdown requiresWritePerm">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true"
aria-expanded="false">
aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-edit"></span>
Edit
<span class="caret"></span></a>
@ -90,11 +123,26 @@
{% endblock %}
</ul>
</li>
{% if pathway.setting.model.app_domain %}
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-expanded="false">
<span class="glyphicon glyphicon-eye-open"></span>
View
<span class="caret"></span></a>
<ul id="editingList" class="dropdown-menu">
<li>
<a class="button" id="app-domain-toggle-button">
<i id="app-domain-toggle-button" class="glyphicon glyphicon-eye-open"></i> App Domain View</a>
</li>
</ul>
</li>
{% endif %}
</ul>
<ul class="nav navbar-nav navbar-right">
<li>
<a role="button" data-toggle="modal" onclick="goFullscreen('pwcontent')">
<a role="button" data-toggle="modal" onclick="goFullscreen('vizdiv')">
<span class="glyphicon glyphicon-fullscreen"></span>
Fullscreen
</a>
@ -126,15 +174,23 @@
</div>
</nav>
<div id="vizdiv" >
<svg width="2000" height="2000">
<svg id="pwsvg">
{% if debug %}
<rect width="100%" height="100%" fill="aliceblue"/>
{% endif %}
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="43" refY="5" markerWidth="6" markerHeight="6"
orient="auto-start-reverse">
orient="auto-start-reverse" markerUnits="userSpaceOnUse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="#999"/>
</marker>
<marker id="arrow_passes_app_domain" viewBox="0 0 10 10" refX="43" refY="5" markerWidth="6" markerHeight="6"
orient="auto-start-reverse" markerUnits="userSpaceOnUse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="green"/>
</marker>
<marker id="arrow_fails_app_domain" viewBox="0 0 10 10" refX="43" refY="5" markerWidth="6" markerHeight="6"
orient="auto-start-reverse" markerUnits="userSpaceOnUse">
<path d="M 0 0 L 10 5 L 0 10 z" fill="red"/>
</marker>
</defs>
<g id="zoomable"></g>
</svg>
@ -153,13 +209,29 @@
</div>
</div>
{% if pathway.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="pathway-scenario-link" data-toggle="collapse" data-parent="#pathway-detail"
href="#pathway-scenario">Scenarios</a>
</h4>
</div>
<div id="pathway-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for s in pathway.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
{% if pathway.setting %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="pathwaySettingLink" data-toggle="collapse" data-parent="#pathwayAccordion"
href="#pathwaySetting">Setting</a></h4>
</div>
<div id="pathwaySetting" class="panel-collapse collapse in">
<div id="pathwaySetting" class="panel-collapse collapse">
<div class="panel-body list-group-item" id="pathwaySettingContent">
<table class="table table-bordered table-hover">
<tr style="background-color: rgba(0, 0, 0, 0.08);">
@ -245,6 +317,8 @@
</div>
</div>
<script>
// Globla switch for app domain view
var appDomainViewEnabled = false;
function goFullscreen(id) {
var element = document.getElementById(id);
@ -266,6 +340,51 @@
// TODO fix somewhere else...
var newDesc = transformReferences($('#DescriptionContent')[0].innerText);
$('#DescriptionContent').html(newDesc);
$('#app-domain-toggle-button').on('click', function () {
// glyphicon glyphicon-eye-close
// glyphicon glyphicon-eye-open
appDomainViewEnabled = !appDomainViewEnabled;
if (appDomainViewEnabled) {
$('#app-domain-toggle-button > i').removeClass('glyphicon-eye-open');
$('#app-domain-toggle-button > i').addClass('glyphicon-eye-close');
nodes.forEach((x) => {
if(x.app_domain) {
if (x.app_domain.inside_app_domain) {
d3.select(x.el).select("circle").classed("inside_app_domain", true);
} else {
d3.select(x.el).select("circle").classed("outside_app_domain", true);
}
}
});
links.forEach((x) => {
if(x.app_domain) {
if (x.app_domain.passes_app_domain) {
d3.select(x.el).attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow_passes_app_domain)");
d3.select(x.el).classed("passes_app_domain", true);
} else {
d3.select(x.el).attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow_fails_app_domain)");
d3.select(x.el).classed("fails_app_domain", true);
}
}
});
} else {
$('#app-domain-toggle-button > i').removeClass('glyphicon-eye-close');
$('#app-domain-toggle-button > i').addClass('glyphicon-eye-open');
nodes.forEach((x) => {
d3.select(x.el).select("circle").classed("inside_app_domain", false);
d3.select(x.el).select("circle").classed("outside_app_domain", false);
});
links.forEach((x) => {
d3.select(x.el).attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)");
d3.select(x.el).classed("passes_app_domain", false);
d3.select(x.el).classed("fails_app_domain", false);
});
}
})
});
</script>

View File

@ -4,7 +4,9 @@
{% block action_modals %}
{% include "modals/objects/edit_reaction_modal.html" %}
{% include "modals/objects/delete_reaction_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="reaction-detail">
@ -118,9 +120,70 @@
{% endfor %}
</div>
</div>
{% endif %}
{% if reaction.scenarios.all %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="reaction-scenario-link" data-toggle="collapse" data-parent="#reaction-detail"
href="#reaction-scenario">Scenarios</a>
</h4>
</div>
<div id="reaction-scenario" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% for s in reaction.scenarios.all %}
<a class="list-group-item" href="{{ s.url }}">{{ s.name }} <i>({{ s.package.name }})</i></a>
{% endfor %}
</div>
</div>
{% endif %}
<!-- External Identifiers -->
{% if reaction.get_external_identifiers %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title">
<a id="reaction-external-identifier-link" data-toggle="collapse" data-parent="#reaction-detail"
href="#reaction-external-identifier">External Identifier</a>
</h4>
</div>
<div id="reaction-external-identifier" class="panel-collapse collapse in">
<div class="panel-body list-group-item">
{% if reaction.get_rhea_identifiers %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">
<h4 class="panel-title">
<a id="reaction-rhea-identifier-link" data-toggle="collapse"
data-parent="#reaction-external-identifier"
href="#reaction-rhea-identifier">Rhea</a>
</h4>
</div>
<div id="reaction-rhea-identifier" class="panel-collapse collapse in">
{% for eid in reaction.get_rhea_identifiers %}
<a class="list-group-item"
href="{{ eid.external_url }}">{{ eid.identifier_value }}</a>
{% endfor %}
</div>
{% endif %}
{% if reaction.get_uniprot_identifiers %}
<div class="panel panel-default panel-heading list-group-item"
style="background-color:silver">
<h4 class="panel-title">
<a id="reaction-uniprot-identifier-link" data-toggle="collapse"
data-parent="#reaction-external-identifier"
href="#reaction-uniprot-identifier">UniProt</a>
</h4>
</div>
<div id="reaction-uniprot-identifier" class="panel-collapse collapse in">
{% for eid in reaction.get_uniprot_identifiers %}
<a class="list-group-item"
href="{{ eid.external_url }}">10 SwissProt entries ({{ eid.identifier_value }})</a>
{% endfor %}
</div>
{% endif %}
</div>
</div>
{% endif %}
</div>
</div>
{% endblock content %}

View File

@ -3,7 +3,9 @@
{% block content %}
{% block action_modals %}
{% include "modals/objects/add_additional_information_modal.html" %}
{% include "modals/objects/update_scenario_additional_information_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="scenario-detail">
<div class="panel panel-default">
@ -24,6 +26,18 @@
</div>
</div>
</div>
<div class="panel panel-default">
<div class="panel-heading">Description</div>
<div class="panel-body">
{{ scenario.description }}
<br>
{{ scenario.scenario_type }}
<br>
Reported {{ scenario.scenario_date }}
</div>
</div>
<div class="table-responsive">
<table id="scenario-table" class="table table-bordered table-striped table-hover">
<tbody>
@ -31,7 +45,9 @@
<th>Property</th>
<th>Value</th>
<th>Unit</th>
{% if meta.can_edit %}
<th>Remove</th>
{% endif %}
</tr>
{% for ai in scenario.get_additional_information %}
@ -39,10 +55,32 @@
<td> {{ ai.property_name|safe }} </td>
<td> {{ ai.property_data|safe }} </td>
<td> {{ ai.property_unit|safe }} </td>
<td></td>
{% if meta.can_edit %}
<td>
<form action="{% url 'package scenario detail' scenario.package.uuid scenario.uuid %}" method="post">
{% csrf_token %}
<input type="hidden" name="uuid" value="{{ ai.uuid }}">
<input type="hidden" name="hidden" value="delete-additional-information">
<button type="submit" class="btn"><span class="glyphicon glyphicon-minus"></span></button>
</form>
</td>
{% endif %}
</tr>
{% endfor %}
{% if meta.can_edit %}
<tr>
<td></td>
<td></td>
<td>Delete all</td>
<td>
<form action="{% url 'package scenario detail' scenario.package.uuid scenario.uuid %}" method="post">
{% csrf_token %}
<input type="hidden" name="hidden" value="delete-all-additional-information">
<button type="submit" class="btn"><span class="glyphicon glyphicon-trash"></span></button>
</form>
</td>
</tr>
{% endif %}
</tbody>
</table>
</div>

View File

@ -4,6 +4,9 @@
{% block action_modals %}
{% include "modals/objects/edit_rule_modal.html" %}
{% include "modals/objects/generic_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="rule-detail">

View File

@ -7,7 +7,7 @@
{% include "modals/objects/edit_password_modal.html" %}
{% include "modals/collections/new_prediction_setting_modal.html" %}
{% include "modals/objects/manage_api_token_modal.html" %}
{% include "modals/objects/delete_user_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %}
<div class="panel-group" id="user-detail">

View File

@ -79,6 +79,10 @@
allEmpty = true;
for (key in data) {
if (key === 'searchterm') {
continue;
}
if (data[key].length < 1) {
continue;
}
@ -176,8 +180,16 @@
$("#selPackages").selectpicker();
$("#search-button").on("click", search);
$("#searchbar").on("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
search(e);
}
});
});
{% if search_result %}
$('#searchbar').val('{{ search_result.searchterm }}')
handleSearchResponse("results", {{ search_result|safe }});
{% endif %}
</script>

View File

@ -1,72 +0,0 @@
{% for obj in available_additional_information.types %}
<div id="table-{{obj.type}}-Div">
<table class="table table-bordered table-hover">
<tbody>
<tr id="{{ obj.type }}GroupRow" style="background-color: rgba(0, 0, 0, 0.08);">
<td><p style="font-size:18px">{{obj.title}}</p></td>
</tr>
<!-- Loop through all AIs and attach the ones without subtype -->
{% for ai in available_additional_information.ais %}
<tr>
{% if obj.type in ai.types and ai.sub_type is not defined %}
<td><span title="">{{ ai.name }}</span></td>
<!-- #TODO -->
{% for c in "1 2 3"|make_list %}
<td class="col-{{ c }}">
{% for form in ai.forms %}
<!-- Build input -->
{% if form.type == 'select' %}
<select class="form-control" name="{{ form.param_name}}">
<option value="">{{ form.placeholder }}</option>
{% for choice in form.choices %}
<option value="{{ choice.value }}">
{{ choice.name }}
</option>
{% endfor %}
</select>
{% else %}
<input type="{{ form.type }}" name="{{ form.param_name }}" class="form-control" placeholder="{{ form.placeholder|safe }}"/>
{% endif %}
{% endfor %}
</td>
{% endfor %}
{% endif %}
{% endfor %}
</tr>
{% for subtype in available_additional_information.subtypes %}
<tr id="{{ subtype.type }}GroupRow" style="background-color: rgba(0, 0, 0, 0.08);">
<td><p style="font-size:18px">{{subtype.title}}</p></td>
</tr>
<!-- Loop through all AIs and attach the ones with the same subtype -->
{% for ai in available_additional_information.ais %}
<tr>
{% if obj.type in ai.types and subtype.type == ai.sub_type %}
<td><span title="">{{ ai.name }}</span></td>
<!-- #TODO -->
{% for c in "1 2 3"|make_list %}
<td class="col-{{ c }}">
{% for form in ai.forms %}
<!-- Build input -->
{% if form.type == 'select' %}
<select class="form-control" name="{{ form.param_name }}">
<option value="">{{ form.placeholder }}</option>
{% for choice in form.choices %}
<option value="{{ choice.value }}">
{{ choice.name }}
</option>
{% endfor %}
</select>
{% else %}
<input type="{{ form.type }}" name="{{ form.param_name }}" class="form-control" placeholder="{{ form.placeholder|safe }}">
{% endif %}
{% endfor %}
</td>
{% endfor %}
{% endif %}
</tr>
{% endfor %}
{% endfor %}
</tbody>
</table>
</div>
{% endfor %}

200
tests/test_copy_objects.py Normal file
View File

@ -0,0 +1,200 @@
import json
from django.test import TestCase
from epdb.logic import PackageManager
from epdb.models import Compound, User, CompoundStructure, Reaction, Rule, MLRelativeReasoning, Pathway
class CopyTest(TestCase):
fixtures = ["test_fixture.cleaned.json"]
def setUp(self):
pass
@classmethod
def setUpClass(cls):
super(CopyTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Source Package', 'No Desc')
cls.AFOXOLANER = Compound.create(
cls.package,
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
name='Afoxolaner',
description='Test compound for copying'
)
cls.FOUR_NITROBENZOIC_ACID = Compound.create(
cls.package,
smiles='[O-][N+](=O)c1ccc(C(=O)[O-])cc1', # Normalized: O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
name='Test Compound',
description='Compound with multiple structures'
)
cls.ETHANOL = Compound.create(
cls.package,
smiles='CCO',
name='Ethanol',
description='Simple alcohol'
)
cls.target_package = PackageManager.create_package(cls.user, 'Target Package', 'No Desc')
cls.reaction_educt = Compound.create(
cls.package,
smiles='C(CCl)Cl',
name='1,2-Dichloroethane',
description='Eawag BBD compound c0001'
).default_structure
cls.reaction_product = Compound.create(
cls.package,
smiles='C(CO)Cl',
name='2-Chloroethanol',
description='Eawag BBD compound c0005'
).default_structure
cls.REACTION = Reaction.create(
package=cls.package,
name='Eawag BBD reaction r0001',
educts=[cls.reaction_educt],
products=[cls.reaction_product],
multi_step=False
)
@classmethod
def tearDownClass(cls):
pass
def tearDown(self):
pass
def test_compound_copy_basic(self):
"""Test basic compound copying functionality"""
mapping = dict()
copied_compound = self.AFOXOLANER.copy(self.target_package, mapping)
self.assertNotEqual(self.AFOXOLANER.uuid, copied_compound.uuid)
self.assertEqual(self.AFOXOLANER.name, copied_compound.name)
self.assertEqual(self.AFOXOLANER.description, copied_compound.description)
self.assertEqual(copied_compound.package, self.target_package)
self.assertEqual(self.AFOXOLANER.package, self.package)
self.assertEqual(self.AFOXOLANER.default_structure.smiles, copied_compound.default_structure.smiles)
def test_compound_copy_with_multiple_structures(self):
"""Test copying a compound with multiple structures"""
original_structure_count = self.FOUR_NITROBENZOIC_ACID.structures.count()
mapping = dict()
# Copy the compound
copied_compound = self.FOUR_NITROBENZOIC_ACID.copy(self.target_package, mapping)
# Verify all structures were copied
self.assertEqual(copied_compound.structures.count(), original_structure_count)
# Verify default_structure is properly set
self.assertIsNotNone(copied_compound.default_structure)
self.assertEqual(
copied_compound.default_structure.smiles,
self.FOUR_NITROBENZOIC_ACID.default_structure.smiles
)
def test_compound_copy_preserves_aliases(self):
"""Test that compound copying preserves aliases"""
# Create a compound and add aliases
original_compound = self.ETHANOL
# Add aliases if the method exists
if hasattr(original_compound, 'add_alias'):
original_compound.add_alias('Ethyl alcohol')
original_compound.add_alias('Grain alcohol')
mapping = dict()
copied_compound = original_compound.copy(self.target_package, mapping)
# Verify aliases were copied if they exist
if hasattr(original_compound, 'aliases') and hasattr(copied_compound, 'aliases'):
original_aliases = original_compound.aliases
copied_aliases = copied_compound.aliases
self.assertEqual(original_aliases, copied_aliases)
def test_compound_copy_preserves_external_identifiers(self):
"""Test that external identifiers are preserved during copying"""
original_compound = self.ETHANOL
# Add external identifiers if the methods exist
if hasattr(original_compound, 'add_cas_number'):
original_compound.add_cas_number('64-17-5')
if hasattr(original_compound, 'add_pubchem_compound_id'):
original_compound.add_pubchem_compound_id('702')
mapping = dict()
copied_compound = original_compound.copy(self.target_package, mapping)
# Verify external identifiers were copied
original_ext_ids = original_compound.external_identifiers.all()
copied_ext_ids = copied_compound.external_identifiers.all()
self.assertEqual(original_ext_ids.count(), copied_ext_ids.count())
# Check that identifier values match
original_values = set(ext_id.identifier_value for ext_id in original_ext_ids)
copied_values = set(ext_id.identifier_value for ext_id in copied_ext_ids)
self.assertEqual(original_values, copied_values)
def test_compound_copy_structure_properties(self):
"""Test that structure properties are properly copied"""
original_compound = self.ETHANOL
mapping = dict()
copied_compound = original_compound.copy(self.target_package, mapping)
# Verify structure properties
original_structure = original_compound.default_structure
copied_structure = copied_compound.default_structure
self.assertEqual(original_structure.smiles, copied_structure.smiles)
self.assertEqual(original_structure.canonical_smiles, copied_structure.canonical_smiles)
self.assertEqual(original_structure.inchikey, copied_structure.inchikey)
self.assertEqual(original_structure.normalized_structure, copied_structure.normalized_structure)
# Verify they are different objects
self.assertNotEqual(original_structure.uuid, copied_structure.uuid)
self.assertEqual(copied_structure.compound, copied_compound)
def test_reaction_copy_basic(self):
"""Test basic reaction copying functionality"""
mapping = dict()
copied_reaction = self.REACTION.copy(self.target_package, mapping)
self.assertNotEqual(self.REACTION.uuid, copied_reaction.uuid)
self.assertEqual(self.REACTION.name, copied_reaction.name)
self.assertEqual(self.REACTION.description, copied_reaction.description)
self.assertEqual(self.REACTION.multi_step, copied_reaction.multi_step)
self.assertEqual(copied_reaction.package, self.target_package)
self.assertEqual(self.REACTION.package, self.package)
def test_reaction_copy_structures(self):
"""Test basic reaction copying functionality"""
mapping = dict()
copied_reaction = self.REACTION.copy(self.target_package, mapping)
for orig_educt, copy_educt in zip(self.REACTION.educts.all(), copied_reaction.educts.all()):
self.assertNotEqual(orig_educt.uuid, copy_educt.uuid)
self.assertEqual(orig_educt.name, copy_educt.name)
self.assertEqual(orig_educt.description, copy_educt.description)
self.assertEqual(copy_educt.compound.package, self.target_package)
self.assertEqual(orig_educt.compound.package, self.package)
self.assertEqual(orig_educt.smiles, copy_educt.smiles)
for orig_product, copy_product in zip(self.REACTION.products.all(), copied_reaction.products.all()):
self.assertNotEqual(orig_product.uuid, copy_product.uuid)
self.assertEqual(orig_product.name, copy_product.name)
self.assertEqual(orig_product.description, copy_product.description)
self.assertEqual(copy_product.compound.package, self.target_package)
self.assertEqual(orig_product.compound.package, self.package)
self.assertEqual(orig_product.smiles, copy_product.smiles)

52
tests/test_dataset.py Normal file
View File

@ -0,0 +1,52 @@
from django.test import TestCase
from epdb.logic import PackageManager
from epdb.models import Reaction, Compound, User, Rule
from utilities.ml import Dataset
class DatasetTest(TestCase):
fixtures = ["test_fixture.cleaned.json"]
def setUp(self):
self.cs1 = Compound.create(
self.package,
name='2,6-Dibromohydroquinone',
description='http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1/compound/d6435251-1a54-4327-b4b1-fd6e9a8f4dc9/structure/d8a0225c-dbb5-4e6c-a642-730081c09c5b',
smiles='C1=C(C(=C(C=C1O)Br)O)Br',
).default_structure
self.cs2 = Compound.create(
self.package,
smiles='O=C(O)CC(=O)/C=C(/Br)C(=O)O',
).default_structure
self.rule1 = Rule.create(
rule_type='SimpleAmbitRule',
package=self.package,
smirks='[#8:8]([H])-[c:4]1[c:3]([H])[c:2](-[#1,#17,#35:9])[c:1](-[#8:7]([H]))[c:6](-[#1,#17,#35])[c:5]([H])1>>[#8-]-[#6:6](=O)-[#6:5]-[#6:4](=[O:8])\[#6:3]=[#6:2](\[#1,#17,#35:9])-[#6:1](-[#8-])=[O:7]',
description='http://localhost:8000/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1/simple-ambit-rule/f6a56c0f-a4a0-4ee3-b006-d765b4767cf6'
)
self.reaction1 = Reaction.create(
package=self.package,
educts=[self.cs1],
products=[self.cs2],
rules=[self.rule1],
multi_step=False
)
@classmethod
def setUpClass(cls):
super(DatasetGeneratorTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
def test_smoke(self):
reactions = [r for r in Reaction.objects.filter(package=self.package)]
applicable_rules = [self.rule1]
ds = Dataset.generate_dataset(reactions, applicable_rules)
self.assertEqual(len(ds.y()), 1)
self.assertEqual(sum(ds.y()[0]), 1)

View File

@ -1,111 +0,0 @@
from django.test import TestCase
from epdb.models import ParallelRule
from utilities.ml import Compound, Reaction, DatasetGenerator
class CompoundTest(TestCase):
def setUp(self):
self.c1 = Compound(smiles="CCN(CC)C(=O)C1=CC(=CC=C1)C", uuid='c1')
self.c2 = Compound(smiles="CCN(CC)C(=O)C1=CC(=CC=C1)C", uuid='c2')
def test_compound_eq_ignores_uuid(self):
self.assertEqual(self.c1, self.c2)
class ReactionTest(TestCase):
def setUp(self):
self.c1 = Compound(smiles="CCN(CC)C(=O)C1=CC(=CC=C1)C")
self.c2 = Compound(smiles="CCN(CCO)C(=O)C1=CC(C)=CC=C1")
# self.r1 = Rule(uuid="bt0334")
# c1 --r1--> c2
self.c3_1 = Compound(smiles="CCNC(=O)C1=CC(C)=CC=C1")
self.c3_2 = Compound(smiles="CC=O")
# self.r2 = Rule(uuid="bt0243")
# c1 --r2--> c3_1, c3_2
def test_reaction_equality_ignores_uuid(self):
r1 = Reaction([self.c1], [self.c2], self.r1, uuid="abc")
r2 = Reaction([self.c1], [self.c2], self.r1, uuid="xyz")
self.assertEqual(r1, r2)
def test_reaction_inequality_on_data_change(self):
r1 = Reaction([self.c1], [self.c2], self.r1)
r2 = Reaction([self.c1], [self.c3_1], self.r1)
self.assertNotEqual(r1, r2)
def test_reaction_is_hashable(self):
r = Reaction([self.c1], [self.c2], self.r1)
reactions = {r}
self.assertIn(Reaction([self.c1], [self.c2], self.r1), reactions)
def test_rule_is_optional(self):
r = Reaction([self.c1], [self.c2])
self.assertIsNone(r.rule)
def test_uuid_is_optional(self):
r = Reaction([self.c1], [self.c2], self.r1)
self.assertIsNone(r.uuid)
def test_repr_includes_uuid(self):
r = Reaction([self.c1], [self.c2], self.r1, uuid="abc")
self.assertIn("abc", repr(r))
def test_reaction_equality_with_multiple_compounds_different_ordering(self):
r1 = Reaction([self.c1], [self.c3_1, self.c3_2], self.r2)
r2 = Reaction([self.c1], [self.c3_2, self.c3_1], self.r2)
self.assertEqual(r1, r2, "Reaction equality should not rely on list order")
class RuleTest(TestCase):
def setUp(self):
pass
# self.r1 = Rule(uuid="bt0334")
# self.r2 = Rule(uuid="bt0243")
class DatasetGeneratorTest(TestCase):
fixtures = ['bootstrap.json']
def setUp(self):
self.c1 = Compound(smiles="CCN(CC)C(=O)C1=CC(=CC=C1)C")
self.c2 = Compound(smiles="CCN(CCO)C(=O)C1=CC(C)=CC=C1")
self.c3_1 = Compound(smiles="CCNC(=O)C1=CC(C)=CC=C1")
self.c3_2 = Compound(smiles="CC=O")
# self.r1 = Rule(uuid="bt0334") # trig
# self.r2 = Rule(uuid="bt0243") # trig
# self.r3 = Rule(uuid="bt0003") # no trig
self.reaction1 = Reaction([self.c1], [self.c2], self.r3)
self.reaction2 = Reaction([self.c1], [self.c3_1, self.c3_2], self.r2)
def test_test(self):
compounds = [
self.c1,
self.c2,
self.c3_1,
self.c3_2,
]
reactions = [
self.reaction1,
self.reaction2,
]
applicable_rules = [
# Rule('bt0334', ParallelRule.objects.get(name='bt0334')),
# Rule('bt0243', ParallelRule.objects.get(name='bt0243')),
# Rule('bt0003', ParallelRule.objects.get(name='bt0003')),
]
ds = DatasetGenerator.generate_dataset(compounds, reactions, applicable_rules)
self.assertIsNotNone(ds)

55
tests/test_model.py Normal file
View File

@ -0,0 +1,55 @@
import json
from django.test import TestCase
from epdb.logic import PackageManager
from epdb.models import Compound, User, CompoundStructure, Reaction, Rule, MLRelativeReasoning
class ModelTest(TestCase):
fixtures = ["test_fixture.cleaned.json"]
def setUp(self):
pass
@classmethod
def setUpClass(cls):
super(ModelTest, cls).setUpClass()
cls.user = User.objects.get(username='anonymous')
cls.package = PackageManager.create_package(cls.user, 'Anon Test Package', 'No Desc')
bbd_data = json.load(open('fixtures/packages/2025-07-18/EAWAG-BBD.json'))
cls.BBD = PackageManager.import_package(bbd_data, cls.user)
@classmethod
def tearDownClass(cls):
pass
def tearDown(self):
pass
def test_smoke(self):
threshold = float(0.5)
# get Package objects from urls
rule_package_objs = [self.BBD]
data_package_objs = [self.BBD]
eval_packages_objs = []
mod = MLRelativeReasoning.create(
self.package,
rule_package_objs,
data_package_objs,
eval_packages_objs,
threshold,
'ECC - BBD - 0.5',
'Created MLRelativeReasoning in Testcase',
)
ds = mod.load_dataset()
mod.build_model()
print("Model built!")
mod.evaluate_model()
print("Model Evaluated")
results = mod.predict('CCN(CC)C(=O)C1=CC(=CC=C1)C')
print(results)

View File

@ -19,9 +19,8 @@ class RuleApplicationTest(TestCase):
@classmethod
def tearDownClass(cls):
from collections import Counter
# print(Counter(cls.error_smiles))
pass
print(f"\nTotal Errors across Rules {len(cls.error_smiles)}")
# print(cls.error_smiles)
def tearDown(self):
print(f"\nTotal errors {self.total_errors}")
@ -36,7 +35,7 @@ class RuleApplicationTest(TestCase):
for comp, ambit_prod in zip(bt_rule['compounds'], bt_rule['products']):
smi = comp['smiles']
products = FormatConverter.apply(smi, smirks, preprocess_smiles=True, bracketize=False)
products = FormatConverter.apply(smi, smirks)
all_rdkit_prods = []
for ps in products:
@ -53,15 +52,15 @@ class RuleApplicationTest(TestCase):
# TODO mode "intersection"
# partial_res = (len(set(ambit_smiles).intersection(set(rdkit_smiles))) > 0) or (len(ambit_smiles) == 0)
# FAILED (failures=42)
# FAILED (failures=33)
# TODO mode = "full ambit"
# partial_res = len(set(ambit_smiles).intersection(set(rdkit_smiles))) == len(ambit_smiles)
# FAILED (failures=52)
# FAILED (failures=44)
# TODO mode = "equality"
partial_res = set(ambit_smiles) == set(rdkit_smiles)
# FAILED (failures=71)
# FAILED (failures=64)
if len(ambit_smiles) and not partial_res:
print(f"""

View File

@ -8,10 +8,12 @@ from indigo import Indigo, IndigoException, IndigoObject
from indigo.renderer import IndigoRenderer
from rdkit import Chem
from rdkit import RDLogger
from rdkit.Chem import MACCSkeys
from rdkit.Chem import MACCSkeys, Descriptors
from rdkit.Chem import rdChemReactions
from rdkit.Chem.Draw import rdMolDraw2D
from rdkit.Chem.MolStandardize import rdMolStandardize
from rdkit.Chem.rdmolops import GetMolFrags
from rdkit.Contrib.IFG import ifg
logger = logging.getLogger(__name__)
RDLogger.DisableLog('rdApp.*')
@ -65,6 +67,18 @@ class PredictionResult(object):
class FormatConverter(object):
@staticmethod
def mass(smiles):
return Descriptors.MolWt(FormatConverter.from_smiles(smiles))
@staticmethod
def charge(smiles):
return Chem.GetFormalCharge(FormatConverter.from_smiles(smiles))
@staticmethod
def formula(smiles):
return Chem.rdMolDescriptors.CalcMolFormula(FormatConverter.from_smiles(smiles))
@staticmethod
def from_smiles(smiles):
return Chem.MolFromSmiles(smiles)
@ -77,6 +91,10 @@ class FormatConverter(object):
def InChIKey(smiles):
return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles))
@staticmethod
def InChI(smiles):
return Chem.MolToInchi(FormatConverter.from_smiles(smiles))
@staticmethod
def canonicalize(smiles: str):
return FormatConverter.to_smiles(FormatConverter.from_smiles(smiles), canonical=True)
@ -87,6 +105,21 @@ class FormatConverter(object):
bitvec = MACCSkeys.GenMACCSKeys(mol)
return bitvec.ToList()
@staticmethod
def get_functional_groups(smiles: str) -> List[str]:
res = list()
try:
m = Chem.MolFromSmiles(smiles)
fgs = ifg.identify_functional_groups(m)
for fg in fgs:
# TODO atoms or type?
res.append(fg.atoms)
except AttributeError:
logger.debug(f"Could not get functional groups for {smiles}")
return res
@staticmethod
def to_svg(smiles, mol_size=(200, 150), kekulize=True):
mol = FormatConverter.from_smiles(smiles)
@ -131,6 +164,24 @@ class FormatConverter(object):
# TODO call to AMBIT Service
return smiles
@staticmethod
def ep_standardize(smiles):
change = True
while change:
change = False
for standardizer in MATCH_STANDARDIZER:
tmp_smiles = standardizer.standardize(smiles)
if tmp_smiles != smiles:
print(f"change {smiles} to {tmp_smiles}")
change = True
smiles = tmp_smiles
if change is False:
print(f"nothing changed")
return smiles
@staticmethod
def standardize(smiles):
# Taken from https://bitsilla.com/blog/2021/06/standardizing-a-molecule-using-rdkit/
@ -180,54 +231,6 @@ class FormatConverter(object):
atom.UpdatePropertyCache()
return mol
# @staticmethod
# def apply(smiles, smirks, preprocess_smiles=True, bracketize=False, standardize=True):
# logger.debug(f'Applying {smirks} on {smiles}')
#
# if bracketize:
# smirks = smirks.split('>>')[0] + ">>(" + smirks.split('>>')[1] + ")"
#
# res = set()
# try:
# rxn = rdChemReactions.ReactionFromSmarts(smirks)
# mol = Chem.MolFromSmiles(smiles)
#
# # Inplace
# if preprocess_smiles:
# Chem.SanitizeMol(mol)
# mol = Chem.AddHs(mol)
#
# # apply!
# reacts = rxn.RunReactants((mol,))
# if len(reacts):
# # Sanitize mols
# for product_set in reacts:
# prod_set = list()
# for product in product_set:
# # Fixes
# # [2025-01-30 23:00:50] ERROR chem - Sanitizing and converting failed:
# # non-ring atom 3 marked aromatic
# # But does not improve overall performance
# #
# # for a in product.GetAtoms():
# # if (not a.IsInRing()) and a.GetIsAromatic():
# # a.SetIsAromatic(False)
# # for b in product.GetBonds():
# # if (not b.IsInRing()) and b.GetIsAromatic():
# # b.SetIsAromatic(False)
#
# try:
# Chem.SanitizeMol(product)
# prod_set.append(FormatConverter.standardize(Chem.MolToSmiles(product)))
# except ValueError as e:
# logger.error(f'Sanitizing and converting failed:\n{e}')
# continue
# res.add(tuple(list(set(prod_set))))
# except Exception as e:
# logger.error(f'Applying {smirks} on {smiles} failed:\n{e}')
#
# return list(res)
@staticmethod
def is_valid_smirks(smirks: str) -> bool:
try:
@ -237,7 +240,7 @@ class FormatConverter(object):
return False
@staticmethod
def apply(smiles: str, smirks: str, preprocess_smiles: bool = True, bracketize: bool = False,
def apply(smiles: str, smirks: str, preprocess_smiles: bool = True, bracketize: bool = True,
standardize: bool = True, kekulize: bool = True) -> List['ProductSet']:
logger.debug(f'Applying {smirks} on {smiles}')
@ -266,8 +269,10 @@ class FormatConverter(object):
for product in product_set:
try:
Chem.SanitizeMol(product)
product = FormatConverter.standardize(Chem.MolToSmiles(product))
product = GetMolFrags(product, asMols=True)
for p in product:
p = FormatConverter.standardize(Chem.MolToSmiles(p))
prods.append(p)
# if kekulize:
# # from rdkit.Chem import MolStandardize
@ -292,13 +297,12 @@ class FormatConverter(object):
# # bond.SetIsAromatic(False)
# Chem.Kekulize(product)
prods.append(product)
except ValueError as e:
logger.error(f'Sanitizing and converting failed:\n{e}')
continue
# TODO doc!
if len(prods) and len(prods) == len(product_set):
if len(prods):
ps = ProductSet(prods)
pss.add(ps)
@ -651,7 +655,7 @@ class IndigoUtils(object):
environment.add(mappedAtom.index())
for k, v in functional_groups.items():
try:
sanitized = IndigoUtils.sanitize_functional_group(k)
query = indigo.loadSmarts(sanitized)
@ -666,6 +670,9 @@ class IndigoUtils(object):
counts[mappedAtom.index()] = max(v, counts[mappedAtom.index()])
except IndigoException as e:
logger.debug(f'Colorizing failed due to {e}')
for k, v in counts.items():
if is_reaction:
color = "128, 0, 128"
@ -691,6 +698,9 @@ class IndigoUtils(object):
i.setOption("render-image-size", width, height)
i.setOption("render-bond-line-width", 2.0)
if '~' in mol_data:
mol = i.loadSmarts(mol_data)
else:
mol = i.loadMolecule(mol_data)
if len(functional_groups.keys()) > 0:

168
utilities/misc.py Normal file
View File

@ -0,0 +1,168 @@
import html
import logging
from collections import defaultdict
from enum import Enum
from types import NoneType
from typing import Dict, List, Any
from envipy_additional_information import Interval, EnviPyModel
from envipy_additional_information import NAME_MAPPING
from pydantic import BaseModel, HttpUrl
logger = logging.getLogger(__name__)
class HTMLGenerator:
registry = {x.__name__: x for x in NAME_MAPPING.values()}
@staticmethod
def generate_html(additional_information: 'EnviPyModel', prefix='') -> str:
from typing import get_origin, get_args, Union
if isinstance(additional_information, type):
clz_name = additional_information.__name__
else:
clz_name = additional_information.__class__.__name__
widget = f'<h4>{clz_name}</h4>'
if hasattr(additional_information, 'uuid'):
uuid = additional_information.uuid
widget += f'<input type="hidden" name="{clz_name}__{prefix}__uuid" value="{uuid}">'
for name, field in additional_information.model_fields.items():
value = getattr(additional_information, name, None)
full_name = f"{clz_name}__{prefix}__{name}"
annotation = field.annotation
base_type = get_origin(annotation) or annotation
# Optional[Interval[float]] alias for Union[X, None]
if base_type is Union:
for arg in get_args(annotation):
if arg is not NoneType:
field_type = arg
break
else:
field_type = base_type
is_interval_float = (
field_type == Interval[float] or
str(field_type) == str(Interval[float]) or
'Interval[float]' in str(field_type)
)
if is_interval_float:
widget += f"""
<div class="form-group row">
<div class="col-md-6">
<label for="{full_name}__start">{' '.join([x.capitalize() for x in name.split('_')])} Start</label>
<input type="number" class="form-control" id="{full_name}__start" name="{full_name}__start" value="{value.start if value else ''}">
</div>
<div class="col-md-6">
<label for="{full_name}__end">{' '.join([x.capitalize() for x in name.split('_')])} End</label>
<input type="number" class="form-control" id="{full_name}__end" name="{full_name}__end" value="{value.end if value else ''}">
</div>
</div>
"""
elif issubclass(field_type, Enum):
options: str = ''
for e in field_type:
options += f'<option value="{e.value}" {"selected" if e == value else ""}>{html.escape(e.name)}</option>'
widget += f"""
<div class="form-group">
<label for="{full_name}">{' '.join([x.capitalize() for x in name.split('_')])}</label>
<select class="form-control" id="{full_name}" name="{full_name}">
<option value="" disabled selected>Select {' '.join([x.capitalize() for x in name.split('_')])}</option>
{options}
</select>
</div>
"""
else:
if field_type == str or field_type == HttpUrl:
input_type = 'text'
elif field_type == float or field_type == int:
input_type = 'number'
elif field_type == bool:
input_type = 'checkbox'
else:
raise ValueError(f"Could not parse field type {field_type} for {name}")
value_to_use = value if value and field_type != bool else ''
widget += f"""
<div class="form-group">
<label for="{full_name}">{' '.join([x.capitalize() for x in name.split('_')])}</label>
<input type="{input_type}" class="form-control" id="{full_name}" name="{full_name}" value="{value_to_use}" {"checked" if value and field_type == bool else ""}>
</div>
"""
return widget + "<hr>"
@staticmethod
def build_models(params) -> Dict[str, List['EnviPyModel']]:
def has_non_none(d):
"""
Recursively checks if any value in a (possibly nested) dict is not None.
"""
for value in d.values():
if isinstance(value, dict):
if has_non_none(value): # recursive check
return True
elif value is not None:
return True
return False
"""
Build Pydantic model instances from flattened HTML parameters.
Args:
params: dict of {param_name: value}, e.g. form data
model_registry: mapping of class names (strings) to Pydantic model classes
Returns:
dict: {ClassName: [list of model instances]}
"""
grouped: Dict[str, Dict[str, Dict[str, Any]]] = {}
# Step 1: group fields by ClassName and Number
for key, value in params.items():
if value == '':
value = None
parts = key.split("__")
if len(parts) < 3:
continue # skip invalid keys
class_name, number, *field_parts = parts
grouped.setdefault(class_name, {}).setdefault(number, {})
# handle nested fields like interval__start
target = grouped[class_name][number]
current = target
for p in field_parts[:-1]:
current = current.setdefault(p, {})
current[field_parts[-1]] = value
# Step 2: instantiate Pydantic models
instances: Dict[str, List[BaseModel]] = defaultdict(list)
for class_name, number_dict in grouped.items():
model_cls = HTMLGenerator.registry.get(class_name)
if not model_cls:
logger.info(f"Could not find model class for {class_name}")
continue
for number, fields in number_dict.items():
if not has_non_none(fields):
print(f"Skipping empty {class_name} {number} {fields}")
continue
uuid = fields.pop('uuid', None)
instance = model_cls(**fields)
if uuid:
instance.__dict__['uuid'] = uuid
instances[class_name].append(instance)
return instances

View File

@ -1,46 +1,29 @@
from __future__ import annotations
import dataclasses
import logging
from abc import ABC, abstractmethod
from collections import defaultdict
from datetime import datetime
from typing import List, Optional
from typing import List, Dict, Set, Tuple
import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score
from sklearn.multioutput import ClassifierChain
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
# @dataclasses.dataclass
# class Feature:
# name: str
# value: float
#
#
#
# class Row:
# def __init__(self, compound_uuid: str, compound_smiles: str, descriptors: List[int]):
# self.data = {}
#
#
#
# class DataSet(object):
#
# def __init__(self):
# self.rows: List[Row] = []
#
# def add_row(self, row: Row):
# pass
logger = logging.getLogger(__name__)
from dataclasses import dataclass, field
from utilities.chem import FormatConverter
from utilities.chem import FormatConverter, PredictionResult
@dataclass
class Compound:
class SCompound:
smiles: str
uuid: str = field(default=None, compare=False, hash=False)
@ -53,10 +36,10 @@ class Compound:
@dataclass
class Reaction:
educts: List[Compound]
products: List[Compound]
rule_uuid: str = field(default=None, compare=False, hash=False)
class SReaction:
educts: List[SCompound]
products: List[SCompound]
rule_uuid: SRule = field(default=None, compare=False, hash=False)
reaction_uuid: str = field(default=None, compare=False, hash=False)
def __hash__(self):
@ -68,7 +51,7 @@ class Reaction:
return self._hash
def __eq__(self, other):
if not isinstance(other, Reaction):
if not isinstance(other, SReaction):
return NotImplemented
return (
sorted(self.educts, key=lambda x: x.smiles) == sorted(other.educts, key=lambda x: x.smiles) and
@ -76,69 +59,296 @@ class Reaction:
)
class Dataset(object):
@dataclass
class SRule(ABC):
def __init__(self, headers=List['str'], data=List[List[str|int|float]]):
self.headers = headers
@abstractmethod
def apply(self):
pass
@dataclass
class SSimpleRule:
pass
@dataclass
class SParallelRule:
pass
class Dataset:
def __init__(self, columns: List[str], num_labels: int, data: List[List[str | int | float]] = None):
self.columns: List[str] = columns
self.num_labels: int = num_labels
if data is None:
self.data: List[List[str | int | float]] = list()
else:
self.data = data
self.num_features: int = len(columns) - self.num_labels
self._struct_features: Tuple[int, int] = self._block_indices('feature_')
self._triggered: Tuple[int, int] = self._block_indices('trig_')
self._observed: Tuple[int, int] = self._block_indices('obs_')
def features(self):
pass
def _block_indices(self, prefix) -> Tuple[int, int]:
indices: List[int] = []
for i, feature in enumerate(self.columns):
if feature.startswith(prefix):
indices.append(i)
def labels(self):
pass
return min(indices), max(indices)
def to_json(self):
pass
def structure_id(self):
return self.data[0][0]
def to_csv(self):
pass
def add_row(self, row: List[str | int | float]):
if len(self.columns) != len(row):
raise ValueError(f"Header and Data are not aligned {len(self.columns)} vs. {len(row)}")
self.data.append(row)
def to_arff(self):
pass
def times_triggered(self, rule_uuid) -> int:
idx = self.columns.index(f'trig_{rule_uuid}')
times_triggered = 0
for row in self.data:
if row[idx] == 1:
times_triggered += 1
return times_triggered
def struct_features(self) -> Tuple[int, int]:
return self._struct_features
def triggered(self) -> Tuple[int, int]:
return self._triggered
def observed(self) -> Tuple[int, int]:
return self._observed
def at(self, position: int) -> Dataset:
return Dataset(self.columns, self.num_labels, [self.data[position]])
def limit(self, limit: int) -> Dataset:
return Dataset(self.columns, self.num_labels, self.data[:limit])
def __iter__(self):
return (self.at(i) for i, _ in enumerate(self.data))
def classification_dataset(self, structures: List[str | 'CompoundStructure'], applicable_rules: List['Rule']) -> Tuple[Dataset, List[List[PredictionResult]]]:
classify_data = []
classify_products = []
for struct in structures:
class DatasetGenerator(object):
if isinstance(struct, str):
struct_id = None
struct_smiles = struct
else:
struct_id = str(struct.uuid)
struct_smiles = struct.smiles
features = FormatConverter.maccs(struct_smiles)
trig = []
prods = []
for rule in applicable_rules:
products = rule.apply(struct_smiles)
if len(products):
trig.append(1)
prods.append(products)
else:
trig.append(0)
prods.append([])
classify_data.append([struct_id] + features + trig + ([-1] * len(trig)))
classify_products.append(prods)
return Dataset(columns=self.columns, num_labels=self.num_labels, data=classify_data), classify_products
@staticmethod
def generate_dataset(compounds: List[Compound], reactions: List[Reaction], applicable_rules: 'Rule',
compounds_to_exclude: Optional[Compound] = None, educts_only: bool = False) -> Dataset:
def generate_dataset(reactions: List['Reaction'], applicable_rules: List['Rule'], educts_only: bool = True) -> Dataset:
_structures = set()
rows = []
if educts_only:
compounds = set()
for r in reactions:
for e in r.educts:
compounds.add(e)
compounds = list(compounds)
for e in r.educts.all():
_structures.add(e)
total = len(compounds)
for i, c in enumerate(compounds):
row = []
print(f"{i + 1}/{total} - {c.smiles}")
for r in applicable_rules:
product_sets = r.rule.apply(c.smiles)
if not educts_only:
for e in r.products:
_structures.add(e)
compounds = sorted(_structures, key=lambda x: x.url)
triggered: Dict[str, Set[str]] = defaultdict(set)
observed: Set[str] = set()
# Apply rules on collected compounds and store tps
for i, comp in enumerate(compounds):
logger.debug(f"{i + 1}/{len(compounds)}...")
for rule in applicable_rules:
product_sets = rule.apply(comp.smiles)
if len(product_sets) == 0:
row.append([])
continue
#triggered.add(f"{r.uuid} + {c.uuid}")
reacts = set()
for ps in product_sets:
products = []
for p in ps:
products.append(Compound(FormatConverter.standardize(p)))
key = f"{rule.uuid} + {comp.uuid}"
reacts.add(Reaction([c], products, r))
row.append(list(reacts))
if key in triggered:
logger.info(f"{key} already present. Duplicate reaction?")
rows.append(row)
for prod_set in product_sets:
for smi in prod_set:
return rows
try:
smi = FormatConverter.standardize(smi)
except Exception:
# :shrug:
logger.debug(f'Standardizing SMILES failed for {smi}')
pass
triggered[key].add(smi)
for i, r in enumerate(reactions):
logger.debug(f"{i + 1}/{len(reactions)}...")
if len(r.educts.all()) != 1:
logger.debug(f"Skipping {r.url} as it has {len(r.educts.all())} substrates!")
continue
for comp in r.educts.all():
for rule in applicable_rules:
key = f"{rule.uuid} + {comp.uuid}"
if key not in triggered:
continue
# standardize products from reactions for comparison
standardized_products = []
for cs in r.products.all():
smi = cs.smiles
try:
smi = FormatConverter.standardize(smi)
except Exception as e:
# :shrug:
logger.debug(f'Standardizing SMILES failed for {smi}')
pass
standardized_products.append(smi)
if len(set(standardized_products).difference(triggered[key])) == 0:
observed.add(key)
else:
pass
ds = None
for i, comp in enumerate(compounds):
# Features
feat = FormatConverter.maccs(comp.smiles)
trig = []
obs = []
for rule in applicable_rules:
key = f"{rule.uuid} + {comp.uuid}"
# Check triggered
if key in triggered:
trig.append(1)
else:
trig.append(0)
# Check obs
if key in observed:
obs.append(1)
elif key not in triggered:
obs.append(None)
else:
obs.append(0)
if ds is None:
header = ['structure_id'] + \
[f'feature_{i}' for i, _ in enumerate(feat)] \
+ [f'trig_{r.uuid}' for r in applicable_rules] \
+ [f'obs_{r.uuid}' for r in applicable_rules]
ds = Dataset(header, len(applicable_rules))
ds.add_row([str(comp.uuid)] + feat + trig + obs)
return ds
def X(self, exclude_id_col=True, na_replacement=0):
res = self.__getitem__((slice(None), slice(1 if exclude_id_col else 0, len(self.columns) - self.num_labels)))
if na_replacement is not None:
res = [[x if x is not None else na_replacement for x in row] for row in res]
return res
def y(self, na_replacement=0):
res = self.__getitem__((slice(None), slice(len(self.columns) - self.num_labels, None)))
if na_replacement is not None:
res = [[x if x is not None else na_replacement for x in row] for row in res]
return res
def __getitem__(self, key):
if not isinstance(key, tuple):
raise TypeError("Dataset must be indexed with dataset[rows, columns]")
row_key, col_key = key
# Normalize rows
if isinstance(row_key, int):
rows = [self.data[row_key]]
else:
rows = self.data[row_key]
# Normalize columns
if isinstance(col_key, int):
res = [row[col_key] for row in rows]
else:
res = [[row[i] for i in range(*col_key.indices(len(row)))] if isinstance(col_key, slice)
else [row[i] for i in col_key] for row in rows]
return res
def save(self, path: 'Path'):
import pickle
with open(path, "wb") as fh:
pickle.dump(self, fh)
@staticmethod
def load(path: 'Path'):
import pickle
return pickle.load(open(path, "rb"))
def to_arff(self, path: 'Path'):
arff = f"@relation 'enviPy-dataset: -C {self.num_labels}'\n"
arff += "\n"
for c in self.columns[-self.num_labels:] + self.columns[:self.num_features]:
if c == 'structure_id':
arff += f"@attribute {c} string\n"
else:
arff += f"@attribute {c} {{0,1}}\n"
arff += f"\n@data\n"
for d in self.data:
ys = ','.join([str(v if v is not None else '?') for v in d[-self.num_labels:]])
xs = ','.join([str(v if v is not None else '?') for v in d[:self.num_features]])
arff += f'{ys},{xs}\n'
with open(path, "w") as fh:
fh.write(arff)
fh.flush()
def __repr__(self):
return f"<Dataset #rows={len(self.data)} #cols={len(self.columns)} #labels={self.num_labels}>"
class SparseLabelECC(BaseEstimator, ClassifierMixin):
@ -166,8 +376,7 @@ class SparseLabelECC(BaseEstimator, ClassifierMixin):
self.keep_columns_.append(col)
y_reduced = y[:, self.keep_columns_]
self.chains_ = [ClassifierChain(self.base_clf, order='random', random_state=i)
for i in range(self.num_chains)]
self.chains_ = [ClassifierChain(self.base_clf) for i in range(self.num_chains)]
for i, chain in enumerate(self.chains_):
print(f"{datetime.now()} fitting {i + 1}/{self.num_chains}")
@ -208,26 +417,169 @@ class SparseLabelECC(BaseEstimator, ClassifierMixin):
return accuracy_score(y_true, y_pred, sample_weight=sample_weight)
class ApplicabilityDomain(PCA):
def __init__(self, n_components=5):
super().__init__(n_components=n_components)
import copy
import numpy as np
from sklearn.dummy import DummyClassifier
from sklearn.tree import DecisionTreeClassifier
class BinaryRelevance:
def __init__(self, baseline_clf):
self.clf = baseline_clf
self.classifiers = None
def fit(self, X, Y):
if self.classifiers is None:
self.classifiers = []
for l in range(len(Y[0])):
X_l = X[~np.isnan(Y[:, l])]
Y_l = (Y[~np.isnan(Y[:, l]), l])
if len(X_l) == 0: # all labels are nan -> predict 0
clf = DummyClassifier(strategy='constant', constant=0)
clf.fit([X[0]], [0])
self.classifiers.append(clf)
continue
elif len(np.unique(Y_l)) == 1: # only one class -> predict that class
clf = DummyClassifier(strategy='most_frequent')
else:
clf = copy.deepcopy(self.clf)
clf.fit(X_l, Y_l)
self.classifiers.append(clf)
def predict(self, X):
labels = []
for clf in self.classifiers:
labels.append(clf.predict(X))
return np.column_stack(labels)
def predict_proba(self, X):
labels = np.empty((len(X), 0))
for clf in self.classifiers:
pred = clf.predict_proba(X)
if pred.shape[1] > 1:
pred = pred[:, 1]
else:
pred = pred * clf.predict([X[0]])[0]
labels = np.column_stack((labels, pred))
return labels
class MissingValuesClassifierChain:
def __init__(self, base_clf):
self.base_clf = base_clf
self.permutation = None
self.classifiers = None
def fit(self, X, Y):
X = np.array(X)
Y = np.array(Y)
if self.permutation is None:
self.permutation = np.random.permutation(len(Y[0]))
Y = Y[:, self.permutation]
if self.classifiers is None:
self.classifiers = []
for p in range(len(self.permutation)):
X_p = X[~np.isnan(Y[:, p])]
Y_p = Y[~np.isnan(Y[:, p]), p]
if len(X_p) == 0: # all labels are nan -> predict 0
clf = DummyClassifier(strategy='constant', constant=0)
self.classifiers.append(clf.fit([X[0]], [0]))
elif len(np.unique(Y_p)) == 1: # only one class -> predict that class
clf = DummyClassifier(strategy='most_frequent')
self.classifiers.append(clf.fit(X_p, Y_p))
else:
clf = copy.deepcopy(self.base_clf)
self.classifiers.append(clf.fit(X_p, Y_p))
newcol = Y[:, p]
pred = clf.predict(X)
newcol[np.isnan(newcol)] = pred[np.isnan(newcol)] # fill in missing values with clf predictions
X = np.column_stack((X, newcol))
def predict(self, X):
labels = np.empty((len(X), 0))
for clf in self.classifiers:
pred = clf.predict(np.column_stack((X, labels)))
labels = np.column_stack((labels, pred))
return labels[:, np.argsort(self.permutation)]
def predict_proba(self, X):
labels = np.empty((len(X), 0))
for clf in self.classifiers:
pred = clf.predict_proba(np.column_stack((X, np.round(labels))))
if pred.shape[1] > 1:
pred = pred[:, 1]
else:
pred = pred * clf.predict(np.column_stack(([X[0]], np.round([labels[0]]))))[0]
labels = np.column_stack((labels, pred))
return labels[:, np.argsort(self.permutation)]
class EnsembleClassifierChain:
def __init__(self, base_clf, num_chains=10):
self.base_clf = base_clf
self.num_chains = num_chains
self.num_labels = None
self.classifiers = None
def fit(self, X, Y):
if self.classifiers is None:
self.classifiers = []
if self.num_labels is None:
self.num_labels = len(Y[0])
for p in range(self.num_chains):
print(f"{datetime.now()} fitting {p + 1}/{self.num_chains}")
clf = MissingValuesClassifierChain(self.base_clf)
clf.fit(X, Y)
self.classifiers.append(clf)
def predict(self, X):
labels = np.zeros((len(X), self.num_labels))
for clf in self.classifiers:
labels += clf.predict(X)
return np.round(labels / self.num_chains)
def predict_proba(self, X):
labels = np.zeros((len(X), self.num_labels))
for clf in self.classifiers:
labels += clf.predict_proba(X)
return labels / self.num_chains
class ApplicabilityDomainPCA(PCA):
def __init__(self, num_neighbours: int = 5):
super().__init__(n_components=num_neighbours)
self.scaler = StandardScaler()
self.num_neighbours = num_neighbours
self.min_vals = None
self.max_vals = None
def build(self, X):
def build(self, train_dataset: 'Dataset'):
# transform
X_scaled = self.scaler.fit_transform(X)
X_scaled = self.scaler.fit_transform(train_dataset.X())
# fit pca
X_pca = self.fit_transform(X_scaled)
self.max_vals = np.max(X_pca, axis=0)
self.min_vals = np.min(X_pca, axis=0)
def is_applicable(self, instances):
def __transform(self, instances):
instances_scaled = self.scaler.transform(instances)
instances_pca = self.transform(instances_scaled)
return instances_pca
def is_applicable(self, classify_instances: 'Dataset'):
instances_pca = self.__transform(classify_instances.X())
is_applicable = []
for i, instance in enumerate(instances_pca):
@ -237,3 +589,17 @@ class ApplicabilityDomain(PCA):
is_applicable[i] = False
return is_applicable
def tanimoto_distance(a: List[int], b: List[int]):
if len(a) != len(b):
raise ValueError(f"Lists must be the same length {len(a)} != {len(b)}")
sum_a = sum(a)
sum_b = sum(b)
sum_c = sum(v1 and v2 for v1, v2 in zip(a, b))
if sum_a + sum_b - sum_c == 0:
return 0.0
return 1 - (sum_c / (sum_a + sum_b - sum_c))

156
uv.lock generated
View File

@ -200,6 +200,63 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 },
]
[[package]]
name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 },
{ url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 },
{ url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 },
{ url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 },
{ url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 },
{ url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 },
{ url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 },
{ url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 },
{ url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 },
{ url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 },
{ url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 },
{ url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 },
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 },
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 },
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 },
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.2"
@ -319,6 +376,53 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
]
[[package]]
name = "cryptography"
version = "45.0.7"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105 },
{ url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799 },
{ url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504 },
{ url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542 },
{ url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244 },
{ url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975 },
{ url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082 },
{ url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397 },
{ url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244 },
{ url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862 },
{ url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578 },
{ url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400 },
{ url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824 },
{ url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233 },
{ url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075 },
{ url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517 },
{ url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893 },
{ url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132 },
{ url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086 },
{ url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383 },
{ url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186 },
{ url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639 },
{ url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552 },
{ url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742 },
{ url = "https://files.pythonhosted.org/packages/13/3e/e42f1528ca1ea82256b835191eab1be014e0f9f934b60d98b0be8a38ed70/cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252", size = 3572442 },
{ url = "https://files.pythonhosted.org/packages/59/aa/e947693ab08674a2663ed2534cd8d345cf17bf6a1facf99273e8ec8986dc/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083", size = 4142233 },
{ url = "https://files.pythonhosted.org/packages/24/06/09b6f6a2fc43474a32b8fe259038eef1500ee3d3c141599b57ac6c57612c/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130", size = 4376202 },
{ url = "https://files.pythonhosted.org/packages/00/f2/c166af87e95ce6ae6d38471a7e039d3a0549c2d55d74e059680162052824/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4", size = 4141900 },
{ url = "https://files.pythonhosted.org/packages/16/b9/e96e0b6cb86eae27ea51fa8a3151535a18e66fe7c451fa90f7f89c85f541/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141", size = 4375562 },
{ url = "https://files.pythonhosted.org/packages/36/d0/36e8ee39274e9d77baf7d0dafda680cba6e52f3936b846f0d56d64fec915/cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7", size = 3322781 },
{ url = "https://files.pythonhosted.org/packages/99/4e/49199a4c82946938a3e05d2e8ad9482484ba48bbc1e809e3d506c686d051/cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde", size = 3584634 },
{ url = "https://files.pythonhosted.org/packages/16/ce/5f6ff59ea9c7779dba51b84871c19962529bdcc12e1a6ea172664916c550/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34", size = 4149533 },
{ url = "https://files.pythonhosted.org/packages/ce/13/b3cfbd257ac96da4b88b46372e662009b7a16833bfc5da33bb97dd5631ae/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9", size = 4385557 },
{ url = "https://files.pythonhosted.org/packages/1c/c5/8c59d6b7c7b439ba4fc8d0cab868027fd095f215031bc123c3a070962912/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae", size = 4149023 },
{ url = "https://files.pythonhosted.org/packages/55/32/05385c86d6ca9ab0b4d5bb442d2e3d85e727939a11f3e163fc776ce5eb40/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b", size = 4385722 },
{ url = "https://files.pythonhosted.org/packages/23/87/7ce86f3fa14bc11a5a48c30d8103c26e09b6465f8d8e9d74cf7a0714f043/cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63", size = 3332908 },
]
[[package]]
name = "django"
version = "5.2.1"
@ -370,6 +474,21 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/9f/a6d819a151723f44a85b1bfebfa60cd09d0219313b175023f5593bb47753/django_ninja-1.4.1-py3-none-any.whl", hash = "sha256:a091aa69be6ba75a89c5043d35f99cf9bf4f5c26e1ac6783accf8eaa1f8cb12b", size = 2425909 },
]
[[package]]
name = "django-oauth-toolkit"
version = "3.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "django" },
{ name = "jwcrypto" },
{ name = "oauthlib" },
{ name = "requests" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fa/d3/d7628a7a4899bf5aafc9c1ec121c507470b37a247f7628acae6e0f78e0d6/django_oauth_toolkit-3.0.1.tar.gz", hash = "sha256:7200e4a9fb229b145a6d808cbf0423b6d69a87f68557437733eec3c0cf71db02", size = 99816 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/40/e556bc19ba65356fe5f0e48ca01c50e81f7c630042fa7411b6ab428ecf68/django_oauth_toolkit-3.0.1-py3-none-any.whl", hash = "sha256:3ef00b062a284f2031b0732b32dc899e3bbf0eac221bbb1cffcb50b8932e55ed", size = 77299 },
]
[[package]]
name = "django-polymorphic"
version = "4.1.0"
@ -401,6 +520,7 @@ dependencies = [
{ name = "django-extensions" },
{ name = "django-model-utils" },
{ name = "django-ninja" },
{ name = "django-oauth-toolkit" },
{ name = "django-polymorphic" },
{ name = "enviformer" },
{ name = "envipy-additional-information" },
@ -424,9 +544,10 @@ requires-dist = [
{ name = "django-extensions", specifier = ">=4.1" },
{ name = "django-model-utils", specifier = ">=5.0.0" },
{ name = "django-ninja", specifier = ">=1.4.1" },
{ name = "django-oauth-toolkit", specifier = ">=3.0.1" },
{ name = "django-polymorphic", specifier = ">=4.1.0" },
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.0" },
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git" },
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4" },
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
{ name = "epam-indigo", specifier = ">=1.30.1" },
{ name = "gunicorn", specifier = ">=23.0.0" },
@ -443,7 +564,7 @@ requires-dist = [
[[package]]
name = "envipy-additional-information"
version = "0.1.0"
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git#4804b24b3479bed6108a49e4401bff8947c03cbd" }
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4#4da604090bf7cf1f3f552d69485472dbc623030a" }
dependencies = [
{ name = "pydantic" },
]
@ -625,6 +746,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 },
]
[[package]]
name = "jwcrypto"
version = "1.5.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/db/870e5d5fb311b0bcf049630b5ba3abca2d339fd5e13ba175b4c13b456d08/jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039", size = 87168 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/cd/58/4a1880ea64032185e9ae9f63940c9327c6952d5584ea544a8f66972f2fda/jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", size = 92520 },
]
[[package]]
name = "kombu"
version = "5.5.3"
@ -1042,6 +1176,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265 },
]
[[package]]
name = "oauthlib"
version = "3.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 },
]
[[package]]
name = "packaging"
version = "25.0"
@ -1284,6 +1427,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 },
]
[[package]]
name = "pycparser"
version = "2.22"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
]
[[package]]
name = "pydantic"
version = "2.11.7"