11 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
39 changed files with 4215 additions and 308 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

@ -308,11 +308,22 @@ 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 # compile into digestible flags

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),
] ]

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}")
@ -426,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()

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'),
),
]

View File

@ -20,6 +20,7 @@ from django.db import models, transaction
from django.db.models import JSONField, Count, Q, QuerySet from django.db.models import JSONField, Count, Q, QuerySet
from django.utils import timezone from django.utils import timezone
from django.utils.functional import cached_property from django.utils.functional import cached_property
from envipy_additional_information import EnviPyModel
from model_utils.models import TimeStampedModel from model_utils.models import TimeStampedModel
from polymorphic.models import PolymorphicModel from polymorphic.models import PolymorphicModel
from sklearn.metrics import precision_score, recall_score, jaccard_score from sklearn.metrics import precision_score, recall_score, jaccard_score
@ -37,9 +38,8 @@ logger = logging.getLogger(__name__)
class User(AbstractUser): class User(AbstractUser):
email = models.EmailField(unique=True) email = models.EmailField(unique=True)
uuid = models.UUIDField(null=False, blank=False, verbose_name='UUID of this object', unique=True, default=uuid4)
uuid = models.UUIDField(null=False, blank=False, verbose_name='UUID of this object', unique=True, url = models.TextField(blank=False, null=True, verbose_name='URL', unique=True)
default=uuid4)
default_package = models.ForeignKey('epdb.Package', verbose_name='Default Package', null=True, default_package = models.ForeignKey('epdb.Package', verbose_name='Default Package', null=True,
on_delete=models.SET_NULL) on_delete=models.SET_NULL)
default_group = models.ForeignKey('Group', verbose_name='Default Group', null=True, blank=False, default_group = models.ForeignKey('Group', verbose_name='Default Group', null=True, blank=False,
@ -50,8 +50,13 @@ class User(AbstractUser):
USERNAME_FIELD = "email" USERNAME_FIELD = "email"
REQUIRED_FIELDS = ['username'] REQUIRED_FIELDS = ['username']
@property def save(self, *args, **kwargs):
def url(self): if not self.url:
self.url = self._url()
super().save(*args, **kwargs)
def _url(self):
return '{}/user/{}'.format(s.SERVER_URL, self.uuid) return '{}/user/{}'.format(s.SERVER_URL, self.uuid)
def prediction_settings(self): def prediction_settings(self):
@ -169,6 +174,7 @@ class APIToken(TimeStampedModel):
class Group(TimeStampedModel): class Group(TimeStampedModel):
uuid = models.UUIDField(null=False, blank=False, verbose_name='UUID of this object', unique=True, default=uuid4) uuid = models.UUIDField(null=False, blank=False, verbose_name='UUID of this object', unique=True, default=uuid4)
url = models.TextField(blank=False, null=True, verbose_name='URL', unique=True)
name = models.TextField(blank=False, null=False, verbose_name='Group name') name = models.TextField(blank=False, null=False, verbose_name='Group name')
owner = models.ForeignKey("User", verbose_name='Group Owner', on_delete=models.CASCADE) owner = models.ForeignKey("User", verbose_name='Group Owner', on_delete=models.CASCADE)
public = models.BooleanField(verbose_name='Public Group', default=False) public = models.BooleanField(verbose_name='Public Group', default=False)
@ -179,8 +185,13 @@ class Group(TimeStampedModel):
def __str__(self): def __str__(self):
return f"{self.name} (pk={self.pk})" return f"{self.name} (pk={self.pk})"
@property def save(self, *args, **kwargs):
def url(self): if not self.url:
self.url = self._url()
super().save(*args, **kwargs)
def _url(self):
return '{}/group/{}'.format(s.SERVER_URL, self.uuid) return '{}/group/{}'.format(s.SERVER_URL, self.uuid)
@ -476,11 +487,18 @@ class EnviPathModel(TimeStampedModel):
name = models.TextField(blank=False, null=False, verbose_name='Name', default='no name') name = models.TextField(blank=False, null=False, verbose_name='Name', default='no name')
description = models.TextField(blank=False, null=False, verbose_name='Descriptions', default='no description') description = models.TextField(blank=False, null=False, verbose_name='Descriptions', default='no description')
url = models.TextField(blank=False, null=True, verbose_name='URL', unique=True)
kv = JSONField(null=True, blank=True, default=dict) kv = JSONField(null=True, blank=True, default=dict)
@property def save(self, *args, **kwargs):
if not self.url:
self.url = self._url()
super().save(*args, **kwargs)
@abc.abstractmethod @abc.abstractmethod
def url(self): def _url(self):
pass pass
def simple_json(self, include_description=False): def simple_json(self, include_description=False):
@ -516,15 +534,16 @@ class AliasMixin(models.Model):
@transaction.atomic @transaction.atomic
def add_alias(self, new_alias, set_as_default=False): def add_alias(self, new_alias, set_as_default=False):
if set_as_default: if set_as_default:
self.aliases.add(self.name) self.aliases.append(self.name)
self.name = new_alias self.name = new_alias
if new_alias in self.aliases: if new_alias in self.aliases:
self.aliases.remove(new_alias) self.aliases.remove(new_alias)
else: else:
if new_alias not in self.aliases: if new_alias not in self.aliases:
self.aliases.add(new_alias) self.aliases.append(new_alias)
self.aliases = sorted(list(set(self.aliases)), key=lambda x: x.lower())
self.save() self.save()
class Meta: class Meta:
@ -585,8 +604,7 @@ class Package(EnviPathModel):
def models(self): def models(self):
return EPModel.objects.filter(package=self) return EPModel.objects.filter(package=self)
@property def _url(self):
def url(self):
return '{}/package/{}'.format(s.SERVER_URL, self.uuid) return '{}/package/{}'.format(s.SERVER_URL, self.uuid)
def get_applicable_rules(self): def get_applicable_rules(self):
@ -632,8 +650,7 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
def normalized_structure(self): def normalized_structure(self):
return CompoundStructure.objects.get(compound=self, normalized_structure=True) return CompoundStructure.objects.get(compound=self, normalized_structure=True)
@property def _url(self):
def url(self):
return '{}/compound/{}'.format(self.package.url, self.uuid) return '{}/compound/{}'.format(self.package.url, self.uuid)
@transaction.atomic @transaction.atomic
@ -747,6 +764,64 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
return cs return cs
@transaction.atomic
def copy(self, target: 'Package', mapping: Dict):
if self in mapping:
return mapping[self]
new_compound = Compound.objects.create(
package=target,
name=self.name,
description=self.description,
kv=self.kv.copy() if self.kv else {}
)
mapping[self] = new_compound
# Copy compound structures
for structure in self.structures.all():
if structure not in mapping:
new_structure = CompoundStructure.objects.create(
compound=new_compound,
smiles=structure.smiles,
canonical_smiles=structure.canonical_smiles,
inchikey=structure.inchikey,
normalized_structure=structure.normalized_structure,
name=structure.name,
description=structure.description,
kv=structure.kv.copy() if structure.kv else {}
)
mapping[structure] = new_structure
# Copy external identifiers for structure
for ext_id in structure.external_identifiers.all():
ExternalIdentifier.objects.create(
content_object=new_structure,
database=ext_id.database,
identifier_value=ext_id.identifier_value,
url=ext_id.url,
is_primary=ext_id.is_primary
)
if self.default_structure:
new_compound.default_structure = mapping.get(self.default_structure)
new_compound.save()
for a in self.aliases:
new_compound.add_alias(a)
new_compound.save()
# Copy external identifiers for compound
for ext_id in self.external_identifiers.all():
ExternalIdentifier.objects.create(
content_object=new_compound,
database=ext_id.database,
identifier_value=ext_id.identifier_value,
url=ext_id.url,
is_primary=ext_id.is_primary
)
return new_compound
class Meta: class Meta:
unique_together = [('uuid', 'package')] unique_together = [('uuid', 'package')]
@ -773,8 +848,7 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
super().save(*args, **kwargs) super().save(*args, **kwargs)
@property def _url(self):
def url(self):
return '{}/structure/{}'.format(self.compound.url, self.uuid) return '{}/structure/{}'.format(self.compound.url, self.uuid)
@staticmethod @staticmethod
@ -803,10 +877,35 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
return cs return cs
@transaction.atomic
def copy(self, target: 'Package', mapping: Dict):
if self in mapping:
return mapping[self]
self.compound.copy(target, mapping)
return mapping[self]
@property @property
def as_svg(self, width: int = 800, height: int = 400): def as_svg(self, width: int = 800, height: int = 400):
return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height) return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height)
@property
def related_pathways(self):
pathways = Node.objects.filter(node_labels__in=[self]).values_list('pathway', flat=True)
return Pathway.objects.filter(package=self.compound.package, id__in=set(pathways)).order_by('name')
@property
def related_reactions(self):
return (
Reaction.objects.filter(package=self.compound.package, educts__in=[self])
|
Reaction.objects.filter(package=self.compound.package, products__in=[self])
).order_by('name')
@property
def is_default_structure(self):
return self.compound.default_structure == self
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin): class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
package = models.ForeignKey('epdb.Package', verbose_name='Package', on_delete=models.CASCADE, db_index=True) package = models.ForeignKey('epdb.Package', verbose_name='Package', on_delete=models.CASCADE, db_index=True)
@ -842,22 +941,54 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
return cls.create(*args, **kwargs) return cls.create(*args, **kwargs)
# @transaction.atomic
# @property def copy(self, target: 'Package', mapping: Dict):
# def related_pathways(self): """Copy a rule to the target package."""
# reaction_ids = self.related_reactions.values_list('id', flat=True) if self in mapping:
# pathways = Edge.objects.filter(edge_label__in=reaction_ids).values_list('pathway', flat=True) return mapping[self]
# return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by('name')
# # Get the specific rule type and copy accordingly
# @property rule_type = type(self)
# def related_reactions(self):
# return ( if rule_type == SimpleAmbitRule:
# Reaction.objects.filter(package=self.package, rules__in=[self]) new_rule = SimpleAmbitRule.objects.create(
# | package=target,
# Reaction.objects.filter(package=self.package, rules__in=[self]) name=self.name,
# ).order_by('name') description=self.description,
# smirks=self.smirks,
# reactant_filter_smarts=self.reactant_filter_smarts,
product_filter_smarts=self.product_filter_smarts,
kv=self.kv.copy() if self.kv else {}
)
elif rule_type == SimpleRDKitRule:
new_rule = SimpleRDKitRule.objects.create(
package=target,
name=self.name,
description=self.description,
reaction_smarts=self.reaction_smarts,
kv=self.kv.copy() if self.kv else {}
)
elif rule_type == ParallelRule:
new_rule = ParallelRule.objects.create(
package=target,
name=self.name,
description=self.description,
kv=self.kv.copy() if self.kv else {}
)
# Copy simple rules relationships
for simple_rule in self.simple_rules.all():
copied_simple_rule = simple_rule.copy(target, mapping)
new_rule.simple_rules.add(copied_simple_rule)
elif rule_type == SequentialRule:
raise ValueError("SequentialRule copy not implemented!")
else:
raise ValueError(f"Unknown rule type: {rule_type}")
mapping[self] = new_rule
return new_rule
class SimpleRule(Rule): class SimpleRule(Rule):
pass pass
@ -917,8 +1048,7 @@ class SimpleAmbitRule(SimpleRule):
r.save() r.save()
return r return r
@property def _url(self):
def url(self):
return '{}/simple-ambit-rule/{}'.format(self.package.url, self.uuid) return '{}/simple-ambit-rule/{}'.format(self.package.url, self.uuid)
def apply(self, smiles): def apply(self, smiles):
@ -953,8 +1083,7 @@ class SimpleRDKitRule(SimpleRule):
def apply(self, smiles): def apply(self, smiles):
return FormatConverter.apply(smiles, self.reaction_smarts) return FormatConverter.apply(smiles, self.reaction_smarts)
@property def _url(self):
def url(self):
return '{}/simple-rdkit-rule/{}'.format(self.package.url, self.uuid) return '{}/simple-rdkit-rule/{}'.format(self.package.url, self.uuid)
@ -963,8 +1092,7 @@ class SimpleRDKitRule(SimpleRule):
class ParallelRule(Rule): class ParallelRule(Rule):
simple_rules = models.ManyToManyField('epdb.SimpleRule', verbose_name='Simple rules') simple_rules = models.ManyToManyField('epdb.SimpleRule', verbose_name='Simple rules')
@property def _url(self):
def url(self):
return '{}/parallel-rule/{}'.format(self.package.url, self.uuid) return '{}/parallel-rule/{}'.format(self.package.url, self.uuid)
@property @property
@ -1003,8 +1131,7 @@ class SequentialRule(Rule):
simple_rules = models.ManyToManyField('epdb.SimpleRule', verbose_name='Simple rules', simple_rules = models.ManyToManyField('epdb.SimpleRule', verbose_name='Simple rules',
through='SequentialRuleOrdering') through='SequentialRuleOrdering')
@property def _url(self):
def url(self):
return '{}/sequential-rule/{}'.format(self.compound.url, self.uuid) return '{}/sequential-rule/{}'.format(self.compound.url, self.uuid)
@property @property
@ -1039,8 +1166,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
external_identifiers = GenericRelation('ExternalIdentifier') external_identifiers = GenericRelation('ExternalIdentifier')
@property def _url(self):
def url(self):
return '{}/reaction/{}'.format(self.package.url, self.uuid) return '{}/reaction/{}'.format(self.package.url, self.uuid)
@staticmethod @staticmethod
@ -1132,6 +1258,50 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
r.save() r.save()
return r return r
@transaction.atomic
def copy(self, target: 'Package', mapping: Dict ) -> 'Reaction':
"""Copy a reaction to the target package."""
if self in mapping:
return mapping[self]
# Create new reaction
new_reaction = Reaction.objects.create(
package=target,
name=self.name,
description=self.description,
multi_step=self.multi_step,
medline_references=self.medline_references,
kv=self.kv.copy() if self.kv else {}
)
mapping[self] = new_reaction
# Copy educts (reactant compounds)
for educt in self.educts.all():
copied_educt = educt.copy(target, mapping)
new_reaction.educts.add(copied_educt)
# Copy products
for product in self.products.all():
copied_product = product.copy(target, mapping)
new_reaction.products.add(copied_product)
# Copy rules
for rule in self.rules.all():
copied_rule = rule.copy(target, mapping)
new_reaction.rules.add(copied_rule)
# Copy external identifiers
for ext_id in self.external_identifiers.all():
ExternalIdentifier.objects.create(
content_object=new_reaction,
database=ext_id.database,
identifier_value=ext_id.identifier_value,
url=ext_id.url,
is_primary=ext_id.is_primary
)
return new_reaction
def smirks(self): def smirks(self):
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}" return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
@ -1167,8 +1337,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
def edges(self): def edges(self):
return Edge.objects.filter(pathway=self) return Edge.objects.filter(pathway=self)
@property def _url(self):
def url(self):
return '{}/pathway/{}'.format(self.package.url, self.uuid) return '{}/pathway/{}'.format(self.package.url, self.uuid)
# Mode # Mode
@ -1367,6 +1536,87 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
return pw return pw
@transaction.atomic
def copy(self, target: 'Package', mapping: Dict) -> 'Pathway':
if self in mapping:
return mapping[self]
# Start copying the pathway
new_pathway = Pathway.objects.create(
package=target,
name=self.name,
description=self.description,
setting=self.setting, # TODO copy settings?
kv=self.kv.copy() if self.kv else {}
)
# # Copy aliases if they exist
# if hasattr(self, 'aliases'):
# new_pathway.aliases.set(self.aliases.all())
#
# # Copy scenarios if they exist
# if hasattr(self, 'scenarios'):
# new_pathway.scenarios.set(self.scenarios.all())
# Copy all nodes first
for node in self.nodes.all():
# Copy the compound structure for the node label
copied_structure = None
if node.default_node_label:
copied_compound = node.default_node_label.compound.copy(target, mapping)
# Find the corresponding structure in the copied compound
for structure in copied_compound.structures.all():
if structure.smiles == node.default_node_label.smiles:
copied_structure = structure
break
new_node = Node.objects.create(
pathway=new_pathway,
default_node_label=copied_structure,
depth=node.depth,
name=node.name,
description=node.description,
kv=node.kv.copy() if node.kv else {}
)
mapping[node] = new_node
# Copy node labels (many-to-many relationship)
for label in node.node_labels.all():
copied_label_compound = label.compound.copy(target, mapping)
for structure in copied_label_compound.structures.all():
if structure.smiles == label.smiles:
new_node.node_labels.add(structure)
break
# Copy all edges
for edge in self.edges.all():
# Copy the reaction for edge label if it exists
copied_reaction = None
if edge.edge_label:
copied_reaction = edge.edge_label.copy(target, mapping)
new_edge = Edge.objects.create(
pathway=new_pathway,
edge_label=copied_reaction,
name=edge.name,
description=edge.description,
kv=edge.kv.copy() if edge.kv else {}
)
# Copy start and end nodes relationships
for start_node in edge.start_nodes.all():
if start_node in mapping:
new_edge.start_nodes.add(mapping[start_node])
for end_node in edge.end_nodes.all():
if end_node in mapping:
new_edge.end_nodes.add(mapping[end_node])
mapping[self] = new_pathway
return new_pathway
@transaction.atomic @transaction.atomic
def add_node(self, smiles: str, name: Optional[str] = None, description: Optional[str] = None): def add_node(self, smiles: str, name: Optional[str] = None, description: Optional[str] = None):
return Node.create(self, smiles, 0) return Node.create(self, smiles, 0)
@ -1386,8 +1636,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin):
out_edges = models.ManyToManyField('epdb.Edge', verbose_name='Outgoing Edges') out_edges = models.ManyToManyField('epdb.Edge', verbose_name='Outgoing Edges')
depth = models.IntegerField(verbose_name='Node depth', null=False, blank=False) depth = models.IntegerField(verbose_name='Node depth', null=False, blank=False)
@property def _url(self):
def url(self):
return '{}/node/{}'.format(self.pathway.url, self.uuid) return '{}/node/{}'.format(self.pathway.url, self.uuid)
def d3_json(self): def d3_json(self):
@ -1463,8 +1712,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
start_nodes = models.ManyToManyField('epdb.Node', verbose_name='Start Nodes', related_name='edge_educts') start_nodes = models.ManyToManyField('epdb.Node', verbose_name='Start Nodes', related_name='edge_educts')
end_nodes = models.ManyToManyField('epdb.Node', verbose_name='End Nodes', related_name='edge_products') end_nodes = models.ManyToManyField('epdb.Node', verbose_name='End Nodes', related_name='edge_products')
@property def _url(self):
def url(self):
return '{}/edge/{}'.format(self.pathway.url, self.uuid) return '{}/edge/{}'.format(self.pathway.url, self.uuid)
def d3_json(self): def d3_json(self):
@ -1556,8 +1804,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
class EPModel(PolymorphicModel, EnviPathModel): class EPModel(PolymorphicModel, EnviPathModel):
package = models.ForeignKey('epdb.Package', verbose_name='Package', on_delete=models.CASCADE, db_index=True) package = models.ForeignKey('epdb.Package', verbose_name='Package', on_delete=models.CASCADE, db_index=True)
@property def _url(self):
def url(self):
return '{}/model/{}'.format(self.package.url, self.uuid) return '{}/model/{}'.format(self.package.url, self.uuid)
@ -2063,7 +2310,8 @@ class ApplicabilityDomain(EnviPathModel):
transformation = { transformation = {
'rule': rule_data, 'rule': rule_data,
'reliability': rule_reliabilities[rule_idx], 'reliability': rule_reliabilities[rule_idx],
# TODO # We're setting it here to False, as we don't know whether "assess" is called during pathway
# prediction or from Model Page. For persisted Nodes this field will be overwritten at runtime
'is_predicted': False, 'is_predicted': False,
'local_compatibility': local_compatibilities[rule_idx], 'local_compatibility': local_compatibilities[rule_idx],
'probability': preds[rule_idx].probability, 'probability': preds[rule_idx].probability,
@ -2173,33 +2421,93 @@ class Scenario(EnviPathModel):
additional_information = models.JSONField(verbose_name='Additional Information') additional_information = models.JSONField(verbose_name='Additional Information')
@property def _url(self):
def url(self):
return '{}/scenario/{}'.format(self.package.url, self.uuid) return '{}/scenario/{}'.format(self.package.url, self.uuid)
@staticmethod @staticmethod
@transaction.atomic @transaction.atomic
def create(package, name, description, date, type, additional_information): def create(package: 'Package', name:str, description:str, scenario_date:str, scenario_type:str, additional_information: List['EnviPyModel']):
s = Scenario() s = Scenario()
s.package = package s.package = package
if name is None or name.strip() == '':
name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}"
s.name = name s.name = name
if description is not None and description.strip() != '':
s.description = description s.description = description
s.date = date
s.type = type if scenario_date is not None and scenario_date.strip() != '':
s.additional_information = additional_information s.scenario_date = scenario_date
if scenario_type is not None and scenario_type.strip() != '':
s.scenario_type = scenario_type
add_inf = defaultdict(list)
for info in additional_information:
cls_name = info.__class__.__name__
ai_data = json.loads(info.model_dump_json())
ai_data['uuid'] = f"{uuid4()}"
add_inf[cls_name].append(ai_data)
s.additional_information = add_inf
s.save() s.save()
return s return s
def add_additional_information(self, data): @transaction.atomic
pass def add_additional_information(self, data: 'EnviPyModel'):
cls_name = data.__class__.__name__
ai_data = json.loads(data.model_dump_json())
ai_data['uuid'] = f"{uuid4()}"
def remove_additional_information(self, data): if cls_name not in self.additional_information:
pass self.additional_information[cls_name] = []
def set_additional_information(self, data): self.additional_information[cls_name].append(ai_data)
pass self.save()
@transaction.atomic
def remove_additional_information(self, ai_uuid):
found_type = None
found_idx = -1
for k, vals in self.additional_information.items():
for i, v in enumerate(vals):
if v['uuid'] == ai_uuid:
found_type = k
found_idx = i
break
if found_type is not None and found_idx >= 0:
if len(self.additional_information[found_type]) == 1:
del self.additional_information[k]
else:
self.additional_information[k].pop(found_idx)
self.save()
else:
raise ValueError(f"Could not find additional information with uuid {ai_uuid}")
@transaction.atomic
def set_additional_information(self, data: Dict[str, 'EnviPyModel']):
new_ais = defaultdict(list)
for k, vals in data.items():
for v in vals:
ai_data = json.loads(v.model_dump_json())
if hasattr(v, 'uuid'):
ai_data['uuid'] = str(v.uuid)
else:
ai_data['uuid'] = str(uuid4())
new_ais[k].append(ai_data)
self.additional_information = new_ais
self.save()
def get_additional_information(self): def get_additional_information(self):
from envipy_additional_information import NAME_MAPPING from envipy_additional_information import NAME_MAPPING
@ -2209,7 +2517,14 @@ class Scenario(EnviPathModel):
continue continue
for v in vals: for v in vals:
yield NAME_MAPPING[k](**json.loads(v)) # Per default additional fields are ignored
MAPPING = {c.__name__: c for c in NAME_MAPPING.values()}
inst = MAPPING[k](**v)
# Add uuid to uniquely identify objects for manipulation
if 'uuid' in v:
inst.__dict__['uuid'] = v['uuid']
yield inst
class UserSettingPermission(Permission): class UserSettingPermission(Permission):
@ -2238,8 +2553,7 @@ class Setting(EnviPathModel):
blank=True) blank=True)
model_threshold = models.FloatField(null=True, blank=True, verbose_name='Setting Model Threshold', default=0.25) model_threshold = models.FloatField(null=True, blank=True, verbose_name='Setting Model Threshold', default=0.25)
@property def _url(self):
def url(self):
return '{}/setting/{}'.format(s.SERVER_URL, self.uuid) return '{}/setting/{}'.format(s.SERVER_URL, self.uuid)
@cached_property @cached_property

View File

@ -4,16 +4,15 @@ from typing import List, Dict, Any
from django.conf import settings as s from django.conf import settings as s
from django.contrib.auth import get_user_model from django.contrib.auth import get_user_model
from django.db.models import F, Value
from django.db.models.fields import CharField
from django.db.models.functions import Concat
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
@ -150,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
@ -160,6 +165,7 @@ 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),
@ -211,6 +217,32 @@ def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
attach_object.set_scenarios(scens) 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'
@ -439,16 +471,7 @@ def scenarios(request):
if request.GET.get('all'): if request.GET.get('all'):
return JsonResponse({ return JsonResponse({
"objects": [ "objects": [
{"name": s.name, "url": s.full_url, "reviewed": True} {"name": s.name, "url": s.url, "reviewed": True} for s in reviewed_scenario_qs
for s in reviewed_scenario_qs.annotate(
full_url=Concat(
Value(s.SERVER_URL + '/package/'),
F("package__uuid"),
Value("/scenario/"),
F("uuid"),
output_field=CharField(),
)
)
] ]
}) })
@ -773,6 +796,14 @@ def package(request, package_uuid):
for g in Group.objects.filter(public=True): for g in Group.objects.filter(public=True):
PackageManager.update_permissions(current_user, current_package, g, Permission.READ[0]) PackageManager.update_permissions(current_user, current_package, g, Permission.READ[0])
return redirect(current_package.url) 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()
@ -1694,7 +1725,7 @@ def package_scenarios(request, package_uuid):
if request.method == 'GET': if request.method == 'GET':
if 'application/json' in request.META.get('HTTP_ACCEPT'): #request.headers.get('Accept') == 'application/json': 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') scens = Scenario.objects.filter(package=current_package).order_by('name')
res = [{'name': s.name, 'url': s.url, 'uuid': s.uuid} for s in scens] res = [{'name': s.name, 'url': s.url, 'uuid': s.uuid} for s in scens]
return JsonResponse(res, safe=False) return JsonResponse(res, safe=False)
@ -1704,7 +1735,7 @@ def package_scenarios(request, package_uuid):
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()
@ -1725,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', ])
@ -1747,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: else:
return HttpResponseNotAllowed(['GET', ]) return HttpResponseBadRequest()
else:
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(['GET', 'POST'])
############## ##############

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

@ -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

@ -11,6 +11,12 @@
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a> <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li> </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> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <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>

View File

@ -8,10 +8,16 @@
<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> <li>
<a class="button" data-toggle="modal" data-target="#download_pathway_modal"> <a class="button" data-toggle="modal" data-target="#download_pathway_modal">
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway</a> <i class="glyphicon glyphicon-floppy-save"></i> Download Pathway</a>
</li> </li>
{% if meta.can_edit %}
<li role="separator" class="divider"></li> <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">

View File

@ -7,6 +7,12 @@
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a> <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li> </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> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <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>

View File

@ -7,6 +7,12 @@
<a role="button" data-toggle="modal" data-target="#set_scenario_modal"> <a role="button" data-toggle="modal" data-target="#set_scenario_modal">
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a> <i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
</li> </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> <li>
<a class="button" data-toggle="modal" data-target="#generic_delete_modal"> <a class="button" data-toggle="modal" data-target="#generic_delete_modal">
<i class="glyphicon glyphicon-trash"></i> Delete Rule</a> <i class="glyphicon glyphicon-trash"></i> Delete Rule</a>

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

@ -2,14 +2,8 @@
{% 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">
<button class="btn btn-secondary btn-lg btn-block" type="button" id="load-remaining">Load all {% if reviewed_objects.count > 0 %} {{ reviewed_objects.count }} {% else %} {{ unreviewed_objects.count }} {% endif %} {{ object_type }}s
</button>
<p></p>
<div id="load-all-loading"></div>
<p></p>
<div id="load-all-error" style="display: none;"> <div id="load-all-error" style="display: none;">
<div class="alert alert-danger" role="alert"> <div class="alert alert-danger" role="alert">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span> <span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
@ -18,11 +12,11 @@
</div> </div>
</div> </div>
<input type="text" id="object-search" class="form-control" placeholder="Search by name" style="display: none;"> <input type="text" id="object-search" class="form-control" placeholder="Search by name"
style="display: none;">
<p></p> <p></p>
</div> </div>
{% endif %} {% 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">
@ -124,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">
@ -197,7 +202,7 @@
{% endif %} {% endif %}
{% block content %} {% block content %}
{% endblock content %} {% endblock content %}
{% if meta.current_package.license %} {% if meta.url_contains_package and meta.current_package.license %}
<p></p> <p></p>
<div class="panel-group" id="license_accordion"> <div class="panel-group" id="license_accordion">
<div class="panel panel-default list-group-item" style="background-color:#f5f5f5"> <div class="panel panel-default list-group-item" style="background-color:#f5f5f5">

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>
<label for="scenario-type">Scenario Type</label>
<select id="scenario-type" name="scenario-type" class="form-control" data-width='100%'>
<option value="empty" selected>Empty Scenario</option>
{% for k, v in scenario_types.items %}
<option value="{{ v.name }}">{{ k }}</option>
{% endfor %}
</select>
{% for type in scenario_types.values %}
<div id="{{ type.name }}-specific-inputs">
{% for widget in type.widgets %}
{{ widget|safe }}
{% endfor %}
</div> </div>
<div class="modal-body hide" data-step="2" data-title="New Scenario - Step 2"> {% endfor %}
<div class="jumbotron">
Do you want to create an empty scenario and fill it
with your own set of attributes, or fill in a
pre-defined set of attributes for soil, sludge or sediment
data?
</div>
<div class="radio">
<label>
<input type="radio" name="type" id="radioEmpty" checked>Empty Scenario
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="type" id="radioSoil" value="soil" >Soil Data
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="type" id="radioSludge" value="sludge">Sludge Data
</label>
</div>
<div class="radio">
<label>
<input type="radio" name="type" id="radioSediment" value="sediment">Water-Sediment System Data
</label>
</div>
</div>
<div class="modal-body hide" data-step="3" data-title="New Scenario - Step 3">
<div class="jumbotron" id="finaljumbo">
All done! Click Submit!
</div>
<div style="float: left"><button
id="addColumnButton" type="button"
class="btn btn-default">Add
another Scenario
</button></div>
<input type="hidden" name="fullScenario" value="true"/>
{% include "tables/scenario.html" %}
</div>
</form> </form>
</div>
<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 () {
<p></p> // Initially hide all "specific" forms
<div id="scenariocontent"></div> $("div[id$='-specific-inputs']").each(function () {
$(this).hide();
<!--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,
}); });
});
// On change hide all and show only selected
$("#scenario-type").change(function () {
$("div[id$='-specific-inputs']").each(function () {
$(this).hide();
});
val = $('option:selected', this).val();
$("#" + val + "-specific-inputs").show();
});
$('#new_scenario_modal_form_submit').on('click', function (e) {
e.preventDefault();
$('#new_scenario_form').submit();
});
});
</script> </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

@ -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,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

@ -5,6 +5,7 @@
{% 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_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}

View File

@ -6,6 +6,7 @@
{% 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/generic_set_scenario_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" %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}

View File

@ -82,6 +82,7 @@
{% 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/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/generic_set_scenario_modal.html" %}
{% include "modals/objects/delete_pathway_node_modal.html" %} {% include "modals/objects/delete_pathway_node_modal.html" %}

View File

@ -5,6 +5,7 @@
{% block action_modals %} {% block action_modals %}
{% include "modals/objects/edit_reaction_modal.html" %} {% include "modals/objects/edit_reaction_modal.html" %}
{% include "modals/objects/generic_set_scenario_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" %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}

View File

@ -3,6 +3,8 @@
{% 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" %} {% 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">
@ -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

@ -5,6 +5,7 @@
{% 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_set_scenario_modal.html" %}
{% include "modals/objects/generic_copy_object_modal.html" %}
{% include "modals/objects/generic_delete_modal.html" %} {% include "modals/objects/generic_delete_modal.html" %}
{% endblock action_modals %} {% endblock action_modals %}

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)

View File

@ -8,7 +8,7 @@ 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
@ -67,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)
@ -79,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)
@ -682,6 +698,9 @@ 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)
if '~' in mol_data:
mol = i.loadSmarts(mol_data)
else:
mol = i.loadMolecule(mol_data) mol = i.loadMolecule(mol_data)
if len(functional_groups.keys()) > 0: if len(functional_groups.keys()) > 0:

168
utilities/misc.py Normal file
View File

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

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" },
] ]