forked from enviPath/enviPy
Compare commits
11 Commits
beta_2025-
...
beta_2025-
| Author | SHA1 | Date | |
|---|---|---|---|
| 4158bd36cb | |||
| 4e02910c62 | |||
| 2babe7f7e2 | |||
| 7da3880a9b | |||
| 52931526c1 | |||
| dd7b28046c | |||
| 8592cfae50 | |||
| ec2b941a85 | |||
| 02d84a9b29 | |||
| 00d9188c0c | |||
| 13816ecaf3 |
@ -1,7 +1,14 @@
|
||||
from epdb.api import router as epdb_app_router
|
||||
from epdb.legacy_api import router as epdb_legacy_app_router
|
||||
from ninja import NinjaAPI
|
||||
|
||||
api = NinjaAPI()
|
||||
|
||||
api.add_router("/", epdb_app_router)
|
||||
from ninja import NinjaAPI
|
||||
|
||||
api_v1 = NinjaAPI(title="API V1 Docs", urls_namespace="api-v1")
|
||||
api_legacy = NinjaAPI(title="Legacy API Docs", urls_namespace="api-legacy")
|
||||
|
||||
# Add routers
|
||||
api_v1.add_router("/", epdb_app_router)
|
||||
api_legacy.add_router("/", epdb_legacy_app_router)
|
||||
|
||||
@ -308,11 +308,22 @@ SENTRY_ENABLED = os.environ.get('SENTRY_ENABLED', 'False') == 'True'
|
||||
if SENTRY_ENABLED:
|
||||
import sentry_sdk
|
||||
|
||||
def before_send(event, hint):
|
||||
# Check if was a handled exception by one of our loggers
|
||||
if event.get('logger'):
|
||||
for log_path in LOGGING.get('loggers').keys():
|
||||
if event['logger'].startswith(log_path):
|
||||
return None
|
||||
|
||||
return event
|
||||
|
||||
sentry_sdk.init(
|
||||
dsn=os.environ.get('SENTRY_DSN'),
|
||||
# Add data like request headers and IP for users,
|
||||
# see https://docs.sentry.io/platforms/python/data-management/data-collected/ for more info
|
||||
send_default_pii=True,
|
||||
environment=os.environ.get('SENTRY_ENVIRONMENT', 'development'),
|
||||
before_send=before_send,
|
||||
)
|
||||
|
||||
# compile into digestible flags
|
||||
|
||||
@ -17,11 +17,12 @@ Including another URLconf
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
from .api import api
|
||||
from .api import api_v1, api_legacy
|
||||
|
||||
urlpatterns = [
|
||||
path("", include("epdb.urls")),
|
||||
path("", include("migration.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/", api.urls),
|
||||
path("api/v1/", api_v1.urls),
|
||||
path("api/legacy/", api_legacy.urls),
|
||||
]
|
||||
|
||||
739
epdb/legacy_api.py
Normal file
739
epdb/legacy_api.py
Normal 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!'}
|
||||
135
epdb/logic.py
135
epdb/logic.py
@ -1,6 +1,7 @@
|
||||
import re
|
||||
import logging
|
||||
from typing import Union, List, Optional, Set, Dict
|
||||
import json
|
||||
from typing import Union, List, Optional, Set, Dict, Any
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
@ -13,6 +14,132 @@ from utilities.chem import FormatConverter
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EPDBURLParser:
|
||||
|
||||
UUID_PATTERN = r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
|
||||
|
||||
MODEL_PATTERNS = {
|
||||
'epdb.User': re.compile(rf'^.*/user/{UUID_PATTERN}'),
|
||||
'epdb.Group': re.compile(rf'^.*/group/{UUID_PATTERN}'),
|
||||
'epdb.Package': re.compile(rf'^.*/package/{UUID_PATTERN}'),
|
||||
'epdb.Compound': re.compile(rf'^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}'),
|
||||
'epdb.CompoundStructure': re.compile(rf'^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}/structure/{UUID_PATTERN}'),
|
||||
'epdb.Rule': re.compile(rf'^.*/package/{UUID_PATTERN}/(?:simple-ambit-rule|simple-rdkit-rule|parallel-rule|sequential-rule|rule)/{UUID_PATTERN}'),
|
||||
'epdb.Reaction': re.compile(rf'^.*/package/{UUID_PATTERN}/reaction/{UUID_PATTERN}$'),
|
||||
'epdb.Pathway': re.compile(rf'^.*/package/{UUID_PATTERN}/pathway/{UUID_PATTERN}'),
|
||||
'epdb.Node': re.compile(rf'^.*/package/{UUID_PATTERN}/pathway/{UUID_PATTERN}/node/{UUID_PATTERN}'),
|
||||
'epdb.Edge': re.compile(rf'^.*/package/{UUID_PATTERN}/pathway/{UUID_PATTERN}/edge/{UUID_PATTERN}'),
|
||||
'epdb.Scenario': re.compile(rf'^.*/package/{UUID_PATTERN}/scenario/{UUID_PATTERN}'),
|
||||
'epdb.EPModel': re.compile(rf'^.*/package/{UUID_PATTERN}/model/{UUID_PATTERN}'),
|
||||
'epdb.Setting': re.compile(rf'^.*/setting/{UUID_PATTERN}'),
|
||||
}
|
||||
|
||||
def __init__(self, url: str):
|
||||
self.url = url
|
||||
self._matches = {}
|
||||
self._analyze_url()
|
||||
|
||||
def _analyze_url(self):
|
||||
for model_path, pattern in self.MODEL_PATTERNS.items():
|
||||
match = pattern.findall(self.url)
|
||||
if match:
|
||||
self._matches[model_path] = match[0]
|
||||
|
||||
def _get_model_class(self, model_path: str):
|
||||
try:
|
||||
from django.apps import apps
|
||||
app_label, model_name = model_path.split('.')[-2:]
|
||||
return apps.get_model(app_label, model_name)
|
||||
except (ImportError, LookupError, ValueError):
|
||||
raise ValueError(f"Model {model_path} does not exist!")
|
||||
|
||||
def _get_object_by_url(self, model_path: str, url: str):
|
||||
model_class = self._get_model_class(model_path)
|
||||
return model_class.objects.get(url=url)
|
||||
|
||||
def is_package_url(self) -> bool:
|
||||
return bool(re.compile(rf'^.*/package/{self.UUID_PATTERN}$').findall(self.url))
|
||||
|
||||
def contains_package_url(self):
|
||||
return bool(self.MODEL_PATTERNS['epdb.Package'].findall(self.url)) and not self.is_package_url()
|
||||
|
||||
def is_user_url(self) -> bool:
|
||||
return bool(self.MODEL_PATTERNS['epdb.User'].findall(self.url))
|
||||
|
||||
def is_group_url(self) -> bool:
|
||||
return bool(self.MODEL_PATTERNS['epdb.Group'].findall(self.url))
|
||||
|
||||
def is_setting_url(self) -> bool:
|
||||
return bool(self.MODEL_PATTERNS['epdb.Setting'].findall(self.url))
|
||||
|
||||
def get_object(self) -> Optional[Any]:
|
||||
# Define priority order from most specific to least specific
|
||||
priority_order = [
|
||||
# 3rd level
|
||||
'epdb.CompoundStructure',
|
||||
'epdb.Node',
|
||||
'epdb.Edge',
|
||||
# 2nd level
|
||||
'epdb.Compound',
|
||||
'epdb.Rule',
|
||||
'epdb.Reaction',
|
||||
'epdb.Scenario',
|
||||
'epdb.EPModel',
|
||||
'epdb.Pathway',
|
||||
# 1st level
|
||||
'epdb.Package',
|
||||
'epdb.Setting',
|
||||
'epdb.Group',
|
||||
'epdb.User',
|
||||
]
|
||||
|
||||
for model_path in priority_order:
|
||||
if model_path in self._matches:
|
||||
url = self._matches[model_path]
|
||||
return self._get_object_by_url(model_path, url)
|
||||
|
||||
raise ValueError(f"No object found for URL {self.url}")
|
||||
|
||||
def get_objects(self) -> List[Any]:
|
||||
"""
|
||||
Get all Django model objects along the URL path in hierarchical order.
|
||||
Returns objects from parent to child (e.g., Package -> Compound -> Structure).
|
||||
"""
|
||||
objects = []
|
||||
|
||||
hierarchy_order = [
|
||||
# 1st level
|
||||
'epdb.Package',
|
||||
'epdb.Setting',
|
||||
'epdb.Group',
|
||||
'epdb.User',
|
||||
# 2nd level
|
||||
'epdb.Compound',
|
||||
'epdb.Rule',
|
||||
'epdb.Reaction',
|
||||
'epdb.Scenario',
|
||||
'epdb.EPModel',
|
||||
'epdb.Pathway',
|
||||
# 3rd level
|
||||
'epdb.CompoundStructure',
|
||||
'epdb.Node',
|
||||
'epdb.Edge',
|
||||
]
|
||||
|
||||
for model_path in hierarchy_order:
|
||||
if model_path in self._matches:
|
||||
url = self._matches[model_path]
|
||||
objects.append(self._get_object_by_url(model_path, url))
|
||||
|
||||
return objects
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"EPDBURLParser(url='{self.url}')"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"EPDBURLParser(url='{self.url}', matches={list(self._matches.keys())})"
|
||||
|
||||
|
||||
class UserManager(object):
|
||||
user_pattern = re.compile(r".*/user/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")
|
||||
|
||||
@ -426,11 +553,13 @@ class PackageManager(object):
|
||||
|
||||
try:
|
||||
res = AdditionalInformationConverter.convert(name, addinf_data)
|
||||
res_cls_name = res.__class__.__name__
|
||||
ai_data = json.loads(res.model_dump_json())
|
||||
ai_data['uuid'] = f"{uuid4()}"
|
||||
new_add_inf[res_cls_name].append(ai_data)
|
||||
except:
|
||||
logger.error(f"Failed to convert {name} with {addinf_data}")
|
||||
|
||||
new_add_inf[name].append(res.model_dump_json())
|
||||
|
||||
scen.additional_information = new_add_inf
|
||||
scen.save()
|
||||
|
||||
|
||||
50
epdb/management/commands/localize_urls.py
Normal file
50
epdb/management/commands/localize_urls.py
Normal 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']))
|
||||
)
|
||||
@ -2,7 +2,6 @@ from django.conf import settings
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
|
||||
class LoginRequiredMiddleware:
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
@ -11,6 +10,7 @@ class LoginRequiredMiddleware:
|
||||
reverse('logout'),
|
||||
reverse('admin:login'),
|
||||
reverse('admin:index'),
|
||||
'/api/legacy/'
|
||||
] + getattr(settings, 'LOGIN_EXEMPT_URLS', [])
|
||||
|
||||
def __call__(self, request):
|
||||
|
||||
594
epdb/migrations/0001_initial.py
Normal file
594
epdb/migrations/0001_initial.py
Normal 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',),
|
||||
),
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@ -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')},
|
||||
},
|
||||
),
|
||||
]
|
||||
@ -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'),
|
||||
),
|
||||
]
|
||||
450
epdb/models.py
450
epdb/models.py
@ -20,6 +20,7 @@ from django.db import models, transaction
|
||||
from django.db.models import JSONField, Count, Q, QuerySet
|
||||
from django.utils import timezone
|
||||
from django.utils.functional import cached_property
|
||||
from envipy_additional_information import EnviPyModel
|
||||
from model_utils.models import TimeStampedModel
|
||||
from polymorphic.models import PolymorphicModel
|
||||
from sklearn.metrics import precision_score, recall_score, jaccard_score
|
||||
@ -37,9 +38,8 @@ logger = logging.getLogger(__name__)
|
||||
|
||||
class User(AbstractUser):
|
||||
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, default=uuid4)
|
||||
url = models.TextField(blank=False, null=True, verbose_name='URL', unique=True)
|
||||
default_package = models.ForeignKey('epdb.Package', verbose_name='Default Package', null=True,
|
||||
on_delete=models.SET_NULL)
|
||||
default_group = models.ForeignKey('Group', verbose_name='Default Group', null=True, blank=False,
|
||||
@ -50,8 +50,13 @@ class User(AbstractUser):
|
||||
USERNAME_FIELD = "email"
|
||||
REQUIRED_FIELDS = ['username']
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.url:
|
||||
self.url = self._url()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _url(self):
|
||||
return '{}/user/{}'.format(s.SERVER_URL, self.uuid)
|
||||
|
||||
def prediction_settings(self):
|
||||
@ -169,6 +174,7 @@ class APIToken(TimeStampedModel):
|
||||
|
||||
class Group(TimeStampedModel):
|
||||
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')
|
||||
owner = models.ForeignKey("User", verbose_name='Group Owner', on_delete=models.CASCADE)
|
||||
public = models.BooleanField(verbose_name='Public Group', default=False)
|
||||
@ -179,8 +185,13 @@ class Group(TimeStampedModel):
|
||||
def __str__(self):
|
||||
return f"{self.name} (pk={self.pk})"
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.url:
|
||||
self.url = self._url()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
def _url(self):
|
||||
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')
|
||||
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)
|
||||
|
||||
@property
|
||||
def save(self, *args, **kwargs):
|
||||
if not self.url:
|
||||
self.url = self._url()
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@abc.abstractmethod
|
||||
def url(self):
|
||||
def _url(self):
|
||||
pass
|
||||
|
||||
def simple_json(self, include_description=False):
|
||||
@ -516,15 +534,16 @@ class AliasMixin(models.Model):
|
||||
@transaction.atomic
|
||||
def add_alias(self, new_alias, set_as_default=False):
|
||||
if set_as_default:
|
||||
self.aliases.add(self.name)
|
||||
self.aliases.append(self.name)
|
||||
self.name = new_alias
|
||||
|
||||
if new_alias in self.aliases:
|
||||
self.aliases.remove(new_alias)
|
||||
else:
|
||||
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()
|
||||
|
||||
class Meta:
|
||||
@ -585,8 +604,7 @@ class Package(EnviPathModel):
|
||||
def models(self):
|
||||
return EPModel.objects.filter(package=self)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def _url(self):
|
||||
return '{}/package/{}'.format(s.SERVER_URL, self.uuid)
|
||||
|
||||
def get_applicable_rules(self):
|
||||
@ -632,8 +650,7 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
||||
def normalized_structure(self):
|
||||
return CompoundStructure.objects.get(compound=self, normalized_structure=True)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def _url(self):
|
||||
return '{}/compound/{}'.format(self.package.url, self.uuid)
|
||||
|
||||
@transaction.atomic
|
||||
@ -747,6 +764,64 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
||||
|
||||
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:
|
||||
unique_together = [('uuid', 'package')]
|
||||
|
||||
@ -773,8 +848,7 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
|
||||
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def _url(self):
|
||||
return '{}/structure/{}'.format(self.compound.url, self.uuid)
|
||||
|
||||
@staticmethod
|
||||
@ -803,10 +877,35 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
|
||||
|
||||
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
|
||||
def as_svg(self, width: int = 800, height: int = 400):
|
||||
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):
|
||||
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)
|
||||
|
||||
|
||||
#
|
||||
# @property
|
||||
# def related_pathways(self):
|
||||
# reaction_ids = self.related_reactions.values_list('id', flat=True)
|
||||
# pathways = Edge.objects.filter(edge_label__in=reaction_ids).values_list('pathway', flat=True)
|
||||
# return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by('name')
|
||||
#
|
||||
# @property
|
||||
# def related_reactions(self):
|
||||
# return (
|
||||
# Reaction.objects.filter(package=self.package, rules__in=[self])
|
||||
# |
|
||||
# Reaction.objects.filter(package=self.package, rules__in=[self])
|
||||
# ).order_by('name')
|
||||
#
|
||||
#
|
||||
@transaction.atomic
|
||||
def copy(self, target: 'Package', mapping: Dict):
|
||||
"""Copy a rule to the target package."""
|
||||
if self in mapping:
|
||||
return mapping[self]
|
||||
|
||||
# Get the specific rule type and copy accordingly
|
||||
rule_type = type(self)
|
||||
|
||||
if rule_type == SimpleAmbitRule:
|
||||
new_rule = SimpleAmbitRule.objects.create(
|
||||
package=target,
|
||||
name=self.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):
|
||||
pass
|
||||
|
||||
@ -917,8 +1048,7 @@ class SimpleAmbitRule(SimpleRule):
|
||||
r.save()
|
||||
return r
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def _url(self):
|
||||
return '{}/simple-ambit-rule/{}'.format(self.package.url, self.uuid)
|
||||
|
||||
def apply(self, smiles):
|
||||
@ -953,8 +1083,7 @@ class SimpleRDKitRule(SimpleRule):
|
||||
def apply(self, smiles):
|
||||
return FormatConverter.apply(smiles, self.reaction_smarts)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def _url(self):
|
||||
return '{}/simple-rdkit-rule/{}'.format(self.package.url, self.uuid)
|
||||
|
||||
|
||||
@ -963,8 +1092,7 @@ class SimpleRDKitRule(SimpleRule):
|
||||
class ParallelRule(Rule):
|
||||
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)
|
||||
|
||||
@property
|
||||
@ -1003,8 +1131,7 @@ class SequentialRule(Rule):
|
||||
simple_rules = models.ManyToManyField('epdb.SimpleRule', verbose_name='Simple rules',
|
||||
through='SequentialRuleOrdering')
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def _url(self):
|
||||
return '{}/sequential-rule/{}'.format(self.compound.url, self.uuid)
|
||||
|
||||
@property
|
||||
@ -1039,8 +1166,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
|
||||
|
||||
external_identifiers = GenericRelation('ExternalIdentifier')
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def _url(self):
|
||||
return '{}/reaction/{}'.format(self.package.url, self.uuid)
|
||||
|
||||
@staticmethod
|
||||
@ -1132,6 +1258,50 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
|
||||
r.save()
|
||||
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):
|
||||
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):
|
||||
return Edge.objects.filter(pathway=self)
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def _url(self):
|
||||
return '{}/pathway/{}'.format(self.package.url, self.uuid)
|
||||
|
||||
# Mode
|
||||
@ -1367,6 +1536,87 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
|
||||
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
|
||||
def add_node(self, smiles: str, name: Optional[str] = None, description: Optional[str] = None):
|
||||
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')
|
||||
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)
|
||||
|
||||
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')
|
||||
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)
|
||||
|
||||
def d3_json(self):
|
||||
@ -1556,8 +1804,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
class EPModel(PolymorphicModel, EnviPathModel):
|
||||
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)
|
||||
|
||||
|
||||
@ -2063,7 +2310,8 @@ class ApplicabilityDomain(EnviPathModel):
|
||||
transformation = {
|
||||
'rule': rule_data,
|
||||
'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,
|
||||
'local_compatibility': local_compatibilities[rule_idx],
|
||||
'probability': preds[rule_idx].probability,
|
||||
@ -2173,33 +2421,93 @@ class Scenario(EnviPathModel):
|
||||
|
||||
additional_information = models.JSONField(verbose_name='Additional Information')
|
||||
|
||||
@property
|
||||
def url(self):
|
||||
def _url(self):
|
||||
return '{}/scenario/{}'.format(self.package.url, self.uuid)
|
||||
|
||||
@staticmethod
|
||||
@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.package = package
|
||||
|
||||
if name is None or name.strip() == '':
|
||||
name = f"Scenario {Scenario.objects.filter(package=package).count() + 1}"
|
||||
|
||||
s.name = name
|
||||
s.description = description
|
||||
s.date = date
|
||||
s.type = type
|
||||
s.additional_information = additional_information
|
||||
|
||||
if description is not None and description.strip() != '':
|
||||
s.description = description
|
||||
|
||||
if scenario_date is not None and scenario_date.strip() != '':
|
||||
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()
|
||||
|
||||
return s
|
||||
|
||||
def add_additional_information(self, data):
|
||||
pass
|
||||
@transaction.atomic
|
||||
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):
|
||||
pass
|
||||
if cls_name not in self.additional_information:
|
||||
self.additional_information[cls_name] = []
|
||||
|
||||
def set_additional_information(self, data):
|
||||
pass
|
||||
self.additional_information[cls_name].append(ai_data)
|
||||
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):
|
||||
from envipy_additional_information import NAME_MAPPING
|
||||
@ -2209,7 +2517,14 @@ class Scenario(EnviPathModel):
|
||||
continue
|
||||
|
||||
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):
|
||||
@ -2238,8 +2553,7 @@ class Setting(EnviPathModel):
|
||||
blank=True)
|
||||
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)
|
||||
|
||||
@cached_property
|
||||
|
||||
169
epdb/views.py
169
epdb/views.py
@ -4,16 +4,15 @@ from typing import List, Dict, Any
|
||||
|
||||
from django.conf import settings as s
|
||||
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.shortcuts import render, redirect
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
from envipy_additional_information import NAME_MAPPING
|
||||
|
||||
from utilities.chem import FormatConverter, IndigoUtils
|
||||
from utilities.decorators import package_permission_required
|
||||
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager
|
||||
from utilities.misc import HTMLGenerator
|
||||
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager, EPDBURLParser
|
||||
from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \
|
||||
EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \
|
||||
UserPackagePermission, Permission, License, User, Edge
|
||||
@ -150,6 +149,12 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
|
||||
current_user = _anonymous_or_real(request)
|
||||
can_edit = editable(request, current_user)
|
||||
|
||||
parser = EPDBURLParser(request.build_absolute_uri(request.path))
|
||||
|
||||
url_contains_package = False
|
||||
if parser.contains_package_url() or parser.is_package_url():
|
||||
url_contains_package = True
|
||||
|
||||
if for_user:
|
||||
current_user = for_user
|
||||
|
||||
@ -160,6 +165,7 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
|
||||
'server_url': s.SERVER_URL,
|
||||
'user': current_user,
|
||||
'can_edit': can_edit,
|
||||
'url_contains_package': url_contains_package,
|
||||
'readable_packages': PackageManager.get_all_readable_packages(current_user, include_reviewed=True),
|
||||
'writeable_packages': PackageManager.get_all_writeable_packages(current_user),
|
||||
'available_groups': GroupManager.get_groups(current_user),
|
||||
@ -211,6 +217,32 @@ def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
|
||||
|
||||
attach_object.set_scenarios(scens)
|
||||
|
||||
|
||||
def copy_object(current_user, target_package: 'Package', source_object_url: str):
|
||||
# Ensures that source is readable
|
||||
source_package = PackageManager.get_package_by_url(current_user, source_object_url)
|
||||
|
||||
parser = EPDBURLParser(source_object_url)
|
||||
|
||||
# if the url won't contain a package or is a plain package
|
||||
if not parser.contains_package_url():
|
||||
raise ValueError(f"Object {source_object_url} can't be copied!")
|
||||
|
||||
# Gets the most specific object
|
||||
source_object = parser.get_object()
|
||||
|
||||
if hasattr(source_object, 'copy'):
|
||||
mapping = dict()
|
||||
copy = source_object.copy(target_package, mapping)
|
||||
|
||||
if s.DEBUG:
|
||||
for k, v in mapping.items():
|
||||
logger.debug(f"Mapping {k.url} to {v.url}")
|
||||
|
||||
return copy
|
||||
|
||||
raise ValueError(f"Object {source_object} can't be copied!")
|
||||
|
||||
def index(request):
|
||||
context = get_base_context(request)
|
||||
context['title'] = 'enviPath - Home'
|
||||
@ -439,16 +471,7 @@ def scenarios(request):
|
||||
if request.GET.get('all'):
|
||||
return JsonResponse({
|
||||
"objects": [
|
||||
{"name": s.name, "url": s.full_url, "reviewed": True}
|
||||
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(),
|
||||
)
|
||||
)
|
||||
{"name": s.name, "url": s.url, "reviewed": True} for s in reviewed_scenario_qs
|
||||
]
|
||||
})
|
||||
|
||||
@ -773,6 +796,14 @@ def package(request, package_uuid):
|
||||
for g in Group.objects.filter(public=True):
|
||||
PackageManager.update_permissions(current_user, current_package, g, Permission.READ[0])
|
||||
return redirect(current_package.url)
|
||||
elif hidden == 'copy':
|
||||
object_to_copy = request.POST.get('object_to_copy')
|
||||
|
||||
if not object_to_copy:
|
||||
return error(request, 'Invalid target package.', 'Please select a target package.')
|
||||
|
||||
copied_object = copy_object(current_user, current_package, object_to_copy)
|
||||
return JsonResponse({'success': copied_object.url})
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
@ -1694,7 +1725,7 @@ def package_scenarios(request, package_uuid):
|
||||
|
||||
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')
|
||||
res = [{'name': s.name, 'url': s.url, 'uuid': s.uuid} for s in scens]
|
||||
return JsonResponse(res, safe=False)
|
||||
@ -1704,7 +1735,7 @@ def package_scenarios(request, package_uuid):
|
||||
|
||||
context['meta']['current_package'] = current_package
|
||||
context['object_type'] = 'scenario'
|
||||
context['breadcrumbs'] = breadcrumbs(current_package, 'pathway')
|
||||
context['breadcrumbs'] = breadcrumbs(current_package, 'scenario')
|
||||
|
||||
reviewed_scenario_qs = Scenario.objects.none()
|
||||
unreviewed_scenario_qs = Scenario.objects.none()
|
||||
@ -1725,8 +1756,57 @@ def package_scenarios(request, package_uuid):
|
||||
context['reviewed_objects'] = reviewed_scenario_qs
|
||||
context['unreviewed_objects'] = unreviewed_scenario_qs
|
||||
|
||||
return render(request, 'collections/objects_list.html', context)
|
||||
from envipy_additional_information import SLUDGE_ADDITIONAL_INFORMATION, SOIL_ADDITIONAL_INFORMATION, \
|
||||
SEDIMENT_ADDITIONAL_INFORMATION
|
||||
context['scenario_types'] = {
|
||||
'Soil Data': {
|
||||
'name': 'soil',
|
||||
'widgets': [HTMLGenerator.generate_html(ai, prefix=f'soil_{0}') for ai in
|
||||
[x for s in SOIL_ADDITIONAL_INFORMATION.values() for x in s]]
|
||||
},
|
||||
'Sludge Data': {
|
||||
'name': 'sludge',
|
||||
'widgets': [HTMLGenerator.generate_html(ai, prefix=f'sludge_{0}') for ai in
|
||||
[x for s in SLUDGE_ADDITIONAL_INFORMATION.values() for x in s]]
|
||||
},
|
||||
'Water-Sediment System Data': {
|
||||
'name': 'sediment',
|
||||
'widgets': [HTMLGenerator.generate_html(ai, prefix=f'sediment_{0}') for ai in
|
||||
[x for s in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in s]]
|
||||
}
|
||||
}
|
||||
|
||||
context['sludge_additional_information'] = SLUDGE_ADDITIONAL_INFORMATION
|
||||
context['soil_additional_information'] = SOIL_ADDITIONAL_INFORMATION
|
||||
context['sediment_additional_information'] = SEDIMENT_ADDITIONAL_INFORMATION
|
||||
|
||||
return render(request, 'collections/objects_list.html', context)
|
||||
elif request.method == 'POST':
|
||||
|
||||
log_post_params(request)
|
||||
|
||||
scenario_name = request.POST.get('scenario-name')
|
||||
scenario_description = request.POST.get('scenario-description')
|
||||
scenario_date_year = request.POST.get('scenario-date-year')
|
||||
scenario_date_month = request.POST.get('scenario-date-month')
|
||||
scenario_date_day = request.POST.get('scenario-date-day')
|
||||
|
||||
scenario_date = scenario_date_year
|
||||
if scenario_date_month is not None and scenario_date_month.strip() != '':
|
||||
scenario_date += f'-{int(scenario_date_month):02d}'
|
||||
if scenario_date_day is not None and scenario_date_day.strip() != '':
|
||||
scenario_date += f'-{int(scenario_date_day):02d}'
|
||||
|
||||
scenario_type = request.POST.get('scenario-type')
|
||||
|
||||
additional_information = HTMLGenerator.build_models(request.POST.dict())
|
||||
additional_information = [x for s in additional_information.values() for x in s]
|
||||
|
||||
s = Scenario.create(current_package, name=scenario_name, description=scenario_description,
|
||||
scenario_date=scenario_date, scenario_type=scenario_type,
|
||||
additional_information=additional_information)
|
||||
|
||||
return redirect(s.url)
|
||||
else:
|
||||
return HttpResponseNotAllowed(['GET', ])
|
||||
|
||||
@ -1747,10 +1827,63 @@ def package_scenario(request, package_uuid, scenario_uuid):
|
||||
|
||||
context['scenario'] = current_scenario
|
||||
|
||||
available_add_infs = []
|
||||
for add_inf in NAME_MAPPING.values():
|
||||
available_add_infs.append({
|
||||
'display_name': add_inf.property_name(None),
|
||||
'name': add_inf.__name__,
|
||||
'widget': HTMLGenerator.generate_html(add_inf, prefix=f'{0}')
|
||||
})
|
||||
context['available_additional_information'] = available_add_infs
|
||||
|
||||
context['update_widgets'] = [HTMLGenerator.generate_html(ai, prefix=f'{i}') for i, ai in enumerate(current_scenario.get_additional_information())]
|
||||
|
||||
return render(request, 'objects/scenario.html', context)
|
||||
|
||||
elif request.method == 'POST':
|
||||
|
||||
log_post_params(request)
|
||||
|
||||
if hidden := request.POST.get('hidden', None):
|
||||
|
||||
if hidden == 'delete':
|
||||
current_scenario.delete()
|
||||
return redirect(current_package.url + '/scenario')
|
||||
elif hidden == 'delete-additional-information':
|
||||
uuid = request.POST.get('uuid')
|
||||
current_scenario.remove_additional_information(uuid)
|
||||
return redirect(current_scenario.url)
|
||||
elif hidden == 'delete-all-additional-information':
|
||||
current_scenario.additional_information = dict()
|
||||
current_scenario.save()
|
||||
return redirect(current_scenario.url)
|
||||
elif hidden == 'set-additional-information':
|
||||
ais = HTMLGenerator.build_models(request.POST.dict())
|
||||
|
||||
if s.DEBUG:
|
||||
logger.info(ais)
|
||||
|
||||
current_scenario.set_additional_information(ais)
|
||||
return redirect(current_scenario.url)
|
||||
elif hidden == 'add-additional-information':
|
||||
ais = HTMLGenerator.build_models(request.POST.dict())
|
||||
|
||||
if len(ais.keys()) != 1:
|
||||
raise ValueError('Only one additional information field can be added at a time.')
|
||||
|
||||
ai = list(ais.values())[0][0]
|
||||
|
||||
if s.DEBUG:
|
||||
logger.info(ais)
|
||||
|
||||
current_scenario.add_additional_information(ai)
|
||||
return redirect(current_scenario.url)
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseNotAllowed(['GET', ])
|
||||
return HttpResponseNotAllowed(['GET', 'POST'])
|
||||
|
||||
|
||||
##############
|
||||
|
||||
@ -29,4 +29,4 @@ dependencies = [
|
||||
[tool.uv.sources]
|
||||
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.0" }
|
||||
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
|
||||
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git" }
|
||||
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.4"}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% if meta.can_edit %}
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#new_pathway_modal">
|
||||
<a role="button" data-toggle="modal" data-target="#new_scenario_modal">
|
||||
<span class="glyphicon glyphicon-plus"></span> New Scenario</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
@ -11,6 +11,12 @@
|
||||
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||
</li>
|
||||
{% if meta.can_edit %}
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
|
||||
|
||||
@ -8,10 +8,16 @@
|
||||
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a>
|
||||
</li>
|
||||
<li role="separator" class="divider"></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#download_pathway_modal">
|
||||
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway</a>
|
||||
</li>
|
||||
{% if meta.can_edit %}
|
||||
<li role="separator" class="divider"></li>
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#edit_pathway_modal">
|
||||
|
||||
@ -7,6 +7,12 @@
|
||||
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||
</li>
|
||||
{% if meta.can_edit %}
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
|
||||
|
||||
@ -7,6 +7,12 @@
|
||||
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||
</li>
|
||||
{% if meta.can_edit %}
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Rule</a>
|
||||
|
||||
@ -1,2 +1,14 @@
|
||||
{% if meta.can_edit %}
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#add_additional_information_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Add Additional Information</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#update_scenario_additional_information_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Set Additional Information</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Scenario</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
@ -2,27 +2,21 @@
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
{% if reviewed_objects.count > 50 or unreviewed_objects.count > 50 %}
|
||||
{% if object_type != 'package' %}
|
||||
<div id="load-remaining-button-div">
|
||||
<button class="btn btn-secondary btn-lg btn-block" type="button" id="load-remaining">Load all {% if reviewed_objects.count > 0 %} {{ reviewed_objects.count }} {% else %} {{ unreviewed_objects.count }} {% endif %} {{ object_type }}s
|
||||
</button>
|
||||
<p></p>
|
||||
<div id="load-all-loading"></div>
|
||||
<p></p>
|
||||
<div id="load-all-error" style="display: none;">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
|
||||
<span class="sr-only">Error:</span>
|
||||
Getting objects failed!
|
||||
</div>
|
||||
</div>
|
||||
{% if object_type != 'package' %}
|
||||
<div>
|
||||
<div id="load-all-error" style="display: none;">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
|
||||
<span class="sr-only">Error:</span>
|
||||
Getting objects failed!
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" id="object-search" class="form-control" placeholder="Search by name" style="display: none;">
|
||||
<p></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
<input type="text" id="object-search" class="form-control" placeholder="Search by name"
|
||||
style="display: none;">
|
||||
<p></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% block action_modals %}
|
||||
{% if object_type == 'package' %}
|
||||
@ -248,21 +242,34 @@
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
<style>
|
||||
.spinner-widget {
|
||||
position: fixed; /* stays in place on scroll */
|
||||
bottom: 20px; /* distance from bottom */
|
||||
right: 20px; /* distance from right */
|
||||
z-index: 9999; /* above most elements */
|
||||
width: 60px; /* adjust to gif size */
|
||||
height: 60px;
|
||||
}
|
||||
|
||||
.spinner-widget img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
</style>
|
||||
{% if object_type != 'package' %}
|
||||
<div id="load-all-loading" class="spinner-widget">
|
||||
<img id="loading-gif" src="{% static '/images/wait.gif' %}" alt="Loading...">
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<script>
|
||||
$(function () {
|
||||
|
||||
$('#modal-form-delete-submit').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('#modal-form-delete').submit();
|
||||
});
|
||||
|
||||
$('#object-search').show();
|
||||
|
||||
if ($('#load-remaining').length) {
|
||||
$('#load-remaining').on('click', function () {
|
||||
|
||||
makeLoadingGif("#load-all-loading", "{% static '/images/wait.gif' %}");
|
||||
{% if object_type != 'package' %}
|
||||
setTimeout(function () {
|
||||
$('#load-all-error').hide();
|
||||
|
||||
$.getJSON('?all=true', function (resp) {
|
||||
@ -272,20 +279,26 @@
|
||||
for (o in resp.objects) {
|
||||
obj = resp.objects[o];
|
||||
if (obj.reviewed) {
|
||||
$('#ReviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + '</a>');
|
||||
$('#ReviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + ' <span class="glyphicon glyphicon-star" aria-hidden="true" style="float:right" data-toggle="tooltip" data-placement="top" title="" data-original-title="Reviewed"></span></a>');
|
||||
} else {
|
||||
$('#UnreviewedContent').append('<a class="list-group-item" href="' + obj.url + '">' + obj.name + '</a>');
|
||||
}
|
||||
}
|
||||
|
||||
$('#load-all-loading').empty();
|
||||
$('#load-all-loading').hide();
|
||||
$('#load-remaining').hide();
|
||||
}).fail(function (resp) {
|
||||
$('#load-all-loading').empty();
|
||||
$('#load-all-loading').hide();
|
||||
$('#load-all-error').show();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
}, 2500);
|
||||
{% endif %}
|
||||
|
||||
$('#modal-form-delete-submit').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('#modal-form-delete').submit();
|
||||
});
|
||||
|
||||
$('#object-search').on('keyup', function () {
|
||||
let query = $(this).val().toLowerCase();
|
||||
|
||||
@ -3,7 +3,12 @@
|
||||
{% load static %}
|
||||
<head>
|
||||
<title>{{ title }}</title>
|
||||
|
||||
<style>
|
||||
html, body {
|
||||
height: 100%; /* ensure body fills viewport */
|
||||
overflow-x: hidden; /* prevent horizontal scroll */
|
||||
}
|
||||
</style>
|
||||
{# TODO use bundles from bootstrap 3.3.7 #}
|
||||
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
|
||||
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css">
|
||||
@ -124,7 +129,7 @@
|
||||
</ul>
|
||||
|
||||
<ul class="nav navbar-nav navbar-right navbar-nav-framework navbar-right-framework">
|
||||
{# <li><a href="{{ meta.server_url }}/search" id="searchLink">Search</a></li>#}
|
||||
<li><a href="https://community.envipath.org/" id="communityLink">Community</a></li>
|
||||
<li class="dropdown">
|
||||
<a data-toggle="dropdown" class="dropdown-toggle" href="#">Info <b class="caret"></b></a>
|
||||
<ul role="menu" class="dropdown-menu">
|
||||
@ -197,22 +202,22 @@
|
||||
{% endif %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
{% if meta.current_package.license %}
|
||||
<p></p>
|
||||
<div class="panel-group" id="license_accordion">
|
||||
<div class="panel panel-default list-group-item" style="background-color:#f5f5f5">
|
||||
<div class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#licence_accordion" href="#license">License</a>
|
||||
{% if meta.url_contains_package and meta.current_package.license %}
|
||||
<p></p>
|
||||
<div class="panel-group" id="license_accordion">
|
||||
<div class="panel panel-default list-group-item" style="background-color:#f5f5f5">
|
||||
<div class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#licence_accordion" href="#license">License</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="license" class="panel-collapse collapse in">
|
||||
<div class="panel-body list-group-item">
|
||||
<a target="_blank" href="{{ meta.current_package.license.link }}">
|
||||
<img src="{{ meta.current_package.license.image_link }}">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="license" class="panel-collapse collapse in">
|
||||
<div class="panel-body list-group-item">
|
||||
<a target="_blank" href="{{ meta.current_package.license.link }}">
|
||||
<img src="{{ meta.current_package.license.image_link }}">
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,115 +1,95 @@
|
||||
<div class="modal fade" tabindex="-1" id="new_scenario_modal" role="dialog" aria-labelledby="newScenGenMod"
|
||||
<div class="modal fade" tabindex="-1" id="new_scenario_modal" role="dialog" aria-labelledby="new_scenario_modal"
|
||||
aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">
|
||||
<span aria-hidden="true">×</span> <span
|
||||
class="sr-only">Close</span>
|
||||
<span aria-hidden="true">×</span>
|
||||
<span class="sr-only">Close</span>
|
||||
</button>
|
||||
<h4 class="js-title-step"></h4>
|
||||
<h4 class="modal-title">New Scenario</h4>
|
||||
</div>
|
||||
<form id="base-scenario-form" accept-charset="UTF-8" action="" data-remote="true" method="POST">
|
||||
<div class="modal-body hide" data-step="1" data-title="New Scenario - Step 1">
|
||||
<div class="jumbotron">Please enter name, description,
|
||||
and date of scenario. Date should be associated to the
|
||||
data, not the current date. For example, this could
|
||||
reflect the publishing date of a study. You can leave
|
||||
all fields but the name empty and fill them in
|
||||
later.
|
||||
<div class="modal-body">
|
||||
<form id="new_scenario_form" accept-charset="UTF-8" action="{{ meta.current_package.url }}/scenario"
|
||||
data-remote="true" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="jumbotron">Please enter name, description, and date of scenario. Date should be
|
||||
associated to the data, not the current date. For example, this could reflect the publishing
|
||||
date of a study. You can leave all fields but the name empty and fill them in later.
|
||||
<a target="_blank" href="https://wiki.envipath.org/index.php/scenario" role="button">wiki
|
||||
>></a>
|
||||
</div>
|
||||
<label for="name">Name</label>
|
||||
<input id="name" name="studyname" placeholder="Name" class="form-control"/>
|
||||
<label for="name">Description</label>
|
||||
<input id="description" name="studydescription" placeholder="Description" class="form-control"/>
|
||||
<label for="scenario-name">Name</label>
|
||||
<input id="scenario-name" name="scenario-name" class="form-control" placeholder="Name"/>
|
||||
<label for="scenario-description">Description</label>
|
||||
<input id="scenario-description" name="scenario-description" class="form-control"
|
||||
placeholder="Description"/>
|
||||
<label id="dateField" for="dateYear">Date</label>
|
||||
<table>
|
||||
<tr>
|
||||
<th>
|
||||
<input type="number" id="dateYear" name="dateYear" class="form-control" placeholder="YYYY">
|
||||
<input type="number" id="dateYear" name="scenario-date-year" class="form-control"
|
||||
placeholder="YYYY">
|
||||
</th>
|
||||
<th>
|
||||
<input type="number" id="dateMonth" name="dateMonth" min="1" max="12"
|
||||
<input type="number" id="dateMonth" name="scenario-date-month" min="1" max="12"
|
||||
class="form-control" placeholder="MM" align="">
|
||||
</th>
|
||||
<th>
|
||||
<input type="number" id="dateDay" name="dateDay" min="1" max="31" class="form-control"
|
||||
<input type="number" id="dateDay" name="scenario-date-day" min="1" max="31" class="form-control"
|
||||
placeholder="DD">
|
||||
</th>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<div class="modal-body hide" data-step="2" data-title="New Scenario - Step 2">
|
||||
<div class="jumbotron">
|
||||
Do you want to create an empty scenario and fill it
|
||||
with your own set of attributes, or fill in a
|
||||
pre-defined set of attributes for soil, sludge or sediment
|
||||
data?
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="type" id="radioEmpty" checked>Empty Scenario
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="type" id="radioSoil" value="soil" >Soil Data
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="type" id="radioSludge" value="sludge">Sludge Data
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="type" id="radioSediment" value="sediment">Water-Sediment System Data
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body hide" data-step="3" data-title="New Scenario - Step 3">
|
||||
<div class="jumbotron" id="finaljumbo">
|
||||
All done! Click Submit!
|
||||
</div>
|
||||
<div style="float: left"><button
|
||||
id="addColumnButton" type="button"
|
||||
class="btn btn-default">Add
|
||||
another Scenario
|
||||
</button></div>
|
||||
<input type="hidden" name="fullScenario" value="true"/>
|
||||
{% include "tables/scenario.html" %}
|
||||
</div>
|
||||
</form>
|
||||
<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>
|
||||
{% endfor %}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default js-btn-step pull-left" data-orientation="cancel" data-dismiss="modal"></button>
|
||||
<button type="button" class="btn btn-default js-btn-step" data-orientation="previous"></button>
|
||||
<button type="button" class="btn btn-default js-btn-step"
|
||||
data-orientation="next" id="nextbutton"></button>
|
||||
<a id="new_scenario_modal_form_submit" class="btn btn-primary" href="#">Submit</a>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
$(function () {
|
||||
// Initially hide all "specific" forms
|
||||
$("div[id$='-specific-inputs']").each(function () {
|
||||
$(this).hide();
|
||||
});
|
||||
|
||||
// On change hide all and show only selected
|
||||
$("#scenario-type").change(function () {
|
||||
$("div[id$='-specific-inputs']").each(function () {
|
||||
$(this).hide();
|
||||
});
|
||||
val = $('option:selected', this).val();
|
||||
$("#" + val + "-specific-inputs").show();
|
||||
});
|
||||
|
||||
<p></p>
|
||||
<div id="scenariocontent"></div>
|
||||
$('#new_scenario_modal_form_submit').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('#new_scenario_form').submit();
|
||||
});
|
||||
|
||||
<!--Template index -->
|
||||
<script language="javascript">
|
||||
$(function() {
|
||||
|
||||
// Hide additional columns per default
|
||||
$('.col-2').hide();
|
||||
$('.col-3').hide();
|
||||
|
||||
//TODO just to see modal
|
||||
$('#new_scenario_modal').modalSteps({
|
||||
btnCancelHtml: 'Cancel',
|
||||
btnPreviousHtml: 'Previous',
|
||||
btnNextHtml: 'Next',
|
||||
btnLastStepHtml: 'Submit',
|
||||
disableNextButton: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
</script>
|
||||
|
||||
|
||||
@ -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">×</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>
|
||||
@ -27,7 +27,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="edit-compound-modal-submit">Create</button>
|
||||
<button type="button" class="btn btn-primary" id="edit-compound-modal-submit">Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
61
templates/modals/objects/generic_copy_object_modal.html
Normal file
61
templates/modals/objects/generic_copy_object_modal.html
Normal 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">×</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>
|
||||
@ -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">×</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>
|
||||
@ -5,6 +5,7 @@
|
||||
{% block action_modals %}
|
||||
{% include "modals/objects/edit_rule_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
{% include "modals/objects/edit_compound_modal.html" %}
|
||||
{% include "modals/objects/add_structure_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
|
||||
@ -82,6 +82,7 @@
|
||||
{% include "modals/objects/add_pathway_node_modal.html" %}
|
||||
{% include "modals/objects/add_pathway_edge_modal.html" %}
|
||||
{% include "modals/objects/download_pathway_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/edit_pathway_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/delete_pathway_node_modal.html" %}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
{% block action_modals %}
|
||||
{% include "modals/objects/edit_reaction_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
{% block content %}
|
||||
|
||||
{% block action_modals %}
|
||||
{% include "modals/objects/add_additional_information_modal.html" %}
|
||||
{% include "modals/objects/update_scenario_additional_information_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
<div class="panel-group" id="scenario-detail">
|
||||
@ -24,6 +26,18 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">Description</div>
|
||||
<div class="panel-body">
|
||||
{{ scenario.description }}
|
||||
<br>
|
||||
{{ scenario.scenario_type }}
|
||||
<br>
|
||||
Reported {{ scenario.scenario_date }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="scenario-table" class="table table-bordered table-striped table-hover">
|
||||
<tbody>
|
||||
@ -31,18 +45,42 @@
|
||||
<th>Property</th>
|
||||
<th>Value</th>
|
||||
<th>Unit</th>
|
||||
{% if meta.can_edit %}
|
||||
<th>Remove</th>
|
||||
{% endif %}
|
||||
</tr>
|
||||
|
||||
{% for ai in scenario.get_additional_information %}
|
||||
<tr>
|
||||
<td>{{ ai.property_name|safe }} </td>
|
||||
<td> {{ ai.property_name|safe }} </td>
|
||||
<td> {{ ai.property_data|safe }} </td>
|
||||
<td> {{ ai.property_unit|safe }} </td>
|
||||
<td></td>
|
||||
{% if meta.can_edit %}
|
||||
<td>
|
||||
<form action="{% url 'package scenario detail' scenario.package.uuid scenario.uuid %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="uuid" value="{{ ai.uuid }}">
|
||||
<input type="hidden" name="hidden" value="delete-additional-information">
|
||||
<button type="submit" class="btn"><span class="glyphicon glyphicon-minus"></span></button>
|
||||
</form>
|
||||
</td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
||||
{% if meta.can_edit %}
|
||||
<tr>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>Delete all</td>
|
||||
<td>
|
||||
<form action="{% url 'package scenario detail' scenario.package.uuid scenario.uuid %}" method="post">
|
||||
{% csrf_token %}
|
||||
<input type="hidden" name="hidden" value="delete-all-additional-information">
|
||||
<button type="submit" class="btn"><span class="glyphicon glyphicon-trash"></span></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
{% block action_modals %}
|
||||
{% include "modals/objects/edit_rule_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
|
||||
@ -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
200
tests/test_copy_objects.py
Normal 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)
|
||||
|
||||
@ -8,7 +8,7 @@ from indigo import Indigo, IndigoException, IndigoObject
|
||||
from indigo.renderer import IndigoRenderer
|
||||
from rdkit import Chem
|
||||
from rdkit import RDLogger
|
||||
from rdkit.Chem import MACCSkeys
|
||||
from rdkit.Chem import MACCSkeys, Descriptors
|
||||
from rdkit.Chem import rdChemReactions
|
||||
from rdkit.Chem.Draw import rdMolDraw2D
|
||||
from rdkit.Chem.MolStandardize import rdMolStandardize
|
||||
@ -67,6 +67,18 @@ class PredictionResult(object):
|
||||
|
||||
class FormatConverter(object):
|
||||
|
||||
@staticmethod
|
||||
def mass(smiles):
|
||||
return Descriptors.MolWt(FormatConverter.from_smiles(smiles))
|
||||
|
||||
@staticmethod
|
||||
def charge(smiles):
|
||||
return Chem.GetFormalCharge(FormatConverter.from_smiles(smiles))
|
||||
|
||||
@staticmethod
|
||||
def formula(smiles):
|
||||
return Chem.rdMolDescriptors.CalcMolFormula(FormatConverter.from_smiles(smiles))
|
||||
|
||||
@staticmethod
|
||||
def from_smiles(smiles):
|
||||
return Chem.MolFromSmiles(smiles)
|
||||
@ -79,6 +91,10 @@ class FormatConverter(object):
|
||||
def InChIKey(smiles):
|
||||
return Chem.MolToInchiKey(FormatConverter.from_smiles(smiles))
|
||||
|
||||
@staticmethod
|
||||
def InChI(smiles):
|
||||
return Chem.MolToInchi(FormatConverter.from_smiles(smiles))
|
||||
|
||||
@staticmethod
|
||||
def canonicalize(smiles: str):
|
||||
return FormatConverter.to_smiles(FormatConverter.from_smiles(smiles), canonical=True)
|
||||
@ -682,7 +698,10 @@ class IndigoUtils(object):
|
||||
i.setOption("render-image-size", width, height)
|
||||
i.setOption("render-bond-line-width", 2.0)
|
||||
|
||||
mol = i.loadMolecule(mol_data)
|
||||
if '~' in mol_data:
|
||||
mol = i.loadSmarts(mol_data)
|
||||
else:
|
||||
mol = i.loadMolecule(mol_data)
|
||||
|
||||
if len(functional_groups.keys()) > 0:
|
||||
IndigoUtils._colorize(i, mol, functional_groups, False)
|
||||
|
||||
168
utilities/misc.py
Normal file
168
utilities/misc.py
Normal 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
4
uv.lock
generated
@ -426,7 +426,7 @@ requires-dist = [
|
||||
{ name = "django-ninja", specifier = ">=1.4.1" },
|
||||
{ name = "django-polymorphic", specifier = ">=4.1.0" },
|
||||
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.0" },
|
||||
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git" },
|
||||
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4" },
|
||||
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
|
||||
{ name = "epam-indigo", specifier = ">=1.30.1" },
|
||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||
@ -443,7 +443,7 @@ requires-dist = [
|
||||
[[package]]
|
||||
name = "envipy-additional-information"
|
||||
version = "0.1.0"
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git#4804b24b3479bed6108a49e4401bff8947c03cbd" }
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4#4da604090bf7cf1f3f552d69485472dbc623030a" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user