30 Commits

Author SHA1 Message Date
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
85 changed files with 7245 additions and 1357 deletions

View File

@ -1,7 +1,14 @@
from epdb.api import router as epdb_app_router from epdb.api import router as epdb_app_router
from epdb.legacy_api import router as epdb_legacy_app_router
from ninja import NinjaAPI from ninja import NinjaAPI
api = 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

@ -260,6 +260,8 @@ CELERY_RESULT_BACKEND = 'redis://localhost:6379/1'
CELERY_ACCEPT_CONTENT = ['json'] CELERY_ACCEPT_CONTENT = ['json']
CELERY_TASK_SERIALIZER = '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 = { DEFAULT_RF_MODEL_PARAMS = {
'base_clf': RandomForestClassifier( 'base_clf': RandomForestClassifier(
n_estimators=100, n_estimators=100,
@ -273,14 +275,14 @@ DEFAULT_RF_MODEL_PARAMS = {
'num_chains': 10, 'num_chains': 10,
} }
DEFAULT_DT_MODEL_PARAMS = { DEFAULT_MODEL_PARAMS = {
'base_clf': DecisionTreeClassifier( 'base_clf': DecisionTreeClassifier(
criterion='entropy', criterion='entropy',
max_depth=3, max_depth=3,
min_samples_split=5, min_samples_split=5,
min_samples_leaf=5, # min_samples_leaf=5,
max_features='sqrt', max_features='sqrt',
class_weight='balanced', # class_weight='balanced',
random_state=42 random_state=42
), ),
'num_chains': 10, 'num_chains': 10,
@ -306,9 +308,30 @@ SENTRY_ENABLED = os.environ.get('SENTRY_ENABLED', 'False') == 'True'
if SENTRY_ENABLED: if SENTRY_ENABLED:
import sentry_sdk 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( sentry_sdk.init(
dsn=os.environ.get('SENTRY_DSN'), dsn=os.environ.get('SENTRY_DSN'),
# Add data like request headers and IP for users, # Add data like request headers and IP for users,
# see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info # see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
send_default_pii=True, 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,
}

View File

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

View File

@ -1,40 +1,105 @@
from django.contrib import admin 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): class UserAdmin(admin.ModelAdmin):
pass pass
class GroupAdmin(admin.ModelAdmin):
pass
class UserPackagePermissionAdmin(admin.ModelAdmin): class UserPackagePermissionAdmin(admin.ModelAdmin):
pass pass
class GroupAdmin(admin.ModelAdmin):
pass
class GroupPackagePermissionAdmin(admin.ModelAdmin): class GroupPackagePermissionAdmin(admin.ModelAdmin):
pass pass
class SettingAdmin(admin.ModelAdmin): class EPAdmin(admin.ModelAdmin):
search_fields = ['name', 'description']
class PackageAdmin(EPAdmin):
pass
class MLRelativeReasoningAdmin(EPAdmin):
pass pass
class SimpleAmbitRuleAdmin(admin.ModelAdmin): class CompoundAdmin(EPAdmin):
pass 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 pass
admin.site.register(User, UserAdmin) admin.site.register(User, UserAdmin)
admin.site.register(Group, GroupAdmin)
admin.site.register(UserPackagePermission, UserPackagePermissionAdmin) admin.site.register(UserPackagePermission, UserPackagePermissionAdmin)
admin.site.register(Group, GroupAdmin)
admin.site.register(GroupPackagePermission, GroupPackagePermissionAdmin) 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(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) admin.site.register(Scenario, ScenarioAdmin)

View File

@ -4,3 +4,6 @@ from django.apps import AppConfig
class EPDBConfig(AppConfig): class EPDBConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField' default_auto_field = 'django.db.models.BigAutoField'
name = 'epdb' 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 re
import logging 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.contrib.auth import get_user_model
from django.db import transaction from django.db import transaction
@ -13,6 +14,132 @@ from utilities.chem import FormatConverter
logger = logging.getLogger(__name__) 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): 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}") 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 @staticmethod
def get_user_by_id(user, user_uuid: str): 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!") raise ValueError("Getting user failed!")
return get_user_model().objects.get(uuid=user_uuid) return get_user_model().objects.get(uuid=user_uuid)
@ -183,6 +310,25 @@ class PackageManager(object):
return True return True
return False 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 @staticmethod
def has_package_permission(user: 'User', package: Union[str, 'Package'], permission: str): def has_package_permission(user: 'User', package: Union[str, 'Package'], permission: str):
@ -339,7 +485,7 @@ class PackageManager(object):
@staticmethod @staticmethod
@transaction.atomic @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 uuid import UUID, uuid4
from datetime import datetime from datetime import datetime
from collections import defaultdict from collections import defaultdict
@ -349,7 +495,12 @@ class PackageManager(object):
pack = Package() pack = Package()
pack.uuid = UUID(data['id'].split('/')[-1]) if keep_ids else uuid4() pack.uuid = UUID(data['id'].split('/')[-1]) if keep_ids else uuid4()
pack.name = '{} - {}'.format(data['name'], datetime.now().strftime('%Y-%m-%d %H:%M'))
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.reviewed = True if data['reviewStatus'] == 'reviewed' else False
pack.description = data['description'] pack.description = data['description']
pack.save() pack.save()
@ -402,11 +553,13 @@ class PackageManager(object):
try: try:
res = AdditionalInformationConverter.convert(name, addinf_data) 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: except:
logger.error(f"Failed to convert {name} with {addinf_data}") 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.additional_information = new_add_inf
scen.save() scen.save()
@ -890,9 +1043,10 @@ class SearchManager(object):
class SNode(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.smiles = smiles
self.depth = depth self.depth = depth
self.app_domain_assessment = app_domain_assessment
def __hash__(self): def __hash__(self):
return hash(self.smiles) return hash(self.smiles)
@ -1035,7 +1189,7 @@ class SPathway(object):
def depth(self): def depth(self):
return max([v.depth for v in self.smiles_to_node.values()]) 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: if depth == 0:
return self.root_nodes return self.root_nodes
@ -1046,7 +1200,7 @@ class SPathway(object):
return sorted(res, key=lambda x: x.smiles) 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 = [] res = []
for e in self.edges: for e in self.edges:
for n in e.educts: for n in e.educts:
@ -1061,7 +1215,7 @@ class SPathway(object):
if from_depth is not None: if from_depth is not None:
substrates = self._get_nodes_for_depth(from_depth) substrates = self._get_nodes_for_depth(from_depth)
elif from_node is not None: elif from_node is not None:
for k,v in self.snode_persist_lookup.items(): for k, v in self.snode_persist_lookup.items():
if from_node == v: if from_node == v:
substrates = [k] substrates = [k]
break break
@ -1071,15 +1225,44 @@ class SPathway(object):
new_tp = False new_tp = False
if substrates: if substrates:
for sub in 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 = 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: for cand_set in candidates:
if cand_set: if cand_set:
new_tp = True new_tp = True
# cand_set is a PredictionResult object that can consist of multiple candidate reactions
for cand in cand_set: for cand in cand_set:
cand_nodes = [] cand_nodes = []
# candidate reactions can have multiple fragments
for c in cand: for c in cand:
if c not in self.smiles_to_node: 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] node = self.smiles_to_node[c]
cand_nodes.append(node) cand_nodes.append(node)
@ -1092,18 +1275,30 @@ class SPathway(object):
if len(substrates) == 0 or from_node is not None: if len(substrates) == 0 or from_node is not None:
self.done = True 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: if new_tp and self.persist:
self._sync_to_pathway() self._sync_to_pathway()
# call save to update internal modified field # call save to update the internal modified field
self.persist.save() self.persist.save()
def _sync_to_pathway(self): def _sync_to_pathway(self) -> None:
logger.info("Updating Pathway with SPathway") logger.info("Updating Pathway with SPathway")
for snode in self.smiles_to_node.values(): for snode in self.smiles_to_node.values():
if snode not in self.snode_persist_lookup: if snode not in self.snode_persist_lookup:
n = Node.create(self.persist, snode.smiles, snode.depth) 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 self.snode_persist_lookup[snode] = n
for sedge in self.edges: for sedge in self.edges:
@ -1125,7 +1320,6 @@ class SPathway(object):
self.sedge_persist_lookup[sedge] = e self.sedge_persist_lookup[sedge] = e
logger.info("Update done!") logger.info("Update done!")
pass
def to_json(self): def to_json(self):
nodes = [] nodes = []

View File

@ -5,7 +5,7 @@ from django.core.management.base import BaseCommand
from django.db import transaction from django.db import transaction
from epdb.logic import UserManager, GroupManager, PackageManager, SettingManager 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): class Command(BaseCommand):
@ -58,7 +58,7 @@ class Command(BaseCommand):
return anon, admin, g, jebus return anon, admin, g, jebus
def import_package(self, data, owner): 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): def create_default_setting(self, owner, packages):
s = SettingManager.create_setting( s = SettingManager.create_setting(
@ -74,6 +74,76 @@ class Command(BaseCommand):
return s 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 @transaction.atomic
def handle(self, *args, **options): def handle(self, *args, **options):
# Create users # Create users
@ -117,17 +187,17 @@ class Command(BaseCommand):
# Create RR # Create RR
ml_model = MLRelativeReasoning.create( ml_model = MLRelativeReasoning.create(
pack, package=pack,
'ECC - BBD - T0.5', rule_packages=[mapping['EAWAG-BBD']],
'ML Relative Reasoning', data_packages=[mapping['EAWAG-BBD']],
[mapping['EAWAG-BBD']], eval_packages=[],
[mapping['EAWAG-BBD']], threshold=0.5,
[], name='ECC - BBD - T0.5',
0.5 description='ML Relative Reasoning',
) )
X, y = ml_model.build_dataset() ml_model.build_dataset()
ml_model.build_model(X, y) ml_model.build_model()
# ml_model.evaluate_model() # ml_model.evaluate_model()
# If available create EnviFormerModel # 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

@ -2,7 +2,6 @@ from django.conf import settings
from django.shortcuts import redirect from django.shortcuts import redirect
from django.urls import reverse from django.urls import reverse
class LoginRequiredMiddleware: class LoginRequiredMiddleware:
def __init__(self, get_response): def __init__(self, get_response):
self.get_response = get_response self.get_response = get_response
@ -11,6 +10,7 @@ class LoginRequiredMiddleware:
reverse('logout'), reverse('logout'),
reverse('admin:login'), reverse('admin:login'),
reverse('admin:index'), reverse('admin:index'),
'/api/legacy/'
] + getattr(settings, 'LOGIN_EXEMPT_URLS', []) ] + getattr(settings, 'LOGIN_EXEMPT_URLS', [])
def __call__(self, request): def __call__(self, 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') @shared_task(queue='model')
def build_model(model_pk: int): def build_model(model_pk: int):
mod = EPModel.objects.get(id=model_pk) mod = EPModel.objects.get(id=model_pk)
X, y = mod.build_dataset() mod.build_dataset()
mod.build_model(X, y) mod.build_model()
@shared_task(queue='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) spw.predict_step(from_depth=level)
level += 1 level += 1
# break in case we are in incremental model # break in case we are in incremental mode
if limit != -1: if limit != -1:
if level >= limit: if level >= limit:
break break

View File

@ -7,10 +7,12 @@ from django.contrib.auth import get_user_model
from django.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest from django.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest
from django.shortcuts import render, redirect from django.shortcuts import render, redirect
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from envipy_additional_information import NAME_MAPPING
from utilities.chem import FormatConverter, IndigoUtils from utilities.chem import FormatConverter, IndigoUtils
from utilities.decorators import package_permission_required from utilities.decorators import package_permission_required
from .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, \ from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \
EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \ EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \
UserPackagePermission, Permission, License, User, Edge UserPackagePermission, Permission, License, User, Edge
@ -103,7 +105,10 @@ def login(request):
else: else:
context['message'] = "Account has been created! You'll receive a mail to activate your account shortly." context['message'] = "Account has been created! You'll receive a mail to activate your account shortly."
return render(request, 'login.html', context) return render(request, 'login.html', context)
else:
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(['GET', 'POST'])
def logout(request): def logout(request):
if request.method == 'POST': if request.method == 'POST':
@ -136,7 +141,7 @@ def editable(request, user):
f"{s.SERVER_URL}/group", f"{s.SERVER_URL}/search"]: f"{s.SERVER_URL}/group", f"{s.SERVER_URL}/search"]:
return True return True
else: else:
print(f"Unknown url: {url}") logger.debug(f"Unknown url: {url}")
return False return False
@ -144,6 +149,12 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
current_user = _anonymous_or_real(request) current_user = _anonymous_or_real(request)
can_edit = editable(request, current_user) 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: if for_user:
current_user = for_user current_user = for_user
@ -154,11 +165,12 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
'server_url': s.SERVER_URL, 'server_url': s.SERVER_URL,
'user': current_user, 'user': current_user,
'can_edit': can_edit, 'can_edit': can_edit,
'url_contains_package': url_contains_package,
'readable_packages': PackageManager.get_all_readable_packages(current_user, include_reviewed=True), 'readable_packages': PackageManager.get_all_readable_packages(current_user, include_reviewed=True),
'writeable_packages': PackageManager.get_all_writeable_packages(current_user), 'writeable_packages': PackageManager.get_all_writeable_packages(current_user),
'available_groups': GroupManager.get_groups(current_user), 'available_groups': GroupManager.get_groups(current_user),
'available_settings': SettingManager.get_all_settings(current_user), 'available_settings': SettingManager.get_all_settings(current_user),
'enabled_features': [], 'enabled_features': s.FLAGS,
'debug': s.DEBUG, 'debug': s.DEBUG,
}, },
} }
@ -196,6 +208,41 @@ def breadcrumbs(first_level_object=None, second_level_namespace=None, second_lev
return bread 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): def index(request):
context = get_base_context(request) context = get_base_context(request)
context['title'] = 'enviPath - Home' context['title'] = 'enviPath - Home'
@ -424,8 +471,7 @@ def scenarios(request):
if request.GET.get('all'): if request.GET.get('all'):
return JsonResponse({ return JsonResponse({
"objects": [ "objects": [
{"name": pw.name, "url": pw.url, "reviewed": True} {"name": s.name, "url": s.url, "reviewed": True} for s in reviewed_scenario_qs
for pw in reviewed_scenario_qs
] ]
}) })
@ -505,6 +551,7 @@ def search(request):
packages = PackageManager.get_reviewed_packages() packages = PackageManager.get_reviewed_packages()
search_result = SearchManager.search(packages, searchterm, mode) search_result = SearchManager.search(packages, searchterm, mode)
return JsonResponse(search_result, safe=False) return JsonResponse(search_result, safe=False)
context = get_base_context(request) context = get_base_context(request)
@ -530,6 +577,7 @@ def search(request):
packages = PackageManager.get_reviewed_packages() packages = PackageManager.get_reviewed_packages()
context['search_result'] = SearchManager.search(packages, searchterm, mode) context['search_result'] = SearchManager.search(packages, searchterm, mode)
context['search_result']['searchterm'] = searchterm
return render(request, 'search.html', context) return render(request, 'search.html', context)
@ -572,15 +620,21 @@ def package_models(request, package_uuid):
context['model_types'] = { context['model_types'] = {
'ML Relative Reasoning': 'ml-relative-reasoning', 'ML Relative Reasoning': 'ml-relative-reasoning',
'Rule Based Relative Reasoning': 'rule-based-relative-reasoning', 'Rule Based Relative Reasoning': 'rule-based-relative-reasoning',
'EnviFormer': 'enviformer',
} }
for k, v in s.CLASSIFIER_PLUGINS.items(): if s.FLAGS.get('ENVIFORMER', False):
context['model_types'][v.display()] = k 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) return render(request, 'collections/objects_list.html', context)
elif request.method == 'POST': elif request.method == 'POST':
log_post_params(request)
name = request.POST.get('model-name') name = request.POST.get('model-name')
description = request.POST.get('model-description') description = request.POST.get('model-description')
@ -603,14 +657,25 @@ def package_models(request, package_uuid):
data_package_objs = [PackageManager.get_package_by_url(current_user, p) for p in data_packages] 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] 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( mod = MLRelativeReasoning.create(
current_package, package=current_package,
name, name=name,
description, description=description,
rule_package_objs, rule_packages=rule_package_objs,
data_package_objs, data_packages=data_package_objs,
eval_packages_objs, eval_packages=eval_packages_objs,
threshold 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 from .tasks import build_model
@ -646,7 +711,7 @@ def package_model(request, package_uuid, model_uuid):
if len(pr) > 0: if len(pr) > 0:
products = [] products = []
for prod_set in pr.product_sets: 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])) products.append(tuple([x for x in prod_set]))
res.append({ res.append({
@ -657,6 +722,12 @@ def package_model(request, package_uuid, model_uuid):
return JsonResponse(res, safe=False) 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 = get_base_context(request)
context['title'] = f'enviPath - {current_package.name} - {current_model.name}' context['title'] = f'enviPath - {current_package.name} - {current_model.name}'
@ -665,12 +736,13 @@ def package_model(request, package_uuid, model_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'model', current_model) context['breadcrumbs'] = breadcrumbs(current_package, 'model', current_model)
context['model'] = current_model context['model'] = current_model
context['current_object'] = current_model
return render(request, 'objects/model.html', context) return render(request, 'objects/model.html', context)
elif request.method == 'POST': elif request.method == 'POST':
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-model': if hidden == 'delete':
current_model.delete() current_model.delete()
return redirect(current_package.url + '/model') return redirect(current_package.url + '/model')
else: else:
@ -696,8 +768,6 @@ def package(request, package_uuid):
context['breadcrumbs'] = breadcrumbs(current_package) context['breadcrumbs'] = breadcrumbs(current_package)
context['package'] = 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) user_perms = UserPackagePermission.objects.filter(package=current_package)
users = get_user_model().objects.exclude( users = get_user_model().objects.exclude(
@ -716,14 +786,24 @@ def package(request, package_uuid):
elif request.method == 'POST': elif request.method == 'POST':
if s.DEBUG: log_post_params(request)
for k, v in request.POST.items():
logger.debug(f"{k}\t{v}")
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-package': if hidden == 'delete':
logger.debug(current_package.delete()) logger.debug(current_package.delete())
return redirect(s.SERVER_URL + '/package') 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: else:
return HttpResponseBadRequest() return HttpResponseBadRequest()
@ -855,17 +935,24 @@ def package_compound(request, package_uuid, compound_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'compound', current_compound) context['breadcrumbs'] = breadcrumbs(current_package, 'compound', current_compound)
context['compound'] = current_compound context['compound'] = current_compound
context['current_object'] = current_compound
return render(request, 'objects/compound.html', context) return render(request, 'objects/compound.html', context)
elif request.method == 'POST': elif request.method == 'POST':
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-compound': if hidden == 'delete':
current_compound.delete() current_compound.delete()
return redirect(current_package.url + '/compound') return redirect(current_package.url + '/compound')
else: else:
return HttpResponseBadRequest() 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_name = request.POST.get('compound-name')
new_compound_description = request.POST.get('compound-description') new_compound_description = request.POST.get('compound-description')
@ -897,6 +984,7 @@ def package_compound_structures(request, package_uuid, compound_uuid):
context['meta']['current_package'] = current_package context['meta']['current_package'] = current_package
context['object_type'] = 'structure' context['object_type'] = 'structure'
context['breadcrumbs'] = breadcrumbs(current_package, 'compound', current_compound, 'structure')
reviewed_compound_structure_qs = CompoundStructure.objects.none() reviewed_compound_structure_qs = CompoundStructure.objects.none()
unreviewed_compound_structure_qs = CompoundStructure.objects.none() unreviewed_compound_structure_qs = CompoundStructure.objects.none()
@ -936,12 +1024,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['title'] = f'enviPath - {current_package.name} - {current_compound.name} - {current_structure.name}'
context['meta']['current_package'] = current_package context['meta']['current_package'] = current_package
context['object_type'] = 'compound' context['object_type'] = 'structure'
context['compound_structure'] = current_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) 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: else:
return HttpResponseNotAllowed(['GET', ]) return HttpResponseNotAllowed(['GET', ])
@ -1022,6 +1143,24 @@ def package_rule(request, package_uuid, rule_uuid):
if request.method == 'GET': if request.method == 'GET':
context = get_base_context(request) 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['title'] = f'enviPath - {current_package.name} - {current_rule.name}'
context['meta']['current_package'] = current_package context['meta']['current_package'] = current_package
@ -1029,6 +1168,8 @@ def package_rule(request, package_uuid, rule_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'rule', current_rule) context['breadcrumbs'] = breadcrumbs(current_package, 'rule', current_rule)
context['rule'] = current_rule context['rule'] = current_rule
context['current_object'] = current_rule
if isinstance(current_rule, SimpleAmbitRule): if isinstance(current_rule, SimpleAmbitRule):
return render(request, 'objects/simple_rule.html', context) return render(request, 'objects/simple_rule.html', context)
else: # isinstance(current_rule, ParallelRule) or isinstance(current_rule, SequentialRule): else: # isinstance(current_rule, ParallelRule) or isinstance(current_rule, SequentialRule):
@ -1036,12 +1177,18 @@ def package_rule(request, package_uuid, rule_uuid):
elif request.method == 'POST': elif request.method == 'POST':
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-rule': if hidden == 'delete':
current_rule.delete() current_rule.delete()
return redirect(current_package.url + '/rule') return redirect(current_package.url + '/rule')
else: else:
return HttpResponseBadRequest() 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_name = request.POST.get('rule-name', '').strip()
rule_description = request.POST.get('rule-description', '').strip() rule_description = request.POST.get('rule-description', '').strip()
@ -1127,17 +1274,24 @@ def package_reaction(request, package_uuid, reaction_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'reaction', current_reaction) context['breadcrumbs'] = breadcrumbs(current_package, 'reaction', current_reaction)
context['reaction'] = current_reaction context['reaction'] = current_reaction
context['current_object'] = current_reaction
return render(request, 'objects/reaction.html', context) return render(request, 'objects/reaction.html', context)
elif request.method == 'POST': elif request.method == 'POST':
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-reaction': if hidden == 'delete':
current_reaction.delete() current_reaction.delete()
return redirect(current_package.url + '/reaction') return redirect(current_package.url + '/reaction')
else: else:
return HttpResponseBadRequest() 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_name = request.POST.get('reaction-name')
new_reaction_description = request.POST.get('reaction-description') new_reaction_description = request.POST.get('reaction-description')
@ -1195,8 +1349,8 @@ def package_pathways(request, package_uuid):
log_post_params(request) log_post_params(request)
name = request.POST.get('name', 'Pathway ' + str(Pathway.objects.filter(package=current_package).count())) name = request.POST.get('name')
description = request.POST.get('description', s.DEFAULT_VALUES['description']) description = request.POST.get('description')
pw_mode = request.POST.get('predict', 'predict') pw_mode = request.POST.get('predict', 'predict')
smiles = request.POST.get('smiles') smiles = request.POST.get('smiles')
@ -1217,7 +1371,14 @@ def package_pathways(request, package_uuid):
return error(request, "Pathway prediction failed!", return error(request, "Pathway prediction failed!",
f'Pathway prediction failed as received mode "{pw_mode}" is none of {modes}') 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) pw = Pathway.create(current_package, stand_smiles, name=name, description=description)
# set mode # set mode
pw.kv.update({'mode': pw_mode}) pw.kv.update({'mode': pw_mode})
pw.save() pw.save()
@ -1230,12 +1391,11 @@ def package_pathways(request, package_uuid):
if pw_mode == 'incremental': if pw_mode == 'incremental':
limit = 1 limit = 1
pred_setting = current_user.prediction_settings() pw.setting = prediction_setting
pw.setting = pred_setting
pw.save() pw.save()
from .tasks import predict 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) return redirect(pw.url)
@ -1254,6 +1414,14 @@ def package_pathway(request, package_uuid, pathway_uuid):
if request.GET.get("last_modified", False): if request.GET.get("last_modified", False):
return JsonResponse({'modified': current_pathway.modified.strftime('%Y-%m-%d %H:%M:%S')}) 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 = get_base_context(request)
context['title'] = f'enviPath - {current_package.name} - {current_pathway.name}' context['title'] = f'enviPath - {current_package.name} - {current_pathway.name}'
@ -1262,6 +1430,7 @@ def package_pathway(request, package_uuid, pathway_uuid):
context['breadcrumbs'] = breadcrumbs(current_package, 'pathway', current_pathway) context['breadcrumbs'] = breadcrumbs(current_package, 'pathway', current_pathway)
context['pathway'] = current_pathway context['pathway'] = current_pathway
context['current_object'] = current_pathway
context['breadcrumbs'] = [ context['breadcrumbs'] = [
{'Home': s.SERVER_URL}, {'Home': s.SERVER_URL},
@ -1276,12 +1445,18 @@ def package_pathway(request, package_uuid, pathway_uuid):
elif request.method == 'POST': elif request.method == 'POST':
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-pathway': if hidden == 'delete':
current_pathway.delete() current_pathway.delete()
return redirect(current_package.url + '/pathway') return redirect(current_package.url + '/pathway')
else: else:
return HttpResponseBadRequest() 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_name = request.POST.get('pathway-name')
pathway_description = request.POST.get('pathway-description') pathway_description = request.POST.get('pathway-description')
@ -1402,19 +1577,33 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
] ]
context['node'] = current_node 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) return render(request, 'objects/node.html', context)
elif request.method == 'POST': elif request.method == 'POST':
if s.DEBUG:
for k, v in request.POST.items(): log_post_params(request)
print(k, v)
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-node': if hidden == 'delete':
current_node.delete()
return redirect(current_pathway.url)
# 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: else:
return HttpResponseNotAllowed(['GET', 'POST']) return HttpResponseNotAllowed(['GET', 'POST'])
@ -1463,6 +1652,8 @@ def package_pathway_edges(request, package_uuid, pathway_uuid):
elif request.method == 'POST': elif request.method == 'POST':
log_post_params(request)
edge_name = request.POST.get('edge-name') edge_name = request.POST.get('edge-name')
edge_description = request.POST.get('edge-description') edge_description = request.POST.get('edge-description')
edge_substrates = request.POST.getlist('edge-substrates') edge_substrates = request.POST.getlist('edge-substrates')
@ -1499,22 +1690,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}' 'title'] = f'enviPath - {current_package.name} - {current_pathway.name} - {current_edge.edge_label.name}'
context['meta']['current_package'] = current_package 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['breadcrumbs'] = breadcrumbs(current_package, 'pathway', current_pathway, 'edge', current_edge)
context['edge'] = current_edge context['edge'] = current_edge
context['current_object'] = current_edge
return render(request, 'objects/edge.html', context) return render(request, 'objects/edge.html', context)
elif request.method == 'POST': elif request.method == 'POST':
if s.DEBUG:
for k, v in request.POST.items(): log_post_params(request)
print(k, v)
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-edge': if hidden == 'delete':
current_edge.delete() current_edge.delete()
return redirect(current_pathway.url) 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: else:
return HttpResponseNotAllowed(['GET', 'POST']) return HttpResponseNotAllowed(['GET', 'POST'])
@ -1525,12 +1724,18 @@ def package_scenarios(request, package_uuid):
current_package = PackageManager.get_package_by_id(current_user, package_uuid) current_package = PackageManager.get_package_by_id(current_user, package_uuid)
if request.method == 'GET': 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 = get_base_context(request)
context['title'] = f'enviPath - {current_package.name} - Scenarios' context['title'] = f'enviPath - {current_package.name} - Scenarios'
context['meta']['current_package'] = current_package context['meta']['current_package'] = current_package
context['object_type'] = 'scenario' context['object_type'] = 'scenario'
context['breadcrumbs'] = breadcrumbs(current_package, 'pathway') context['breadcrumbs'] = breadcrumbs(current_package, 'scenario')
reviewed_scenario_qs = Scenario.objects.none() reviewed_scenario_qs = Scenario.objects.none()
unreviewed_scenario_qs = Scenario.objects.none() unreviewed_scenario_qs = Scenario.objects.none()
@ -1551,8 +1756,57 @@ def package_scenarios(request, package_uuid):
context['reviewed_objects'] = reviewed_scenario_qs context['reviewed_objects'] = reviewed_scenario_qs
context['unreviewed_objects'] = unreviewed_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: else:
return HttpResponseNotAllowed(['GET', ]) return HttpResponseNotAllowed(['GET', ])
@ -1573,10 +1827,63 @@ def package_scenario(request, package_uuid, scenario_uuid):
context['scenario'] = current_scenario 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) 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 HttpResponseBadRequest()
else:
return HttpResponseBadRequest()
else: else:
return HttpResponseNotAllowed(['GET', ]) return HttpResponseNotAllowed(['GET', 'POST'])
############## ##############
@ -1587,7 +1894,6 @@ def users(request):
context = get_base_context(request) context = get_base_context(request)
context['title'] = f'enviPath - Users' context['title'] = f'enviPath - Users'
context['meta']['current_package'] = context['meta']['user'].default_package
context['object_type'] = 'user' context['object_type'] = 'user'
context['breadcrumbs'] = [ context['breadcrumbs'] = [
{'Home': s.SERVER_URL}, {'Home': s.SERVER_URL},
@ -1611,27 +1917,27 @@ def user(request, user_uuid):
if str(current_user.uuid) != user_uuid and not current_user.is_superuser: if str(current_user.uuid) != user_uuid and not current_user.is_superuser:
return HttpResponseBadRequest() 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['title'] = f'enviPath - User'
context['object_type'] = 'user' context['object_type'] = 'user'
context['breadcrumbs'] = [ context['breadcrumbs'] = [
{'Home': s.SERVER_URL}, {'Home': s.SERVER_URL},
{'User': s.SERVER_URL + '/user'}, {'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() 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 model_qs |= p.models
context['models'] = model_qs 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) return render(request, 'objects/user.html', context)
@ -1700,8 +2006,6 @@ def user(request, user_uuid):
} }
} }
print(setting)
return HttpResponseBadRequest() return HttpResponseBadRequest()
else: else:
@ -1715,7 +2019,6 @@ def groups(request):
context = get_base_context(request) context = get_base_context(request)
context['title'] = f'enviPath - Groups' context['title'] = f'enviPath - Groups'
context['meta']['current_package'] = context['meta']['user'].default_package
context['object_type'] = 'group' context['object_type'] = 'group'
context['breadcrumbs'] = [ context['breadcrumbs'] = [
{'Home': s.SERVER_URL}, {'Home': s.SERVER_URL},
@ -1764,12 +2067,10 @@ def group(request, group_uuid):
elif request.method == 'POST': elif request.method == 'POST':
if s.DEBUG: log_post_params(request)
for k, v in request.POST.items():
print(k, v)
if hidden := request.POST.get('hidden', None): if hidden := request.POST.get('hidden', None):
if hidden == 'delete-group': if hidden == 'delete':
current_group.delete() current_group.delete()
return redirect(s.SERVER_URL + '/group') return redirect(s.SERVER_URL + '/group')
else: else:

File diff suppressed because one or more lines are too long

View File

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

View File

@ -29,4 +29,4 @@ dependencies = [
[tool.uv.sources] [tool.uv.sources]
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.0" } 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-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) { function predictFromNode(url) {
$.post("", {node: url}) $.post("", {node: url})
@ -28,62 +27,165 @@ function draw(pathway, elem) {
const horizontalSpacing = 75; // horizontal space between nodes const horizontalSpacing = 75; // horizontal space between nodes
const depthMap = new Map(); 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)) { if (!depthMap.has(node.depth)) {
depthMap.set(node.depth, 0); depthMap.set(node.depth, 0);
} }
const nodesInLevel = nodes.filter(n => n.depth === node.depth).length; 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); 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() { function ticked() {
// Update pseudo node positions first
updatePseudoNodePositions();
link.attr("x1", d => d.source.x) link.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y) .attr("y1", d => d.source.y)
.attr("x2", d => d.target.x) .attr("x2", d => d.target.x)
.attr("y2", d => d.target.y); .attr("y2", d => d.target.y);
node.attr("transform", d => `translate(${d.x},${d.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) { function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart(); 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; 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) { function dragged(event, d) {
d.fx = event.x; // Position direkt an Maus anpassen d.fx = event.x;
d.fy = event.y; d.fy = event.y;
// Update connected pseudo nodes in real-time
if (!d.pseudo) {
updateConnectedPseudoNodes(d);
}
} }
function dragended(event, d) { function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0); 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 // t -> ref to "this" from d3
function nodeClick(event, node, t) { function nodeClick(event, node, t) {
console.log(node); console.log(node);
@ -105,7 +207,7 @@ function draw(pathway, elem) {
pop = $(e).attr("aria-describedby") pop = $(e).attr("aria-describedby")
h = $('#' + pop).height(); h = $('#' + pop).height();
$('#' + pop).attr("style", `position: fixed; top: ${clientY - (h / 2.0)}px; left: ${clientX + 10}px; margin: 0px; max-width: 1000px; display: block;`) $('#' + pop).attr("style", `position: fixed; top: ${clientY - (h / 2.0)}px; left: ${clientX + 10}px; margin: 0px; max-width: 1000px; display: block;`)
setTimeout(function () { setTimeout(function () {
var close = setInterval(function () { var close = setInterval(function () {
if (!$(".popover:hover").length // mouse outside popover if (!$(".popover:hover").length // mouse outside popover
&& !$(e).is(':hover')) { // mouse outside element && !$(e).is(':hover')) { // mouse outside element
@ -140,11 +242,20 @@ function draw(pathway, elem) {
}); });
} }
function node_popup(n) { function node_popup(n) {
popupContent = "<a href='" + n.url +"'>" + n.name + "</a><br>"; popupContent = "<a href='" + n.url + "'>" + n.name + "</a><br>";
popupContent += "Depth " + n.depth + "<br>" popupContent += "Depth " + n.depth + "<br>"
popupContent += "<img src='" + n.image + "' width='"+ 20 * nodeRadius +"'><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) { if (n.scenarios.length > 0) {
popupContent += '<b>Half-lives and related scenarios:</b><br>' popupContent += '<b>Half-lives and related scenarios:</b><br>'
for (var s of n.scenarios) { for (var s of n.scenarios) {
@ -153,7 +264,7 @@ function draw(pathway, elem) {
} }
var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0; var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0;
if(pathway.isIncremental && isLeaf) { if (pathway.isIncremental && isLeaf) {
popupContent += '<br><a class="btn btn-primary" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>'; popupContent += '<br><a class="btn btn-primary" onclick="predictFromNode(\'' + n.url + '\')" href="#">Predict from here</a><br>';
} }
@ -161,13 +272,24 @@ function draw(pathway, elem) {
} }
function edge_popup(e) { function edge_popup(e) {
popupContent = "<a href='" + e.url +"'>" + e.name + "</a><br>"; popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br>";
popupContent += "<img src='" + e.image + "' width='"+ 20 * nodeRadius +"'><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) { if (e.reaction_probability) {
popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>'; popupContent += '<b>Probability:</b><br>' + e.reaction_probability.toFixed(3) + '<br>';
} }
if (e.scenarios.length > 0) { if (e.scenarios.length > 0) {
popupContent += '<b>Half-lives and related scenarios:</b><br>' popupContent += '<b>Half-lives and related scenarios:</b><br>'
for (var s of e.scenarios) { for (var s of e.scenarios) {
@ -180,9 +302,9 @@ function draw(pathway, elem) {
var clientX; var clientX;
var clientY; var clientY;
document.addEventListener('mousemove', function(event) { document.addEventListener('mousemove', function (event) {
clientX = event.clientX; clientX = event.clientX;
clientY =event.clientY; clientY = event.clientY;
}); });
const zoomable = d3.select("#zoomable"); const zoomable = d3.select("#zoomable");
@ -233,13 +355,12 @@ function draw(pathway, elem) {
.enter().append("line") .enter().append("line")
// Check if target is pseudo and draw marker only if not pseudo // Check if target is pseudo and draw marker only if not pseudo
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link") .attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
// .on("mouseover", (event, d) => { .attr("marker-end", d => d.target.pseudo ? '' : 'url(#arrow)')
// tooltip.style("visibility", "visible")
// .text(`Link: ${d.source.id} → ${d.target.id}`) // add element to links array
// .style("top", `${event.pageY + 5}px`) link.each(function (d) {
// .style("left", `${event.pageX + 5}px`); d.el = this; // attach the DOM element to the data object
// }) });
// .on("mouseout", () => tooltip.style("visibility", "hidden"));
pop_add(link, "Reaction", edge_popup); pop_add(link, "Reaction", edge_popup);
@ -255,20 +376,10 @@ function draw(pathway, elem) {
.on("click", function (event, d) { .on("click", function (event, d) {
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted")); 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 // Kreise für die Knoten hinzufügen
node.append("circle") node.append("circle")
// make radius "invisible" // make radius "invisible" for pseudo nodes
.attr("r", d => d.pseudo ? 0.01 : nodeRadius) .attr("r", d => d.pseudo ? 0.01 : nodeRadius)
.style("fill", "#e8e8e8"); .style("fill", "#e8e8e8");
@ -280,5 +391,10 @@ function draw(pathway, elem) {
.attr("width", nodeRadius * 2) .attr("width", nodeRadius * 2)
.attr("height", nodeRadius * 2); .attr("height", nodeRadius * 2);
pop_add(node, "Compound", node_popup); // 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> <li>
<a role="button" data-toggle="modal" data-target="#new_model_modal"> <a role="button" data-toggle="modal" data-target="#new_model_modal">
<span class="glyphicon glyphicon-plus"></span> New Model</a> <span class="glyphicon glyphicon-plus"></span> New Model</a>

View File

@ -1,6 +1,6 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <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> <span class="glyphicon glyphicon-plus"></span> New Scenario</a>
</li> </li>
{% endif %} {% endif %}

View File

@ -8,7 +8,17 @@
<i class="glyphicon glyphicon-plus"></i> Add Structure</a> <i class="glyphicon glyphicon-plus"></i> Add Structure</a>
</li> </li>
<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> <i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
</li> </li>
{% endif %} {% endif %}

View File

@ -4,7 +4,11 @@
<i class="glyphicon glyphicon-edit"></i> Edit Compound Structure</a> <i class="glyphicon glyphicon-edit"></i> Edit Compound Structure</a>
</li> </li>
<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> <i class="glyphicon glyphicon-trash"></i> Delete Compound Structure</a>
</li> </li>
{% endif %} {% 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 %} {% if meta.can_edit %}
<li> <li>
<a role="button" data-toggle="modal" data-target="#delete_group_modal"> <a role="button" data-toggle="modal" data-target="#edit_group_member_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> <i class="glyphicon glyphicon-trash"></i> Add/Remove Member</a>
</li> </li>
<li>
<a role="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Group</a>
</li>
{% endif %} {% endif %}

View File

@ -1,6 +1,6 @@
{% if meta.can_edit %} {% if meta.can_edit %}
<li> <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> <i class="glyphicon glyphicon-trash"></i> Delete Model</a>
</li> </li>
{% endif %} {% endif %}

View File

@ -4,7 +4,11 @@
<i class="glyphicon glyphicon-edit"></i> Edit Node</a> <i class="glyphicon glyphicon-edit"></i> Edit Node</a>
</li> </li>
<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> <i class="glyphicon glyphicon-trash"></i> Delete Node</a>
</li> </li>
{% endif %} {% endif %}

View File

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

View File

@ -8,25 +8,40 @@
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a> <i class="glyphicon glyphicon-plus"></i> Add Reaction</a>
</li> </li>
<li role="separator" class="divider"></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> <li>
<a class="button" data-toggle="modal" data-target="#edit_pathway_modal"> <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>
{# <li>#} <li>
{# <a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal">#} <a role="button" data-toggle="modal" data-target="#set_scenario_modal">
{# <i class="glyphicon glyphicon-plus"></i> Calculate Compound Properties</a>#} <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
{# </li>#} </li>
{# <li>#}
{# <a class="button" data-toggle="modal" data-target="#add_pathway_edge_modal">#}
{# <i class="glyphicon glyphicon-plus"></i> Calculate Compound Properties</a>#}
{# </li>#}
<li role="separator" class="divider"></li> <li role="separator" class="divider"></li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#delete_pathway_node_modal"> <a class="button" data-toggle="modal" data-target="#delete_pathway_node_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a> <i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
</li> </li>
<li> <li>
<a class="button" data-toggle="modal" data-target="#delete_pathway_edge_modal"> <a class="button" data-toggle="modal" data-target="#delete_pathway_edge_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a> <i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
</li> </li>
<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> <i class="glyphicon glyphicon-trash"></i> Delete Pathway</a>
</li> </li>
{% endif %} {% endif %}

View File

@ -4,7 +4,17 @@
<i class="glyphicon glyphicon-edit"></i> Edit Reaction</a> <i class="glyphicon glyphicon-edit"></i> Edit Reaction</a>
</li> </li>
<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> <i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
</li> </li>
{% endif %} {% endif %}

View File

@ -3,4 +3,18 @@
<a role="button" data-toggle="modal" data-target="#edit_rule_modal"> <a role="button" data-toggle="modal" data-target="#edit_rule_modal">
<i class="glyphicon glyphicon-edit"></i> Edit Rule</a> <i class="glyphicon glyphicon-edit"></i> Edit Rule</a>
</li> </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 %} {% endif %}

View File

@ -1,2 +1,14 @@
{% if meta.can_edit %} {% 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 %} {% endif %}

View File

@ -16,7 +16,7 @@
{# <i class="glyphicon glyphicon-console"></i> Manage API Token</a>#} {# <i class="glyphicon glyphicon-console"></i> Manage API Token</a>#}
{# </li>#} {# </li>#}
<li> <li>
<a 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> <i class="glyphicon glyphicon-trash"></i> Delete Account</a>
</li> </li>
{% endif %} {% endif %}

View File

@ -2,27 +2,21 @@
{% load static %} {% load static %}
{% block content %} {% block content %}
{% if reviewed_objects.count > 50 or unreviewed_objects.count > 50 %} {% if object_type != 'package' %}
{% if object_type != 'package' %} <div>
<div id="load-remaining-button-div"> <div id="load-all-error" style="display: none;">
<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 <div class="alert alert-danger" role="alert">
</button> <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<p></p> <span class="sr-only">Error:</span>
<div id="load-all-loading"></div> Getting objects failed!
<p></p> </div>
<div id="load-all-error" style="display: none;"> </div>
<div class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">Error:</span>
Getting objects failed!
</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"
<p></p> style="display: none;">
</div> <p></p>
{% endif %} </div>
{% endif %} {% endif %}
{% block action_modals %} {% block action_modals %}
{% if object_type == 'package' %} {% if object_type == 'package' %}
@ -248,21 +242,34 @@
</ul> </ul>
{% endif %} {% endif %}
</div> </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> </div>
<script> <script>
$(function () { $(function () {
$('#modal-form-delete-submit').on('click', function (e) {
e.preventDefault();
$('#modal-form-delete').submit();
});
$('#object-search').show(); $('#object-search').show();
if ($('#load-remaining').length) { {% if object_type != 'package' %}
$('#load-remaining').on('click', function () { setTimeout(function () {
makeLoadingGif("#load-all-loading", "{% static '/images/wait.gif' %}");
$('#load-all-error').hide(); $('#load-all-error').hide();
$.getJSON('?all=true', function (resp) { $.getJSON('?all=true', function (resp) {
@ -272,20 +279,26 @@
for (o in resp.objects) { for (o in resp.objects) {
obj = resp.objects[o]; obj = resp.objects[o];
if (obj.reviewed) { 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 { } else {
$('#UnreviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + '</a>'); $('#UnreviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + '</a>');
} }
} }
$('#load-all-loading').empty(); $('#load-all-loading').hide();
$('#load-remaining').hide(); $('#load-remaining').hide();
}).fail(function (resp) { }).fail(function (resp) {
$('#load-all-loading').empty(); $('#load-all-loading').hide();
$('#load-all-error').show(); $('#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 () { $('#object-search').on('keyup', function () {
let query = $(this).val().toLowerCase(); let query = $(this).val().toLowerCase();

View File

@ -3,7 +3,12 @@
{% load static %} {% load static %}
<head> <head>
<title>{{ title }}</title> <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 #} {# TODO use bundles from bootstrap 3.3.7 #}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script> <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"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
@ -51,7 +56,7 @@
(function () { (function () {
var u = "//matomo.envipath.com/"; var u = "//matomo.envipath.com/";
_paq.push(['setTrackerUrl', u + 'matomo.php']); _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]; var d = document, g = d.createElement('script'), s = d.getElementsByTagName('script')[0];
g.async = true; g.async = true;
g.src = u + 'matomo.js'; g.src = u + 'matomo.js';
@ -83,21 +88,26 @@
<!-- Collect the nav links, forms, and other content for toggling --> <!-- Collect the nav links, forms, and other content for toggling -->
<div class="collapse navbar-collapse collapse-framework navbar-collapse-framework" id="navbarCollapse"> <div class="collapse navbar-collapse collapse-framework navbar-collapse-framework" id="navbarCollapse">
<ul class="nav navbar-nav navbar-nav-framework"> <ul class="nav navbar-nav navbar-nav-framework">
<li class="dropdown"> <li>
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Predict Pathway<b class="caret"></b></a> <a class="button" data-toggle="modal" data-target="#predict_modal">
<ul role="menu" class="dropdown-menu"> Predict Pathway
<li> </a>
<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>
{# <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 }}/package" id="packageLink">Package</a></li>
<li><a href="{{ meta.server_url }}/search" id="searchLink">Search</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> <li><a href="{{ meta.server_url }}/model" id="modelLink">Modelling</a></li>
@ -119,7 +129,7 @@
</ul> </ul>
<ul class="nav navbar-nav navbar-right navbar-nav-framework navbar-right-framework"> <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"> <li class="dropdown">
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Info <b class="caret"></b></a> <a data-toggle="dropdown" class="dropdown-toggle" href="#">Info <b class="caret"></b></a>
<ul role="menu" class="dropdown-menu"> <ul role="menu" class="dropdown-menu">
@ -192,6 +202,23 @@
{% endif %} {% endif %}
{% block content %} {% block content %}
{% endblock 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> </div>
<!-- FOOTER --> <!-- FOOTER -->

View File

@ -143,6 +143,14 @@
} }
$(function () { $(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 // Code that should be executed once DOM is ready goes here
$('#dropdown-predict').on('click', actionDropdownClicked); $('#dropdown-predict').on('click', actionDropdownClicked);
$('#dropdown-search').on('click', actionDropdownClicked); $('#dropdown-search').on('click', actionDropdownClicked);

View File

@ -30,7 +30,7 @@
.center-button { .center-button {
position: absolute; position: absolute;
top: 50%; top: 70%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 1; z-index: 1;
@ -38,7 +38,7 @@
.center-message { .center-message {
position: absolute; position: absolute;
top: 40%; top: 35%;
left: 50%; left: 50%;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
z-index: 1; z-index: 1;

View File

@ -16,14 +16,14 @@
<div class="jumbotron">Create a new Model to <div class="jumbotron">Create a new Model to
limit the number of degradation products in the limit the number of degradation products in the
prediction. You just need to set a name and the packages 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 you want the object to be based on. There are multiple types of models available.
default options suggested by us, simply click Submit, For additional information have a look at our
otherwise click Advanced Options. <a target="_blank" href="https://wiki.envipath.org/index.php/relative-reasoning" role="button">wiki &gt;&gt;</a>
</div> </div>
<label for="name">Name</label> <label for="model-name">Name</label>
<input id="name" name="model-name" class="form-control" placeholder="Name"/> <input id="model-name" name="model-name" class="form-control" placeholder="Name"/>
<label for="description">Description</label> <label for="model-description">Description</label>
<input id="description" name="model-description" class="form-control" <input id="model-description" name="model-description" class="form-control"
placeholder="Description"/> placeholder="Description"/>
<label for="model-type">Model Type</label> <label for="model-type">Model Type</label>
<select id="model-type" name="model-type" class="form-control" data-width='100%'> <select id="model-type" name="model-type" class="form-control" data-width='100%'>
@ -35,7 +35,7 @@
<!-- ML Based Form--> <!-- ML Based Form-->
<div id="ml-relative-reasoning-specific-form"> <div id="ml-relative-reasoning-specific-form">
<!-- Rule Packages --> <!-- 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" <select id="ml-relative-reasoning-rule-packages" name="ml-relative-reasoning-rule-packages"
data-actions-box='true' class="form-control" multiple data-width='100%'> data-actions-box='true' class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
@ -53,7 +53,7 @@
{% endfor %} {% endfor %}
</select> </select>
<!-- Data Packages --> <!-- 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" <select id="ml-relative-reasoning-data-packages" name="ml-relative-reasoning-data-packages"
data-actions-box='true' class="form-control" multiple data-width='100%'> data-actions-box='true' class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
@ -77,22 +77,24 @@
class="form-control"> class="form-control">
<option value="MACCS" selected>MACCS Fingerprinter</option> <option value="MACCS" selected>MACCS Fingerprinter</option>
</select> </select>
{% if 'plugins' in meta.enabled_features %} {% if meta.enabled_features.PLUGINS and additional_descriptors %}
<!-- Property Plugins go here --> <!-- Property Plugins go here -->
<label for="ml-relative-reasoning-additional-fingerprinter">Fingerprinter</label> <label for="ml-relative-reasoning-additional-fingerprinter">Additional Fingerprinter / Descriptors</label>
<select id="ml-relative-reasoning-additional-fingerprinter" <select id="ml-relative-reasoning-additional-fingerprinter" name="ml-relative-reasoning-additional-fingerprinter" class="form-control">
name="ml-relative-reasoning-additional-fingerprinter" <option disabled selected>Select Additional Fingerprinter / Descriptor</option>
class="form-control"> {% for k, v in additional_descriptors.items %}
<option value="{{ v }}">{{ k }}</option>
{% endfor %}
</select> </select>
{% endif %} {% endif %}
<label for="ml-relative-reasoning-threshold">Threshold</label> <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" id="ml-relative-reasoning-threshold"
name="ml-relative-reasoning-threshold" class="form-control"> name="ml-relative-reasoning-threshold" class="form-control">
<!-- Evaluation --> <!-- Evaluation -->
<label for="ml-relative-reasoning-evaluation-packages">Evaluation Packages</label>
<label>Evaluation Packages</label><br>
<select id="ml-relative-reasoning-evaluation-packages" name="ml-relative-reasoning-evaluation-packages" <select id="ml-relative-reasoning-evaluation-packages" name="ml-relative-reasoning-evaluation-packages"
data-actions-box='true' class="form-control" multiple data-width='100%'> data-actions-box='true' class="form-control" multiple data-width='100%'>
<option disabled>Reviewed Packages</option> <option disabled>Reviewed Packages</option>
@ -110,6 +112,26 @@
{% endfor %} {% endfor %}
</select> </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> </div>
<!-- Rule Based Based Form--> <!-- Rule Based Based Form-->
<div id="rule-based-relative-reasoning-specific-form"> <div id="rule-based-relative-reasoning-specific-form">
@ -118,47 +140,9 @@
<!-- EnviFormer--> <!-- EnviFormer-->
<div id="enviformer-specific-form"> <div id="enviformer-specific-form">
<label for="enviformer-threshold">Threshold</label> <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"> name="enviformer-threshold" class="form-control">
</div> </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> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@ -179,6 +163,9 @@ $(function() {
$("#ml-relative-reasoning-rule-packages").selectpicker(); $("#ml-relative-reasoning-rule-packages").selectpicker();
$("#ml-relative-reasoning-data-packages").selectpicker(); $("#ml-relative-reasoning-data-packages").selectpicker();
$("#ml-relative-reasoning-evaluation-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 // On change hide all and show only selected
$("#model-type").change(function() { $("#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"> aria-hidden="true">
<div class="modal-dialog modal-lg"> <div class="modal-dialog modal-lg">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<button type="button" class="close" data-dismiss="modal"> <button type="button" class="close" data-dismiss="modal">
<span aria-hidden="true">&times;</span> <span <span aria-hidden="true">&times;</span>
class="sr-only">Close</span> <span class="sr-only">Close</span>
</button> </button>
<h4 class="js-title-step"></h4> <h4 class="modal-title">New Scenario</h4>
</div> </div>
<form id="base-scenario-form" accept-charset="UTF-8" action="" data-remote="true" method="POST"> <div class="modal-body">
<div class="modal-body hide" data-step="1" data-title="New Scenario - Step 1"> <form id="new_scenario_form" accept-charset="UTF-8" action="{{ meta.current_package.url }}/scenario"
<div class="jumbotron">Please enter name, description, data-remote="true" method="post">
and date of scenario. Date should be associated to the {% csrf_token %}
data, not the current date. For example, this could <div class="jumbotron">Please enter name, description, and date of scenario. Date should be
reflect the publishing date of a study. You can leave associated to the data, not the current date. For example, this could reflect the publishing
all fields but the name empty and fill them in date of a study. You can leave all fields but the name empty and fill them in later.
later. <a target="_blank" href="https://wiki.envipath.org/index.php/scenario" role="button">wiki
&gt;&gt;</a>
</div> </div>
<label for="name">Name</label> <label for="scenario-name">Name</label>
<input id="name" name="studyname" placeholder="Name" class="form-control"/> <input id="scenario-name" name="scenario-name" class="form-control" placeholder="Name"/>
<label for="name">Description</label> <label for="scenario-description">Description</label>
<input id="description" name="studydescription" placeholder="Description" class="form-control"/> <input id="scenario-description" name="scenario-description" class="form-control"
placeholder="Description"/>
<label id="dateField" for="dateYear">Date</label> <label id="dateField" for="dateYear">Date</label>
<table> <table>
<tr> <tr>
<th> <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>
<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=""> class="form-control" placeholder="MM" align="">
</th> </th>
<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"> placeholder="DD">
</th> </th>
</tr> </tr>
</table> </table>
</div> <label for="scenario-type">Scenario Type</label>
<div class="modal-body hide" data-step="2" data-title="New Scenario - Step 2"> <select id="scenario-type" name="scenario-type" class="form-control" data-width='100%'>
<div class="jumbotron"> <option value="empty" selected>Empty Scenario</option>
Do you want to create an empty scenario and fill it {% for k, v in scenario_types.items %}
with your own set of attributes, or fill in a <option value="{{ v.name }}">{{ k }}</option>
pre-defined set of attributes for soil, sludge or sediment {% endfor %}
data? </select>
</div>
<div class="radio"> {% for type in scenario_types.values %}
<label> <div id="{{ type.name }}-specific-inputs">
<input type="radio" name="type" id="radioEmpty" checked>Empty Scenario {% for widget in type.widgets %}
</label> {{ widget|safe }}
</div> {% endfor %}
<div class="radio"> </div>
<label> {% endfor %}
<input type="radio" name="type" id="radioSoil" value="soil" >Soil Data
</label> </form>
</div> </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>
</form>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-default js-btn-step pull-left" data-orientation="cancel" data-dismiss="modal"></button> <a id="new_scenario_modal_form_submit" class="btn btn-primary" href="#">Submit</a>
<button type="button" class="btn btn-default js-btn-step" data-orientation="previous"></button> <button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-default js-btn-step"
data-orientation="next" id="nextbutton"></button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<script>
$(function () {
// 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();
});
<p></p> $('#new_scenario_modal_form_submit').on('click', function (e) {
<div id="scenariocontent"></div> e.preventDefault();
$('#new_scenario_form').submit();
});
<!--Template index -->
<script language="javascript">
$(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,
}); });
});
</script> </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> <option value="{{ e.url }}">{{ e.edge_label.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" id="hidden" name="hidden" value="delete-edge"/> <input type="hidden" id="hidden" name="hidden" value="delete"/>
</form> </form>
<p></p> <p></p>
<div id="delete_pathway_edge_image"></div> <div id="delete_pathway_edge_image"></div>

View File

@ -22,7 +22,7 @@
<option value="{{ n.url }}">{{ n.default_node_label.name }}</option> <option value="{{ n.url }}">{{ n.default_node_label.name }}</option>
{% endfor %} {% endfor %}
</select> </select>
<input type="hidden" id="hidden" name="hidden" value="delete-node"/> <input type="hidden" id="hidden" name="hidden" value="delete"/>
</form> </form>
<p></p> <p></p>
<div id="delete_pathway_node_image"></div> <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 %} {% load static %}
<!-- Delete Pathway --> <!-- Download Pathway -->
<div id="delete_pathway_modal" class="modal" tabindex="-1"> <div id="download_pathway_modal" class="modal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
Deletes the Pathway together with all Nodes and Edges. By clicking on Download the Pathway will be converted into a CSV and directly downloaded.
<form id="delete-pathway-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post"> <form id="download-pathway-modal-form" accept-charset="UTF-8" action="{{ pathway.url }}"
{% csrf_token %} data-remote="true" method="GET">
<input type="hidden" id="hidden" name="hidden" value="delete-pathway"/> <input type="hidden" name="download" value="true"/>
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <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> </div>
</div> </div>
@ -26,9 +26,10 @@
<script> <script>
$(function () { $(function () {
$('#delete-pathway-modal-submit').click(function (e) { $('#download-pathway-modal-submit').click(function (e) {
e.preventDefault(); 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>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <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> </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 %} {% load static %}
<!-- Delete Compound --> <!-- Publish a Package -->
<div id="delete_compound_modal" class="modal" tabindex="-1"> <div id="publish_package_modal" class="modal" tabindex="-1">
<div class="modal-dialog"> <div class="modal-dialog">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <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"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
Deletes the Compound and associated Structures. <p>Clicking on Publish will make this Package publicly available!</p>
<form id="delete-compound-modal-form" accept-charset="UTF-8" action="" data-remote="true" method="post"> <form id="publish-package-modal-form" accept-charset="UTF-8" action="{{ current_package.url }}" data-remote="true" method="post">
{% csrf_token %} {% csrf_token %}
<input type="hidden" id="hidden" name="hidden" value="delete-compound"/> <input type="hidden" name="hidden" value="publish-package">
</form> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button> <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> </div>
</div> </div>
@ -26,9 +26,9 @@
<script> <script>
$(function() { $(function() {
$('#delete-compound-modal-submit').click(function(e){ $('#publish-package-modal-form-submit').click(function(e){
e.preventDefault(); 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%" <iframe id="predict-modal-ketcher" src="{% static '/js/ketcher2/ketcher.html' %}" width="100%"
height="510"></iframe> height="510"></iframe>
</div> </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> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">

View File

@ -4,6 +4,9 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_rule_modal.html" %} {% 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 %} {% endblock action_modals %}
<div class="panel-group" id="rule-detail"> <div class="panel-group" id="rule-detail">
@ -49,7 +52,22 @@
</div> </div>
</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 --> <!-- EC Numbers -->
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">

View File

@ -5,7 +5,9 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_compound_modal.html" %} {% include "modals/objects/edit_compound_modal.html" %}
{% include "modals/objects/add_structure_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 %} {% endblock action_modals %}
<div class="panel-group" id="compound-detail"> <div class="panel-group" id="compound-detail">
@ -134,7 +136,69 @@
</div> </div>
</div> </div>
{% endif %} {% 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 --> <!-- 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> </div>

View File

@ -4,6 +4,8 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_compound_structure_modal.html" %} {% 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 %} {% endblock action_modals %}
<div class="panel-group" id="compound-structure-detail"> <div class="panel-group" id="compound-structure-detail">
@ -54,6 +56,22 @@
</div> </div>
</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 --> <!-- Reactions -->
<!-- Pathways --> <!-- Pathways -->

View File

@ -4,7 +4,8 @@
{% block action_modals %} {% block action_modals %}
{# {% include "modals/objects/edit_edge_modal.html" %}#} {# {% 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 %} {% endblock action_modals %}
<div class="panel-group" id="edge-detail"> <div class="panel-group" id="edge-detail">
@ -19,7 +20,7 @@
style="padding-right:1em"></span></a> style="padding-right:1em"></span></a>
<ul id="actionsList" class="dropdown-menu"> <ul id="actionsList" class="dropdown-menu">
{% block actions %} {% block actions %}
{# {% include "actions/objects/edge.html" %}#} {% include "actions/objects/edge.html" %}
{% endblock %} {% endblock %}
</ul> </ul>
</div> </div>
@ -103,6 +104,21 @@
</div> </div>
{% endif %} {% 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> </div>
{% endblock content %} {% endblock content %}

View File

@ -5,7 +5,7 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_group_modal.html" %} {% include "modals/objects/edit_group_modal.html" %}
{% include "modals/objects/edit_group_member_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 %} {% endblock action_modals %}
<div class="panel-group" id="package-detail"> <div class="panel-group" id="package-detail">

View File

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

View File

@ -4,7 +4,8 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_node_modal.html" %} {% 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 %} {% endblock action_modals %}
<div class="panel-group" id="node-detail"> <div class="panel-group" id="node-detail">
@ -69,7 +70,34 @@
</div> </div>
</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>
</div> </div>
{% endblock content %} {% endblock content %}

View File

@ -5,8 +5,9 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_package_modal.html" %} {% include "modals/objects/edit_package_modal.html" %}
{% include "modals/objects/edit_package_permissions_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/set_license_modal.html" %}
{% include "modals/objects/delete_package_modal.html" %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}
<div class="panel-group" id="package-detail"> <div class="panel-group" id="package-detail">
@ -52,23 +53,5 @@
</div> </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> </div>
{% endblock content %} {% endblock content %}

View File

@ -4,16 +4,22 @@
<script src="https://d3js.org/d3.v7.min.js"></script> <script src="https://d3js.org/d3.v7.min.js"></script>
<style> <style>
svg { #vizdiv {
width: 100%; width: 100%;
height: 600px; height: 600px;
background: white;
}
#pwsvg {
width: 100%;
height: 100%;
color: red; color: red;
} }
.link { .link {
stroke: #999; stroke: #999;
stroke-opacity: 0.6; stroke-opacity: 0.6;
marker-end: url(#arrow); //marker-end: url(#arrow);
} }
.link_no_arrow { .link_no_arrow {
@ -31,6 +37,31 @@
stroke-width: 1.5px; 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 { .highlighted {
stroke: red; stroke: red;
stroke-width: 3px; stroke-width: 3px;
@ -50,10 +81,13 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/add_pathway_node_modal.html" %} {% include "modals/objects/add_pathway_node_modal.html" %}
{% include "modals/objects/add_pathway_edge_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/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_node_modal.html" %}
{% include "modals/objects/delete_pathway_edge_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 %} {% endblock action_modals %}
<p></p> <p></p>
@ -79,8 +113,7 @@
<ul class="nav navbar-nav"> <ul class="nav navbar-nav">
<li class="dropdown requiresWritePerm"> <li class="dropdown requiresWritePerm">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" <a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button"
aria-haspopup="true" aria-haspopup="true" aria-expanded="false">
aria-expanded="false">
<span class="glyphicon glyphicon-edit"></span> <span class="glyphicon glyphicon-edit"></span>
Edit Edit
<span class="caret"></span></a> <span class="caret"></span></a>
@ -90,11 +123,26 @@
{% endblock %} {% endblock %}
</ul> </ul>
</li> </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>
<ul class="nav navbar-nav navbar-right"> <ul class="nav navbar-nav navbar-right">
<li> <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> <span class="glyphicon glyphicon-fullscreen"></span>
Fullscreen Fullscreen
</a> </a>
@ -125,16 +173,24 @@
</div> </div>
</nav> </nav>
<div id="vizdiv"> <div id="vizdiv" >
<svg width="2000" height="2000"> <svg id="pwsvg">
{% if debug %} {% if debug %}
<rect width="100%" height="100%" fill="aliceblue"/> <rect width="100%" height="100%" fill="aliceblue"/>
{% endif %} {% endif %}
<defs> <defs>
<marker id="arrow" viewBox="0 0 10 10" refX="43" refY="5" markerWidth="6" markerHeight="6" <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"/> <path d="M 0 0 L 10 5 L 0 10 z" fill="#999"/>
</marker> </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> </defs>
<g id="zoomable"></g> <g id="zoomable"></g>
</svg> </svg>
@ -153,13 +209,29 @@
</div> </div>
</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 %} {% if pathway.setting %}
<div class="panel panel-default panel-heading list-group-item" style="background-color:silver"> <div class="panel panel-default panel-heading list-group-item" style="background-color:silver">
<h4 class="panel-title"> <h4 class="panel-title">
<a id="pathwaySettingLink" data-toggle="collapse" data-parent="#pathwayAccordion" <a id="pathwaySettingLink" data-toggle="collapse" data-parent="#pathwayAccordion"
href="#pathwaySetting">Setting</a></h4> href="#pathwaySetting">Setting</a></h4>
</div> </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"> <div class="panel-body list-group-item" id="pathwaySettingContent">
<table class="table table-bordered table-hover"> <table class="table table-bordered table-hover">
<tr style="background-color: rgba(0, 0, 0, 0.08);"> <tr style="background-color: rgba(0, 0, 0, 0.08);">
@ -245,6 +317,8 @@
</div> </div>
</div> </div>
<script> <script>
// Globla switch for app domain view
var appDomainViewEnabled = false;
function goFullscreen(id) { function goFullscreen(id) {
var element = document.getElementById(id); var element = document.getElementById(id);
@ -266,6 +340,51 @@
// TODO fix somewhere else... // TODO fix somewhere else...
var newDesc = transformReferences($('#DescriptionContent')[0].innerText); var newDesc = transformReferences($('#DescriptionContent')[0].innerText);
$('#DescriptionContent').html(newDesc); $('#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> </script>

View File

@ -4,7 +4,9 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_reaction_modal.html" %} {% 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 %} {% endblock action_modals %}
<div class="panel-group" id="reaction-detail"> <div class="panel-group" id="reaction-detail">
@ -118,9 +120,70 @@
{% endfor %} {% endfor %}
</div> </div>
</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> </div>
{% endif %} {% 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> </div>
{% endblock content %} {% endblock content %}

View File

@ -3,7 +3,9 @@
{% block content %} {% block content %}
{% block action_modals %} {% 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 %} {% endblock action_modals %}
<div class="panel-group" id="scenario-detail"> <div class="panel-group" id="scenario-detail">
<div class="panel panel-default"> <div class="panel panel-default">
@ -24,6 +26,18 @@
</div> </div>
</div> </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"> <div class="table-responsive">
<table id="scenario-table" class="table table-bordered table-striped table-hover"> <table id="scenario-table" class="table table-bordered table-striped table-hover">
<tbody> <tbody>
@ -31,18 +45,42 @@
<th>Property</th> <th>Property</th>
<th>Value</th> <th>Value</th>
<th>Unit</th> <th>Unit</th>
{% if meta.can_edit %}
<th>Remove</th> <th>Remove</th>
{% endif %}
</tr> </tr>
{% for ai in scenario.get_additional_information %} {% for ai in scenario.get_additional_information %}
<tr> <tr>
<td>{{ ai.property_name|safe }} </td> <td> {{ ai.property_name|safe }} </td>
<td> {{ ai.property_data|safe }} </td> <td> {{ ai.property_data|safe }} </td>
<td> {{ ai.property_unit|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> </tr>
{% endfor %} {% 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> </tbody>
</table> </table>
</div> </div>

View File

@ -4,6 +4,9 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_rule_modal.html" %} {% 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 %} {% endblock action_modals %}
<div class="panel-group" id="rule-detail"> <div class="panel-group" id="rule-detail">

View File

@ -7,7 +7,7 @@
{% include "modals/objects/edit_password_modal.html" %} {% include "modals/objects/edit_password_modal.html" %}
{% include "modals/collections/new_prediction_setting_modal.html" %} {% include "modals/collections/new_prediction_setting_modal.html" %}
{% include "modals/objects/manage_api_token_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 %} {% endblock action_modals %}
<div class="panel-group" id="user-detail"> <div class="panel-group" id="user-detail">

View File

@ -79,6 +79,10 @@
allEmpty = true; allEmpty = true;
for (key in data) { for (key in data) {
if (key === 'searchterm') {
continue;
}
if (data[key].length < 1) { if (data[key].length < 1) {
continue; continue;
} }
@ -176,8 +180,16 @@
$("#selPackages").selectpicker(); $("#selPackages").selectpicker();
$("#search-button").on("click", search); $("#search-button").on("click", search);
$("#searchbar").on("keydown", function (e) {
if (e.key === "Enter") {
e.preventDefault();
search(e);
}
});
}); });
{% if search_result %} {% if search_result %}
$('#searchbar').val('{{ search_result.searchterm }}')
handleSearchResponse("results", {{ search_result|safe }}); handleSearchResponse("results", {{ search_result|safe }});
{% endif %} {% endif %}
</script> </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 @classmethod
def tearDownClass(cls): def tearDownClass(cls):
from collections import Counter print(f"\nTotal Errors across Rules {len(cls.error_smiles)}")
# print(Counter(cls.error_smiles)) # print(cls.error_smiles)
pass
def tearDown(self): def tearDown(self):
print(f"\nTotal errors {self.total_errors}") 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']): for comp, ambit_prod in zip(bt_rule['compounds'], bt_rule['products']):
smi = comp['smiles'] smi = comp['smiles']
products = FormatConverter.apply(smi, smirks, preprocess_smiles=True, bracketize=False) products = FormatConverter.apply(smi, smirks)
all_rdkit_prods = [] all_rdkit_prods = []
for ps in products: for ps in products:
@ -53,15 +52,15 @@ class RuleApplicationTest(TestCase):
# TODO mode "intersection" # TODO mode "intersection"
# partial_res = (len(set(ambit_smiles).intersection(set(rdkit_smiles))) > 0) or (len(ambit_smiles) == 0) # 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" # TODO mode = "full ambit"
# partial_res = len(set(ambit_smiles).intersection(set(rdkit_smiles))) == len(ambit_smiles) # partial_res = len(set(ambit_smiles).intersection(set(rdkit_smiles))) == len(ambit_smiles)
# FAILED (failures=52) # FAILED (failures=44)
# TODO mode = "equality" # TODO mode = "equality"
partial_res = set(ambit_smiles) == set(rdkit_smiles) partial_res = set(ambit_smiles) == set(rdkit_smiles)
# FAILED (failures=71) # FAILED (failures=64)
if len(ambit_smiles) and not partial_res: if len(ambit_smiles) and not partial_res:
print(f""" print(f"""

View File

@ -8,10 +8,12 @@ from indigo import Indigo, IndigoException, IndigoObject
from indigo.renderer import IndigoRenderer from indigo.renderer import IndigoRenderer
from rdkit import Chem from rdkit import Chem
from rdkit import RDLogger from rdkit import RDLogger
from rdkit.Chem import MACCSkeys from rdkit.Chem import MACCSkeys, Descriptors
from rdkit.Chem import rdChemReactions from rdkit.Chem import rdChemReactions
from rdkit.Chem.Draw import rdMolDraw2D from rdkit.Chem.Draw import rdMolDraw2D
from rdkit.Chem.MolStandardize import rdMolStandardize from rdkit.Chem.MolStandardize import rdMolStandardize
from rdkit.Chem.rdmolops import GetMolFrags
from rdkit.Contrib.IFG import ifg
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
RDLogger.DisableLog('rdApp.*') RDLogger.DisableLog('rdApp.*')
@ -65,6 +67,18 @@ class PredictionResult(object):
class FormatConverter(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 @staticmethod
def from_smiles(smiles): def from_smiles(smiles):
return Chem.MolFromSmiles(smiles) return Chem.MolFromSmiles(smiles)
@ -77,6 +91,10 @@ class FormatConverter(object):
def InChIKey(smiles): def InChIKey(smiles):
return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles)) return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles))
@staticmethod
def InChI(smiles):
return Chem.MolToInchi(FormatConverter.from_smiles(smiles))
@staticmethod @staticmethod
def canonicalize(smiles: str): def canonicalize(smiles: str):
return FormatConverter.to_smiles(FormatConverter.from_smiles(smiles), canonical=True) return FormatConverter.to_smiles(FormatConverter.from_smiles(smiles), canonical=True)
@ -87,6 +105,21 @@ class FormatConverter(object):
bitvec = MACCSkeys.GenMACCSKeys(mol) bitvec = MACCSkeys.GenMACCSKeys(mol)
return bitvec.ToList() 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 @staticmethod
def to_svg(smiles, mol_size=(200, 150), kekulize=True): def to_svg(smiles, mol_size=(200, 150), kekulize=True):
mol = FormatConverter.from_smiles(smiles) mol = FormatConverter.from_smiles(smiles)
@ -131,6 +164,24 @@ class FormatConverter(object):
# TODO call to AMBIT Service # TODO call to AMBIT Service
return smiles 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 @staticmethod
def standardize(smiles): def standardize(smiles):
# Taken from https://bitsilla.com/blog/2021/06/standardizing-a-molecule-using-rdkit/ # Taken from https://bitsilla.com/blog/2021/06/standardizing-a-molecule-using-rdkit/
@ -180,54 +231,6 @@ class FormatConverter(object):
atom.UpdatePropertyCache() atom.UpdatePropertyCache()
return mol 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 @staticmethod
def is_valid_smirks(smirks: str) -> bool: def is_valid_smirks(smirks: str) -> bool:
try: try:
@ -237,7 +240,7 @@ class FormatConverter(object):
return False return False
@staticmethod @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']: standardize: bool = True, kekulize: bool = True) -> List['ProductSet']:
logger.debug(f'Applying {smirks} on {smiles}') logger.debug(f'Applying {smirks} on {smiles}')
@ -266,8 +269,10 @@ class FormatConverter(object):
for product in product_set: for product in product_set:
try: try:
Chem.SanitizeMol(product) Chem.SanitizeMol(product)
product = GetMolFrags(product, asMols=True)
product = FormatConverter.standardize(Chem.MolToSmiles(product)) for p in product:
p = FormatConverter.standardize(Chem.MolToSmiles(p))
prods.append(p)
# if kekulize: # if kekulize:
# # from rdkit.Chem import MolStandardize # # from rdkit.Chem import MolStandardize
@ -292,13 +297,12 @@ class FormatConverter(object):
# # bond.SetIsAromatic(False) # # bond.SetIsAromatic(False)
# Chem.Kekulize(product) # Chem.Kekulize(product)
prods.append(product)
except ValueError as e: except ValueError as e:
logger.error(f'Sanitizing and converting failed:\n{e}') logger.error(f'Sanitizing and converting failed:\n{e}')
continue continue
# TODO doc! if len(prods):
if len(prods) and len(prods) == len(product_set):
ps = ProductSet(prods) ps = ProductSet(prods)
pss.add(ps) pss.add(ps)
@ -651,20 +655,23 @@ class IndigoUtils(object):
environment.add(mappedAtom.index()) environment.add(mappedAtom.index())
for k, v in functional_groups.items(): for k, v in functional_groups.items():
try:
sanitized = IndigoUtils.sanitize_functional_group(k)
sanitized = IndigoUtils.sanitize_functional_group(k) query = indigo.loadSmarts(sanitized)
query = indigo.loadSmarts(sanitized) for match in matcher.iterateMatches(query):
if match is not None:
for match in matcher.iterateMatches(query): for atom in query.iterateAtoms():
if match is not None: mappedAtom = match.mapAtom(atom)
if mappedAtom is None or mappedAtom.index() in environment:
continue
for atom in query.iterateAtoms(): counts[mappedAtom.index()] = max(v, counts[mappedAtom.index()])
mappedAtom = match.mapAtom(atom)
if mappedAtom is None or mappedAtom.index() in environment:
continue
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(): for k, v in counts.items():
if is_reaction: if is_reaction:
@ -691,7 +698,10 @@ class IndigoUtils(object):
i.setOption("render-image-size", width, height) i.setOption("render-image-size", width, height)
i.setOption("render-bond-line-width", 2.0) i.setOption("render-bond-line-width", 2.0)
mol = i.loadMolecule(mol_data) if '~' in mol_data:
mol = i.loadSmarts(mol_data)
else:
mol = i.loadMolecule(mol_data)
if len(functional_groups.keys()) > 0: if len(functional_groups.keys()) > 0:
IndigoUtils._colorize(i, mol, functional_groups, False) IndigoUtils._colorize(i, mol, functional_groups, False)

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 from __future__ import annotations
import dataclasses import logging
from abc import ABC, abstractmethod
from collections import defaultdict from collections import defaultdict
from datetime import datetime from datetime import datetime
from typing import List, Optional from typing import List, Dict, Set, Tuple
import numpy as np import numpy as np
from sklearn.base import BaseEstimator, ClassifierMixin from sklearn.base import BaseEstimator, ClassifierMixin
from sklearn.decomposition import PCA from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import accuracy_score from sklearn.metrics import accuracy_score
from sklearn.multioutput import ClassifierChain from sklearn.multioutput import ClassifierChain
from sklearn.preprocessing import StandardScaler from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
# @dataclasses.dataclass logger = logging.getLogger(__name__)
# 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
from dataclasses import dataclass, field from dataclasses import dataclass, field
from utilities.chem import FormatConverter from utilities.chem import FormatConverter, PredictionResult
@dataclass @dataclass
class Compound: class SCompound:
smiles: str smiles: str
uuid: str = field(default=None, compare=False, hash=False) uuid: str = field(default=None, compare=False, hash=False)
@ -53,10 +36,10 @@ class Compound:
@dataclass @dataclass
class Reaction: class SReaction:
educts: List[Compound] educts: List[SCompound]
products: List[Compound] products: List[SCompound]
rule_uuid: str = field(default=None, compare=False, hash=False) rule_uuid: SRule = field(default=None, compare=False, hash=False)
reaction_uuid: str = field(default=None, compare=False, hash=False) reaction_uuid: str = field(default=None, compare=False, hash=False)
def __hash__(self): def __hash__(self):
@ -68,77 +51,304 @@ class Reaction:
return self._hash return self._hash
def __eq__(self, other): def __eq__(self, other):
if not isinstance(other, Reaction): if not isinstance(other, SReaction):
return NotImplemented return NotImplemented
return ( return (
sorted(self.educts, key=lambda x: x.smiles) == sorted(other.educts, key=lambda x: x.smiles) and sorted(self.educts, key=lambda x: x.smiles) == sorted(other.educts, key=lambda x: x.smiles) and
sorted(self.products, key=lambda x: x.smiles) == sorted(other.products, key=lambda x: x.smiles) sorted(self.products, key=lambda x: x.smiles) == sorted(other.products, key=lambda x: x.smiles)
) )
class Dataset(object): @dataclass
class SRule(ABC):
def __init__(self, headers=List['str'], data=List[List[str|int|float]]): @abstractmethod
self.headers = headers def apply(self):
self.data = data
def features(self):
pass
def labels(self):
pass
def to_json(self):
pass
def to_csv(self):
pass
def to_arff(self):
pass pass
@dataclass
class SSimpleRule:
pass
class DatasetGenerator(object):
@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 _block_indices(self, prefix) -> Tuple[int, int]:
indices: List[int] = []
for i, feature in enumerate(self.columns):
if feature.startswith(prefix):
indices.append(i)
return min(indices), max(indices)
def structure_id(self):
return self.data[0][0]
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 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:
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 @staticmethod
def generate_dataset(compounds: List[Compound], reactions: List[Reaction], applicable_rules: 'Rule', def generate_dataset(reactions: List['Reaction'], applicable_rules: List['Rule'], educts_only: bool = True) -> Dataset:
compounds_to_exclude: Optional[Compound] = None, educts_only: bool = False) -> Dataset: _structures = set()
rows = [] for r in reactions:
for e in r.educts.all():
_structures.add(e)
if educts_only: if not educts_only:
compounds = set() for e in r.products:
for r in reactions: _structures.add(e)
for e in r.educts:
compounds.add(e)
compounds = list(compounds)
total = len(compounds) compounds = sorted(_structures, key=lambda x: x.url)
for i, c in enumerate(compounds):
row = [] triggered: Dict[str, Set[str]] = defaultdict(set)
print(f"{i + 1}/{total} - {c.smiles}") observed: Set[str] = set()
for r in applicable_rules:
product_sets = r.rule.apply(c.smiles) # 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: if len(product_sets) == 0:
row.append([])
continue continue
#triggered.add(f"{r.uuid} + {c.uuid}") key = f"{rule.uuid} + {comp.uuid}"
reacts = set()
for ps in product_sets:
products = []
for p in ps:
products.append(Compound(FormatConverter.standardize(p)))
reacts.add(Reaction([c], products, r)) if key in triggered:
row.append(list(reacts)) 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): class SparseLabelECC(BaseEstimator, ClassifierMixin):
@ -166,8 +376,7 @@ class SparseLabelECC(BaseEstimator, ClassifierMixin):
self.keep_columns_.append(col) self.keep_columns_.append(col)
y_reduced = y[:, self.keep_columns_] y_reduced = y[:, self.keep_columns_]
self.chains_ = [ClassifierChain(self.base_clf, order='random', random_state=i) self.chains_ = [ClassifierChain(self.base_clf) for i in range(self.num_chains)]
for i in range(self.num_chains)]
for i, chain in enumerate(self.chains_): for i, chain in enumerate(self.chains_):
print(f"{datetime.now()} fitting {i + 1}/{self.num_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) return accuracy_score(y_true, y_pred, sample_weight=sample_weight)
class ApplicabilityDomain(PCA):
def __init__(self, n_components=5): import copy
super().__init__(n_components=n_components)
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.scaler = StandardScaler()
self.num_neighbours = num_neighbours
self.min_vals = None self.min_vals = None
self.max_vals = None self.max_vals = None
def build(self, X): def build(self, train_dataset: 'Dataset'):
# transform # transform
X_scaled = self.scaler.fit_transform(X) X_scaled = self.scaler.fit_transform(train_dataset.X())
# fit pca # fit pca
X_pca = self.fit_transform(X_scaled) X_pca = self.fit_transform(X_scaled)
self.max_vals = np.max(X_pca, axis=0) self.max_vals = np.max(X_pca, axis=0)
self.min_vals = np.min(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_scaled = self.scaler.transform(instances)
instances_pca = self.transform(instances_scaled) 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 = [] is_applicable = []
for i, instance in enumerate(instances_pca): for i, instance in enumerate(instances_pca):
@ -237,3 +589,17 @@ class ApplicabilityDomain(PCA):
is_applicable[i] = False is_applicable[i] = False
return is_applicable 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))

4
uv.lock generated
View File

@ -426,7 +426,7 @@ requires-dist = [
{ name = "django-ninja", specifier = ">=1.4.1" }, { name = "django-ninja", specifier = ">=1.4.1" },
{ name = "django-polymorphic", specifier = ">=4.1.0" }, { name = "django-polymorphic", specifier = ">=4.1.0" },
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.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 = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
{ name = "epam-indigo", specifier = ">=1.30.1" }, { name = "epam-indigo", specifier = ">=1.30.1" },
{ name = "gunicorn", specifier = ">=23.0.0" }, { name = "gunicorn", specifier = ">=23.0.0" },
@ -443,7 +443,7 @@ requires-dist = [
[[package]] [[package]]
name = "envipy-additional-information" name = "envipy-additional-information"
version = "0.1.0" 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 = [ dependencies = [
{ name = "pydantic" }, { name = "pydantic" },
] ]