forked from enviPath/enviPy
Compare commits
15 Commits
beta_2025-
...
beta_2025-
| Author | SHA1 | Date | |
|---|---|---|---|
| a16035677c | |||
| 498a53ab3d | |||
| aa3b53e94b | |||
| 3c8f0e80cb | |||
| 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)
|
||||
|
||||
@ -43,6 +43,7 @@ INSTALLED_APPS = [
|
||||
'django.contrib.staticfiles',
|
||||
# 3rd party
|
||||
'django_extensions',
|
||||
'oauth2_provider',
|
||||
# Custom
|
||||
'epdb',
|
||||
'migration',
|
||||
@ -60,8 +61,13 @@ MIDDLEWARE = [
|
||||
'django.contrib.auth.middleware.AuthenticationMiddleware',
|
||||
'django.contrib.messages.middleware.MessageMiddleware',
|
||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||
'oauth2_provider.middleware.OAuth2TokenMiddleware',
|
||||
]
|
||||
|
||||
OAUTH2_PROVIDER = {
|
||||
"PKCE_REQUIRED": False, # Accept PKCE requests but dont require them
|
||||
}
|
||||
|
||||
if os.environ.get('REGISTRATION_MANDATORY', False) == 'True':
|
||||
MIDDLEWARE.append('epdb.middleware.login_required_middleware.LoginRequiredMiddleware')
|
||||
|
||||
@ -308,11 +314,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
|
||||
@ -324,3 +341,9 @@ FLAGS = {
|
||||
'ENVIFORMER': ENVIFORMER_PRESENT,
|
||||
'APPLICABILITY_DOMAIN': APPLICABILITY_DOMAIN_ENABLED,
|
||||
}
|
||||
|
||||
LOGIN_EXEMPT_URLS = [
|
||||
'/api/legacy/',
|
||||
'/o/token/',
|
||||
'/o/userinfo/',
|
||||
]
|
||||
|
||||
@ -17,11 +17,13 @@ Including another URLconf
|
||||
from django.contrib import admin
|
||||
from django.urls import include, path
|
||||
|
||||
from .api import api
|
||||
from .api import api_v1, api_legacy
|
||||
|
||||
urlpatterns = [
|
||||
path("", include("epdb.urls")),
|
||||
path("", include("migration.urls")),
|
||||
path("admin/", admin.site.urls),
|
||||
path("api/", api.urls),
|
||||
path("api/v1/", api_v1.urls),
|
||||
path("api/legacy/", api_legacy.urls),
|
||||
path("o/", include("oauth2_provider.urls", namespace="oauth2_provider")),
|
||||
]
|
||||
|
||||
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']))
|
||||
)
|
||||
@ -1,7 +1,7 @@
|
||||
from django.conf import settings
|
||||
from django.shortcuts import redirect
|
||||
from django.urls import reverse
|
||||
|
||||
from urllib.parse import quote
|
||||
|
||||
class LoginRequiredMiddleware:
|
||||
def __init__(self, get_response):
|
||||
@ -17,5 +17,8 @@ class LoginRequiredMiddleware:
|
||||
if not request.user.is_authenticated:
|
||||
path = request.path_info
|
||||
if not any(path.startswith(url) for url in self.exempt_urls):
|
||||
if request.method == 'GET':
|
||||
if request.get_full_path() and request.get_full_path() != '/':
|
||||
return redirect(f"{settings.LOGIN_URL}?next={quote(request.get_full_path())}")
|
||||
return redirect(settings.LOGIN_URL)
|
||||
return self.get_response(request)
|
||||
|
||||
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'),
|
||||
),
|
||||
]
|
||||
448
epdb/models.py
448
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
|
||||
|
||||
if description is not None and description.strip() != '':
|
||||
s.description = description
|
||||
s.date = date
|
||||
s.type = type
|
||||
s.additional_information = additional_information
|
||||
|
||||
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
|
||||
|
||||
@ -76,5 +76,7 @@ urlpatterns = [
|
||||
re_path(r'^indigo/dearomatize$', v.dearomatize, name='indigo_dearomatize'),
|
||||
re_path(r'^indigo/layout$', v.layout, name='indigo_layout'),
|
||||
|
||||
re_path(r'^depict$', v.depict, name='depict')
|
||||
re_path(r'^depict$', v.depict, name='depict'),
|
||||
|
||||
path("o/userinfo/", v.userinfo, name="oauth_userinfo"),
|
||||
]
|
||||
|
||||
190
epdb/views.py
190
epdb/views.py
@ -4,16 +4,16 @@ 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 oauth2_provider.decorators import protected_resource
|
||||
|
||||
from utilities.chem import FormatConverter, IndigoUtils
|
||||
from utilities.decorators import package_permission_required
|
||||
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager
|
||||
from utilities.misc import HTMLGenerator
|
||||
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager, EPDBURLParser
|
||||
from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \
|
||||
EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \
|
||||
UserPackagePermission, Permission, License, User, Edge
|
||||
@ -47,6 +47,7 @@ def login(request):
|
||||
|
||||
if request.method == 'GET':
|
||||
context['title'] = 'enviPath'
|
||||
context['next'] = request.GET.get('next', '')
|
||||
return render(request, 'login.html', context)
|
||||
|
||||
elif request.method == 'POST':
|
||||
@ -60,7 +61,7 @@ def login(request):
|
||||
username = request.POST.get('username')
|
||||
password = request.POST.get('password')
|
||||
|
||||
# Get email for username and check if account is active
|
||||
# Get email for username and check if the account is active
|
||||
try:
|
||||
temp_user = get_user_model().objects.get(username=username)
|
||||
|
||||
@ -80,6 +81,10 @@ def login(request):
|
||||
|
||||
if user is not None:
|
||||
login(request, user)
|
||||
|
||||
if next := request.POST.get('next'):
|
||||
return redirect(next)
|
||||
|
||||
return redirect(s.SERVER_URL)
|
||||
else:
|
||||
context['message'] = "Login failed!"
|
||||
@ -150,6 +155,12 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
|
||||
current_user = _anonymous_or_real(request)
|
||||
can_edit = editable(request, current_user)
|
||||
|
||||
parser = EPDBURLParser(request.build_absolute_uri(request.path))
|
||||
|
||||
url_contains_package = False
|
||||
if parser.contains_package_url() or parser.is_package_url():
|
||||
url_contains_package = True
|
||||
|
||||
if for_user:
|
||||
current_user = for_user
|
||||
|
||||
@ -160,6 +171,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 +223,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 +477,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 +802,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 +1731,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 +1741,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 +1762,57 @@ def package_scenarios(request, package_uuid):
|
||||
context['reviewed_objects'] = reviewed_scenario_qs
|
||||
context['unreviewed_objects'] = unreviewed_scenario_qs
|
||||
|
||||
return render(request, 'collections/objects_list.html', context)
|
||||
from envipy_additional_information import SLUDGE_ADDITIONAL_INFORMATION, SOIL_ADDITIONAL_INFORMATION, \
|
||||
SEDIMENT_ADDITIONAL_INFORMATION
|
||||
context['scenario_types'] = {
|
||||
'Soil Data': {
|
||||
'name': 'soil',
|
||||
'widgets': [HTMLGenerator.generate_html(ai, prefix=f'soil_{0}') for ai in
|
||||
[x for s in SOIL_ADDITIONAL_INFORMATION.values() for x in s]]
|
||||
},
|
||||
'Sludge Data': {
|
||||
'name': 'sludge',
|
||||
'widgets': [HTMLGenerator.generate_html(ai, prefix=f'sludge_{0}') for ai in
|
||||
[x for s in SLUDGE_ADDITIONAL_INFORMATION.values() for x in s]]
|
||||
},
|
||||
'Water-Sediment System Data': {
|
||||
'name': 'sediment',
|
||||
'widgets': [HTMLGenerator.generate_html(ai, prefix=f'sediment_{0}') for ai in
|
||||
[x for s in SEDIMENT_ADDITIONAL_INFORMATION.values() for x in s]]
|
||||
}
|
||||
}
|
||||
|
||||
context['sludge_additional_information'] = SLUDGE_ADDITIONAL_INFORMATION
|
||||
context['soil_additional_information'] = SOIL_ADDITIONAL_INFORMATION
|
||||
context['sediment_additional_information'] = SEDIMENT_ADDITIONAL_INFORMATION
|
||||
|
||||
return render(request, 'collections/objects_list.html', context)
|
||||
elif request.method == 'POST':
|
||||
|
||||
log_post_params(request)
|
||||
|
||||
scenario_name = request.POST.get('scenario-name')
|
||||
scenario_description = request.POST.get('scenario-description')
|
||||
scenario_date_year = request.POST.get('scenario-date-year')
|
||||
scenario_date_month = request.POST.get('scenario-date-month')
|
||||
scenario_date_day = request.POST.get('scenario-date-day')
|
||||
|
||||
scenario_date = scenario_date_year
|
||||
if scenario_date_month is not None and scenario_date_month.strip() != '':
|
||||
scenario_date += f'-{int(scenario_date_month):02d}'
|
||||
if scenario_date_day is not None and scenario_date_day.strip() != '':
|
||||
scenario_date += f'-{int(scenario_date_day):02d}'
|
||||
|
||||
scenario_type = request.POST.get('scenario-type')
|
||||
|
||||
additional_information = HTMLGenerator.build_models(request.POST.dict())
|
||||
additional_information = [x for s in additional_information.values() for x in s]
|
||||
|
||||
s = Scenario.create(current_package, name=scenario_name, description=scenario_description,
|
||||
scenario_date=scenario_date, scenario_type=scenario_type,
|
||||
additional_information=additional_information)
|
||||
|
||||
return redirect(s.url)
|
||||
else:
|
||||
return HttpResponseNotAllowed(['GET', ])
|
||||
|
||||
@ -1747,10 +1833,63 @@ def package_scenario(request, package_uuid, scenario_uuid):
|
||||
|
||||
context['scenario'] = current_scenario
|
||||
|
||||
available_add_infs = []
|
||||
for add_inf in NAME_MAPPING.values():
|
||||
available_add_infs.append({
|
||||
'display_name': add_inf.property_name(None),
|
||||
'name': add_inf.__name__,
|
||||
'widget': HTMLGenerator.generate_html(add_inf, prefix=f'{0}')
|
||||
})
|
||||
context['available_additional_information'] = available_add_infs
|
||||
|
||||
context['update_widgets'] = [HTMLGenerator.generate_html(ai, prefix=f'{i}') for i, ai in enumerate(current_scenario.get_additional_information())]
|
||||
|
||||
return render(request, 'objects/scenario.html', context)
|
||||
|
||||
elif request.method == 'POST':
|
||||
|
||||
log_post_params(request)
|
||||
|
||||
if hidden := request.POST.get('hidden', None):
|
||||
|
||||
if hidden == 'delete':
|
||||
current_scenario.delete()
|
||||
return redirect(current_package.url + '/scenario')
|
||||
elif hidden == 'delete-additional-information':
|
||||
uuid = request.POST.get('uuid')
|
||||
current_scenario.remove_additional_information(uuid)
|
||||
return redirect(current_scenario.url)
|
||||
elif hidden == 'delete-all-additional-information':
|
||||
current_scenario.additional_information = dict()
|
||||
current_scenario.save()
|
||||
return redirect(current_scenario.url)
|
||||
elif hidden == 'set-additional-information':
|
||||
ais = HTMLGenerator.build_models(request.POST.dict())
|
||||
|
||||
if s.DEBUG:
|
||||
logger.info(ais)
|
||||
|
||||
current_scenario.set_additional_information(ais)
|
||||
return redirect(current_scenario.url)
|
||||
elif hidden == 'add-additional-information':
|
||||
ais = HTMLGenerator.build_models(request.POST.dict())
|
||||
|
||||
if len(ais.keys()) != 1:
|
||||
raise ValueError('Only one additional information field can be added at a time.')
|
||||
|
||||
ai = list(ais.values())[0][0]
|
||||
|
||||
if s.DEBUG:
|
||||
logger.info(ais)
|
||||
|
||||
current_scenario.add_additional_information(ai)
|
||||
return redirect(current_scenario.url)
|
||||
else:
|
||||
return HttpResponseNotAllowed(['GET', ])
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
return HttpResponseNotAllowed(['GET', 'POST'])
|
||||
|
||||
|
||||
##############
|
||||
@ -2080,3 +2219,16 @@ def depict(request):
|
||||
return HttpResponse(IndigoUtils.smirks_to_svg(smirks, query_smirks), content_type='image/svg+xml')
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@protected_resource()
|
||||
def userinfo(request):
|
||||
user = request.resource_owner
|
||||
res = {
|
||||
"sub": str(user.uuid),
|
||||
"email": user.email,
|
||||
"username": user.username,
|
||||
"name": user.get_full_name() or user.username,
|
||||
"email_verified": user.is_active,
|
||||
}
|
||||
return JsonResponse(res)
|
||||
|
||||
@ -10,6 +10,7 @@ dependencies = [
|
||||
"django-extensions>=4.1",
|
||||
"django-model-utils>=5.0.0",
|
||||
"django-ninja>=1.4.1",
|
||||
"django-oauth-toolkit>=3.0.1",
|
||||
"django-polymorphic>=4.1.0",
|
||||
"enviformer",
|
||||
"envipy-additional-information",
|
||||
@ -29,4 +30,4 @@ dependencies = [
|
||||
[tool.uv.sources]
|
||||
enviformer = { git = "ssh://git@git.envipath.com/enviPath/enviformer.git", rev = "v0.1.0" }
|
||||
envipy-plugins = { git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git", rev = "v0.1.0" }
|
||||
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git" }
|
||||
envipy-additional-information = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git", rev = "v0.1.4"}
|
||||
|
||||
@ -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,14 +2,8 @@
|
||||
{% load static %}
|
||||
{% block content %}
|
||||
|
||||
{% if reviewed_objects.count > 50 or unreviewed_objects.count > 50 %}
|
||||
{% if object_type != 'package' %}
|
||||
<div id="load-remaining-button-div">
|
||||
<button class="btn btn-secondary btn-lg btn-block" type="button" id="load-remaining">Load all {% if reviewed_objects.count > 0 %} {{ reviewed_objects.count }} {% else %} {{ unreviewed_objects.count }} {% endif %} {{ object_type }}s
|
||||
</button>
|
||||
<p></p>
|
||||
<div id="load-all-loading"></div>
|
||||
<p></p>
|
||||
{% 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>
|
||||
@ -18,11 +12,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<input type="text" id="object-search" class="form-control" placeholder="Search by name" style="display: none;">
|
||||
<input type="text" id="object-search" class="form-control" placeholder="Search by name"
|
||||
style="display: none;">
|
||||
<p></p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</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,7 +202,7 @@
|
||||
{% endif %}
|
||||
{% block content %}
|
||||
{% endblock content %}
|
||||
{% if meta.current_package.license %}
|
||||
{% 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">
|
||||
|
||||
@ -124,6 +124,7 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="next" value="{{ next }}" />
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@ -3,13 +3,25 @@
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4>How to cite enviPath</h4>
|
||||
<h3>How to cite enviPath</h3>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
enviPath–The environmental contaminant biotransformation pathway resource. J Wicker, T Lorsbach, M
|
||||
Gütlein, E Schmid, D Latino, S Kramer, K Fenner. Nucleic Acids Research, gkv1229
|
||||
</p>
|
||||
<ol class="list-group list-group-numbered">
|
||||
<li class="list-group-item">
|
||||
Hafner, J., Lorsbach, T., Schmidt, S. <em>et al.</em>
|
||||
<cite>Advancements in biotransformation pathway prediction: enhancements, datasets, and novel
|
||||
functionalities in enviPath.</cite>
|
||||
<a href="https://doi.org/10.1186/s13321-024-00881-6" target="_blank">J Cheminform 16, 93
|
||||
(2024)</a>
|
||||
</li>
|
||||
<li class="list-group-item">
|
||||
Wicker, J., Lorsbach, T., Gütlein, M., Schmid, E., Latino, D., Kramer, S., Fenner, K.
|
||||
<cite>enviPath - The environmental contaminant biotransformation pathway resource</cite>
|
||||
<a href="https://doi.org/10.1093/nar/gkv1229" target="_blank">
|
||||
Nucleic Acids Research, Volume 44, Issue D1, 4 January 2016, Pages D502-D508
|
||||
</a>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
|
||||
@ -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>
|
||||
<label for="scenario-type">Scenario Type</label>
|
||||
<select id="scenario-type" name="scenario-type" class="form-control" data-width='100%'>
|
||||
<option value="empty" selected>Empty Scenario</option>
|
||||
{% for k, v in scenario_types.items %}
|
||||
<option value="{{ v.name }}">{{ k }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
{% for type in scenario_types.values %}
|
||||
<div id="{{ type.name }}-specific-inputs">
|
||||
{% for widget in type.widgets %}
|
||||
{{ widget|safe }}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div class="modal-body hide" data-step="2" data-title="New Scenario - Step 2">
|
||||
<div class="jumbotron">
|
||||
Do you want to create an empty scenario and fill it
|
||||
with your own set of attributes, or fill in a
|
||||
pre-defined set of attributes for soil, sludge or sediment
|
||||
data?
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="type" id="radioEmpty" checked>Empty Scenario
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="type" id="radioSoil" value="soil" >Soil Data
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="type" id="radioSludge" value="sludge">Sludge Data
|
||||
</label>
|
||||
</div>
|
||||
<div class="radio">
|
||||
<label>
|
||||
<input type="radio" name="type" id="radioSediment" value="sediment">Water-Sediment System Data
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body hide" data-step="3" data-title="New Scenario - Step 3">
|
||||
<div class="jumbotron" id="finaljumbo">
|
||||
All done! Click Submit!
|
||||
</div>
|
||||
<div style="float: left"><button
|
||||
id="addColumnButton" type="button"
|
||||
class="btn btn-default">Add
|
||||
another Scenario
|
||||
</button></div>
|
||||
<input type="hidden" name="fullScenario" value="true"/>
|
||||
{% include "tables/scenario.html" %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default js-btn-step pull-left" data-orientation="cancel" data-dismiss="modal"></button>
|
||||
<button type="button" class="btn btn-default js-btn-step" data-orientation="previous"></button>
|
||||
<button type="button" class="btn btn-default js-btn-step"
|
||||
data-orientation="next" id="nextbutton"></button>
|
||||
<a id="new_scenario_modal_form_submit" class="btn btn-primary" href="#">Submit</a>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p></p>
|
||||
<div id="scenariocontent"></div>
|
||||
|
||||
<!--Template index -->
|
||||
<script language="javascript">
|
||||
$(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>
|
||||
$(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();
|
||||
});
|
||||
|
||||
$('#new_scenario_modal_form_submit').on('click', function (e) {
|
||||
e.preventDefault();
|
||||
$('#new_scenario_form').submit();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
</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,6 +698,9 @@ class IndigoUtils(object):
|
||||
i.setOption("render-image-size", width, height)
|
||||
i.setOption("render-bond-line-width", 2.0)
|
||||
|
||||
if '~' in mol_data:
|
||||
mol = i.loadSmarts(mol_data)
|
||||
else:
|
||||
mol = i.loadMolecule(mol_data)
|
||||
|
||||
if len(functional_groups.keys()) > 0:
|
||||
|
||||
168
utilities/misc.py
Normal file
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
|
||||
156
uv.lock
generated
156
uv.lock
generated
@ -200,6 +200,63 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cffi"
|
||||
version = "1.17.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pycparser" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191 },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592 },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/cc/4635c320081c78d6ffc2cab0a76025b691a91204f4aa317d568ff9280a2d/cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382", size = 426024 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/7b/3b2b250f3aab91abe5f8a51ada1b717935fdaec53f790ad4100fe2ec64d1/cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702", size = 448188 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/48/1b9283ebbf0ec065148d8de05d647a986c5f22586b18120020452fff8f5d/cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3", size = 455571 },
|
||||
{ url = "https://files.pythonhosted.org/packages/40/87/3b8452525437b40f39ca7ff70276679772ee7e8b394934ff60e63b7b090c/cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6", size = 436687 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/fb/4da72871d177d63649ac449aec2e8a29efe0274035880c7af59101ca2232/cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17", size = 446211 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/a0/62f00bcb411332106c02b663b26f3545a9ef136f80d5df746c05878f8c4b/cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8", size = 461325 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/83/76127035ed2e7e27b0787604d99da630ac3123bfb02d8e80c633f218a11d/cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e", size = 438784 },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/81/a6cd025db2f08ac88b901b745c163d884641909641f9b826e8cb87645942/cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be", size = 461564 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/fe/4d41c2f200c4a457933dbd98d3cf4e911870877bd94d9656cc0fcb390681/cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c", size = 171804 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/b6/0b0f5ab93b0df4acc49cae758c81fe4e5ef26c3ae2e10cc69249dfd8b3ab/cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15", size = 181299 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264 },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651 },
|
||||
{ url = "https://files.pythonhosted.org/packages/94/dd/a3f0118e688d1b1a57553da23b16bdade96d2f9bcda4d32e7d2838047ff7/cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4", size = 445259 },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/ea/70ce63780f096e16ce8588efe039d3c4f91deb1dc01e9c73a287939c79a6/cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41", size = 469200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/a0/a4fa9f4f781bda074c3ddd57a572b060fa0df7655d2a4247bbe277200146/cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1", size = 477235 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/12/ce8710b5b8affbcdd5c6e367217c242524ad17a02fe5beec3ee339f69f85/cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6", size = 459721 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/6b/d45873c5e0242196f042d555526f92aa9e0c32355a1be1ff8c27f077fd37/cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d", size = 467242 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/52/d9a0e523a572fbccf2955f5abe883cfa8bcc570d7faeee06336fbd50c9fc/cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6", size = 477999 },
|
||||
{ url = "https://files.pythonhosted.org/packages/44/74/f2a2460684a1a2d00ca799ad880d54652841a780c4c97b87754f660c7603/cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f", size = 454242 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/4a/34599cac7dfcd888ff54e801afe06a19c17787dfd94495ab0c8d35fe99fb/cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b", size = 478604 },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/33/e1b8a1ba29025adbdcda5fb3a36f94c03d771c1b7b12f726ff7fef2ebe36/cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655", size = 171727 },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/97/50228be003bb2802627d28ec0627837ac0bf35c90cf769812056f235b2d1/cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0", size = 181400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
|
||||
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
|
||||
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
|
||||
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
|
||||
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "charset-normalizer"
|
||||
version = "3.4.2"
|
||||
@ -319,6 +376,53 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "45.0.7"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105 },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504 },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542 },
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975 },
|
||||
{ url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397 },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244 },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862 },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400 },
|
||||
{ url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233 },
|
||||
{ url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075 },
|
||||
{ url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517 },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893 },
|
||||
{ url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132 },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086 },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186 },
|
||||
{ url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639 },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552 },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742 },
|
||||
{ url = "https://files.pythonhosted.org/packages/13/3e/e42f1528ca1ea82256b835191eab1be014e0f9f934b60d98b0be8a38ed70/cryptography-45.0.7-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:de58755d723e86175756f463f2f0bddd45cc36fbd62601228a3f8761c9f58252", size = 3572442 },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/aa/e947693ab08674a2663ed2534cd8d345cf17bf6a1facf99273e8ec8986dc/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a20e442e917889d1a6b3c570c9e3fa2fdc398c20868abcea268ea33c024c4083", size = 4142233 },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/06/09b6f6a2fc43474a32b8fe259038eef1500ee3d3c141599b57ac6c57612c/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:258e0dff86d1d891169b5af222d362468a9570e2532923088658aa866eb11130", size = 4376202 },
|
||||
{ url = "https://files.pythonhosted.org/packages/00/f2/c166af87e95ce6ae6d38471a7e039d3a0549c2d55d74e059680162052824/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d97cf502abe2ab9eff8bd5e4aca274da8d06dd3ef08b759a8d6143f4ad65d4b4", size = 4141900 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/b9/e96e0b6cb86eae27ea51fa8a3151535a18e66fe7c451fa90f7f89c85f541/cryptography-45.0.7-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:c987dad82e8c65ebc985f5dae5e74a3beda9d0a2a4daf8a1115f3772b59e5141", size = 4375562 },
|
||||
{ url = "https://files.pythonhosted.org/packages/36/d0/36e8ee39274e9d77baf7d0dafda680cba6e52f3936b846f0d56d64fec915/cryptography-45.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c13b1e3afd29a5b3b2656257f14669ca8fa8d7956d509926f0b130b600b50ab7", size = 3322781 },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/4e/49199a4c82946938a3e05d2e8ad9482484ba48bbc1e809e3d506c686d051/cryptography-45.0.7-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a862753b36620af6fc54209264f92c716367f2f0ff4624952276a6bbd18cbde", size = 3584634 },
|
||||
{ url = "https://files.pythonhosted.org/packages/16/ce/5f6ff59ea9c7779dba51b84871c19962529bdcc12e1a6ea172664916c550/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:06ce84dc14df0bf6ea84666f958e6080cdb6fe1231be2a51f3fc1267d9f3fb34", size = 4149533 },
|
||||
{ url = "https://files.pythonhosted.org/packages/ce/13/b3cfbd257ac96da4b88b46372e662009b7a16833bfc5da33bb97dd5631ae/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d0c5c6bac22b177bf8da7435d9d27a6834ee130309749d162b26c3105c0795a9", size = 4385557 },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/c5/8c59d6b7c7b439ba4fc8d0cab868027fd095f215031bc123c3a070962912/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:2f641b64acc00811da98df63df7d59fd4706c0df449da71cb7ac39a0732b40ae", size = 4149023 },
|
||||
{ url = "https://files.pythonhosted.org/packages/55/32/05385c86d6ca9ab0b4d5bb442d2e3d85e727939a11f3e163fc776ce5eb40/cryptography-45.0.7-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:f5414a788ecc6ee6bc58560e85ca624258a55ca434884445440a810796ea0e0b", size = 4385722 },
|
||||
{ url = "https://files.pythonhosted.org/packages/23/87/7ce86f3fa14bc11a5a48c30d8103c26e09b6465f8d8e9d74cf7a0714f043/cryptography-45.0.7-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f3d56f73595376f4244646dd5c5870c14c196949807be39e79e7bd9bac3da63", size = 3332908 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django"
|
||||
version = "5.2.1"
|
||||
@ -370,6 +474,21 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fd/9f/a6d819a151723f44a85b1bfebfa60cd09d0219313b175023f5593bb47753/django_ninja-1.4.1-py3-none-any.whl", hash = "sha256:a091aa69be6ba75a89c5043d35f99cf9bf4f5c26e1ac6783accf8eaa1f8cb12b", size = 2425909 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-oauth-toolkit"
|
||||
version = "3.0.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "django" },
|
||||
{ name = "jwcrypto" },
|
||||
{ name = "oauthlib" },
|
||||
{ name = "requests" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/fa/d3/d7628a7a4899bf5aafc9c1ec121c507470b37a247f7628acae6e0f78e0d6/django_oauth_toolkit-3.0.1.tar.gz", hash = "sha256:7200e4a9fb229b145a6d808cbf0423b6d69a87f68557437733eec3c0cf71db02", size = 99816 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/40/e556bc19ba65356fe5f0e48ca01c50e81f7c630042fa7411b6ab428ecf68/django_oauth_toolkit-3.0.1-py3-none-any.whl", hash = "sha256:3ef00b062a284f2031b0732b32dc899e3bbf0eac221bbb1cffcb50b8932e55ed", size = 77299 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "django-polymorphic"
|
||||
version = "4.1.0"
|
||||
@ -401,6 +520,7 @@ dependencies = [
|
||||
{ name = "django-extensions" },
|
||||
{ name = "django-model-utils" },
|
||||
{ name = "django-ninja" },
|
||||
{ name = "django-oauth-toolkit" },
|
||||
{ name = "django-polymorphic" },
|
||||
{ name = "enviformer" },
|
||||
{ name = "envipy-additional-information" },
|
||||
@ -424,9 +544,10 @@ requires-dist = [
|
||||
{ name = "django-extensions", specifier = ">=4.1" },
|
||||
{ name = "django-model-utils", specifier = ">=5.0.0" },
|
||||
{ name = "django-ninja", specifier = ">=1.4.1" },
|
||||
{ name = "django-oauth-toolkit", specifier = ">=3.0.1" },
|
||||
{ name = "django-polymorphic", specifier = ">=4.1.0" },
|
||||
{ name = "enviformer", git = "ssh://git@git.envipath.com/enviPath/enviformer.git?rev=v0.1.0" },
|
||||
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git" },
|
||||
{ name = "envipy-additional-information", git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4" },
|
||||
{ name = "envipy-plugins", git = "ssh://git@git.envipath.com/enviPath/enviPy-plugins.git?rev=v0.1.0" },
|
||||
{ name = "epam-indigo", specifier = ">=1.30.1" },
|
||||
{ name = "gunicorn", specifier = ">=23.0.0" },
|
||||
@ -443,7 +564,7 @@ requires-dist = [
|
||||
[[package]]
|
||||
name = "envipy-additional-information"
|
||||
version = "0.1.0"
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git#4804b24b3479bed6108a49e4401bff8947c03cbd" }
|
||||
source = { git = "ssh://git@git.envipath.com/enviPath/enviPy-additional-information.git?rev=v0.1.4#4da604090bf7cf1f3f552d69485472dbc623030a" }
|
||||
dependencies = [
|
||||
{ name = "pydantic" },
|
||||
]
|
||||
@ -625,6 +746,19 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/4f/1195bbac8e0c2acc5f740661631d8d750dc38d4a32b23ee5df3cde6f4e0d/joblib-1.5.1-py3-none-any.whl", hash = "sha256:4719a31f054c7d766948dcd83e9613686b27114f190f717cec7eaa2084f8a74a", size = 307746 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jwcrypto"
|
||||
version = "1.5.6"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "cryptography" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/db/870e5d5fb311b0bcf049630b5ba3abca2d339fd5e13ba175b4c13b456d08/jwcrypto-1.5.6.tar.gz", hash = "sha256:771a87762a0c081ae6166958a954f80848820b2ab066937dc8b8379d65b1b039", size = 87168 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cd/58/4a1880ea64032185e9ae9f63940c9327c6952d5584ea544a8f66972f2fda/jwcrypto-1.5.6-py3-none-any.whl", hash = "sha256:150d2b0ebbdb8f40b77f543fb44ffd2baeff48788be71f67f03566692fd55789", size = 92520 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "kombu"
|
||||
version = "5.5.3"
|
||||
@ -1042,6 +1176,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oauthlib"
|
||||
version = "3.3.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/0b/5f/19930f824ffeb0ad4372da4812c50edbd1434f678c90c2733e1188edfc63/oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9", size = 185918 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/be/9c/92789c596b8df838baa98fa71844d84283302f7604ed565dafe5a6b5041a/oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1", size = 160065 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "25.0"
|
||||
@ -1284,6 +1427,15 @@ wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pycparser"
|
||||
version = "2.22"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pydantic"
|
||||
version = "2.11.7"
|
||||
|
||||
Reference in New Issue
Block a user