forked from enviPath/enviPy
Compare commits
5 Commits
f9cc71d375
...
develop-ba
| Author | SHA1 | Date | |
|---|---|---|---|
| 421d33dddc | |||
| 72a63b4876 | |||
| 2c2437e3f5 | |||
| 9bc9f86ff1 | |||
| dba6514013 |
@ -185,7 +185,7 @@ class PESStructure(CompoundStructure):
|
|||||||
def create(
|
def create(
|
||||||
compound: Compound,
|
compound: Compound,
|
||||||
pes_link: str,
|
pes_link: str,
|
||||||
mol_file: str,
|
molfile: str,
|
||||||
smiles: str,
|
smiles: str,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
@ -204,7 +204,7 @@ class PESStructure(CompoundStructure):
|
|||||||
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
cs.description = nh3.clean(description, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
|
||||||
cs.smiles = smiles
|
cs.smiles = smiles
|
||||||
cs.mol_file = mol_file
|
cs.molfile = molfile
|
||||||
cs.pes_link = pes_link
|
cs.pes_link = pes_link
|
||||||
cs.compound = compound
|
cs.compound = compound
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,11 @@
|
|||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
<div class="collapse-content">{{ compound_structure.pes_link }}</div>
|
<div class="collapse-content">
|
||||||
|
<p>
|
||||||
|
<a href="{{ compound_structure.pes_link }}" class="hover:bg-base-200">{{ compound_structure.pes_link }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Representation -->
|
<!-- Image Representation -->
|
||||||
|
|||||||
@ -3,7 +3,11 @@
|
|||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
<div class="collapse-content">{{ compound.default_structure.pes_link }}</div>
|
<div class="collapse-content">
|
||||||
|
<p>
|
||||||
|
<a href="{{ compound.default_structure.pes_link }}" class="hover:bg-base-200">{{ compound.default_structure.pes_link }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Representation -->
|
<!-- Image Representation -->
|
||||||
|
|||||||
@ -3,7 +3,11 @@
|
|||||||
<div class="collapse-arrow bg-base-200 collapse">
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
<input type="checkbox" checked />
|
<input type="checkbox" checked />
|
||||||
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
<div class="collapse-title text-xl font-medium">Link to PES</div>
|
||||||
<div class="collapse-content">{{ node.default_node_label.pes_link }}</div>
|
<div class="collapse-content">
|
||||||
|
<p>
|
||||||
|
<a href="{{ node.default_node_label.pes_link }}" class="hover:bg-base-200">{{ node.default_node_label.pes_link }}</a>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Image Representation -->
|
<!-- Image Representation -->
|
||||||
|
|||||||
@ -2,14 +2,13 @@ import base64
|
|||||||
|
|
||||||
import requests
|
import requests
|
||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.core.exceptions import BadRequest
|
from django.http import HttpResponse, HttpResponseBadRequest
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.shortcuts import redirect
|
from django.shortcuts import redirect
|
||||||
|
|
||||||
from bayer.models import PESCompound
|
from bayer.models import PESCompound
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Pathway, Node
|
from epdb.models import Pathway, Node
|
||||||
from epdb.views import _anonymous_or_real
|
from epdb.views import _anonymous_or_real, error
|
||||||
from utilities.decorators import package_permission_required
|
from utilities.decorators import package_permission_required
|
||||||
|
|
||||||
Package = s.GET_PACKAGE_MODEL()
|
Package = s.GET_PACKAGE_MODEL()
|
||||||
@ -23,7 +22,11 @@ def create_pes(request, package_uuid):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
||||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
raise BadRequest("Cannot create PESs for internal packages.")
|
return error(
|
||||||
|
request,
|
||||||
|
f'Creation of PESs for package {current_package.name} failed!',
|
||||||
|
"Creating PESs for internal packages is not allowed.",
|
||||||
|
)
|
||||||
|
|
||||||
compound_name = request.POST.get('compound-name')
|
compound_name = request.POST.get('compound-name')
|
||||||
compound_description = request.POST.get('compound-description')
|
compound_description = request.POST.get('compound-description')
|
||||||
@ -33,25 +36,25 @@ def create_pes(request, package_uuid):
|
|||||||
try:
|
try:
|
||||||
pes_data = fetch_pes(request, pes_link)
|
pes_data = fetch_pes(request, pes_link)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
return HttpResponseBadRequest(f"Could not fetch PES data for {pes_link}")
|
||||||
|
|
||||||
classification = pes_data.get("classificationLevel", "")
|
classification = pes_data.get("classificationLevel", "")
|
||||||
if "secret" == classification.lower():
|
if "secret" == classification.lower():
|
||||||
|
|
||||||
if current_package.classification_level != Package.Classification.SECRET:
|
if current_package.classification_level != Package.Classification.SECRET:
|
||||||
return BadRequest("Cannot create PESs for non-secret packages.")
|
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.")
|
||||||
|
|
||||||
data_pools = pes_data.get("dataPools")
|
data_pools = pes_data.get("dataPools")
|
||||||
if data_pools:
|
if data_pools:
|
||||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
return BadRequest(
|
return HttpResponseBadRequest(
|
||||||
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||||
|
|
||||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
|
|
||||||
return redirect(pes.url)
|
return redirect(pes.url)
|
||||||
else:
|
else:
|
||||||
return BadRequest("Please provide a PES link.")
|
return HttpResponseBadRequest("Please provide a PES link.")
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@ -65,7 +68,11 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
|
|
||||||
if current_package.classification_level == Package.Classification.INTERNAL:
|
if current_package.classification_level == Package.Classification.INTERNAL:
|
||||||
raise BadRequest("Cannot create PESs for internal packages.")
|
return error(
|
||||||
|
request,
|
||||||
|
f'Creation of PESs for package {current_package.name} failed!',
|
||||||
|
"Creating PESs for internal packages is not allowed.",
|
||||||
|
)
|
||||||
|
|
||||||
compound_name = request.POST.get('compound-name')
|
compound_name = request.POST.get('compound-name')
|
||||||
compound_description = request.POST.get('compound-description')
|
compound_description = request.POST.get('compound-description')
|
||||||
@ -75,18 +82,18 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
try:
|
try:
|
||||||
pes_data = fetch_pes(request, pes_link)
|
pes_data = fetch_pes(request, pes_link)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
return BadRequest(f"Could not fetch PES data for {pes_link}")
|
return HttpResponseBadRequest(f"Could not fetch PES data for {pes_link}")
|
||||||
|
|
||||||
classification = pes_data.get("classificationLevel", "")
|
classification = pes_data.get("classificationLevel", "")
|
||||||
if "secret" == classification.lower():
|
if "secret" == classification.lower():
|
||||||
|
|
||||||
if current_package.classification_level != Package.Classification.SECRET:
|
if current_package.classification_level != Package.Classification.SECRET:
|
||||||
return BadRequest("Cannot create PESs for non-secret packages.")
|
return HttpResponseBadRequest("Cannot create PESs for non-secret packages.")
|
||||||
|
|
||||||
data_pools = pes_data.get("dataPools")
|
data_pools = pes_data.get("dataPools")
|
||||||
if data_pools:
|
if data_pools:
|
||||||
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
if s.DATA_POOL_MAPPING[current_package.data_pool.name] not in data_pools:
|
||||||
return BadRequest(
|
return HttpResponseBadRequest(
|
||||||
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
f"PES data pool {s.DATA_POOL_MAPPING[current_package.data_pool.name]} not found in PES data")
|
||||||
|
|
||||||
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
pes = PESCompound.create(current_package, pes_data, compound_name, compound_description)
|
||||||
@ -109,7 +116,7 @@ def create_pes_node(request, package_uuid, pathway_uuid):
|
|||||||
return redirect(current_pathway.url)
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return BadRequest("Please provide a PES link.")
|
return HttpResponseBadRequest("Please provide a PES link.")
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|||||||
@ -117,25 +117,28 @@ class APIPermissionTestBase(TestCase):
|
|||||||
|
|
||||||
# Create test compounds in each package
|
# Create test compounds in each package
|
||||||
cls.reviewed_compound = Compound.create(
|
cls.reviewed_compound = Compound.create(
|
||||||
cls.reviewed_package, "C", "Reviewed Compound", "Test compound"
|
cls.reviewed_package, "C", name="Reviewed Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.owned_compound = Compound.create(
|
cls.owned_compound = Compound.create(
|
||||||
cls.unreviewed_package_owned, "CC", "Owned Compound", "Test compound"
|
cls.unreviewed_package_owned, "CC", name="Owned Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.read_compound = Compound.create(
|
cls.read_compound = Compound.create(
|
||||||
cls.unreviewed_package_read, "CCC", "Read Compound", "Test compound"
|
cls.unreviewed_package_read, "CCC", name="Read Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.write_compound = Compound.create(
|
cls.write_compound = Compound.create(
|
||||||
cls.unreviewed_package_write, "CCCC", "Write Compound", "Test compound"
|
cls.unreviewed_package_write, "CCCC", name="Write Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.all_compound = Compound.create(
|
cls.all_compound = Compound.create(
|
||||||
cls.unreviewed_package_all, "CCCCC", "All Compound", "Test compound"
|
cls.unreviewed_package_all, "CCCCC", name="All Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
cls.no_access_compound = Compound.create(
|
cls.no_access_compound = Compound.create(
|
||||||
cls.unreviewed_package_no_access, "CCCCCC", "No Access Compound", "Test compound"
|
cls.unreviewed_package_no_access,
|
||||||
|
"CCCCCC",
|
||||||
|
name="No Access Compound",
|
||||||
|
description="Test compound",
|
||||||
)
|
)
|
||||||
cls.group_compound = Compound.create(
|
cls.group_compound = Compound.create(
|
||||||
cls.group_package, "CCCCCCC", "Group Compound", "Test compound"
|
cls.group_package, "CCCCCCC", name="Group Compound", description="Test compound"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -294,8 +294,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|||||||
return Compound.create(
|
return Compound.create(
|
||||||
package,
|
package,
|
||||||
smiles,
|
smiles,
|
||||||
f"Reviewed Compound {idx:03d}",
|
name=f"Reviewed Compound {idx:03d}",
|
||||||
"Compound for pagination tests",
|
description="Compound for pagination tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@ -305,8 +305,8 @@ class CompoundPaginationAPITest(BaseTestAPIGetPaginated, TestCase):
|
|||||||
return Compound.create(
|
return Compound.create(
|
||||||
package,
|
package,
|
||||||
smiles,
|
smiles,
|
||||||
f"Draft Compound {idx:03d}",
|
name=f"Draft Compound {idx:03d}",
|
||||||
"Compound for pagination tests",
|
description="Compound for pagination tests",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1,2 +1,10 @@
|
|||||||
class InvalidSMILESException(Exception):
|
class InvalidSMILESException(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidMolfileException(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class PackageImportException(Exception):
|
||||||
|
pass
|
||||||
|
|||||||
@ -633,9 +633,14 @@ class CompoundSchema(Schema):
|
|||||||
reviewStatus: str = Field(False, alias="review_status")
|
reviewStatus: str = Field(False, alias="review_status")
|
||||||
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
||||||
structures: List["CompoundStructureSchema"] = []
|
structures: List["CompoundStructureSchema"] = []
|
||||||
|
pesLink: str | None = Field(None, alias="pes_link")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_status(obj: CompoundStructure):
|
def resolve_pes_link(obj: Compound):
|
||||||
|
return getattr(obj.default_structure, "pes_link", None)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def resolve_review_status(obj: Compound):
|
||||||
return "reviewed" if obj.package.reviewed else "unreviewed"
|
return "reviewed" if obj.package.reviewed else "unreviewed"
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -709,6 +714,7 @@ class CompoundStructureSchema(Schema):
|
|||||||
reviewStatus: str = Field(None, alias="review_status")
|
reviewStatus: str = Field(None, alias="review_status")
|
||||||
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
scenarios: List["SimpleScenario"] = Field([], alias="scenarios")
|
||||||
smiles: str = Field(None, alias="smiles")
|
smiles: str = Field(None, alias="smiles")
|
||||||
|
pesLink: str | None = Field(None, alias="pes_link")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_review_status(obj: CompoundStructure):
|
def resolve_review_status(obj: CompoundStructure):
|
||||||
@ -844,6 +850,7 @@ def get_package_compound_structure(request, package_uuid, compound_uuid, structu
|
|||||||
|
|
||||||
class CreateCompound(Schema):
|
class CreateCompound(Schema):
|
||||||
compoundSmiles: str
|
compoundSmiles: str
|
||||||
|
compoundMolFile: str | None = None
|
||||||
compoundName: str | None = None
|
compoundName: str | None = None
|
||||||
compoundDescription: str | None = None
|
compoundDescription: str | None = None
|
||||||
inchi: str | None = None
|
inchi: str | None = None
|
||||||
@ -883,7 +890,12 @@ def create_package_compound(
|
|||||||
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
|
c = PESCompound.create(p, pes_data, c.compoundName, c.compoundDescription)
|
||||||
else:
|
else:
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
p, c.compoundSmiles, c.compoundName, c.compoundDescription, inchi=c.inchi
|
p,
|
||||||
|
c.compoundSmiles,
|
||||||
|
molfile=c.compoundMolFile,
|
||||||
|
name=c.compoundName,
|
||||||
|
description=c.compoundDescription,
|
||||||
|
inchi=c.inchi
|
||||||
)
|
)
|
||||||
return redirect(c.url)
|
return redirect(c.url)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
@ -1687,14 +1699,15 @@ class PathwayNode(Schema):
|
|||||||
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
dt50s: List[Dict[str, str]] = Field([], alias="dt50s")
|
||||||
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
engineeredIntermediate: bool = Field(None, alias="engineered_intermediate")
|
||||||
id: str = Field(None, alias="url")
|
id: str = Field(None, alias="url")
|
||||||
idcomp: str = Field(None, alias="default_node_label.url")
|
idcomp: str = Field(None, alias="node_label_id")
|
||||||
idreact: str = Field(None, alias="default_node_label.url")
|
idreact: str = Field(None, alias="node_label_id")
|
||||||
image: str = Field(None, alias="image")
|
image: str = Field(None, alias="image")
|
||||||
imageSize: int = Field(None, alias="image_size")
|
imageSize: int = Field(None, alias="image_size")
|
||||||
name: str = Field(None, alias="name")
|
name: str = Field(None, alias="name")
|
||||||
proposed: List[Dict[str, str]] = Field([], alias="proposed_intermediate")
|
proposed: List[Dict[str, str]] = []
|
||||||
smiles: str = Field(None, alias="default_node_label.smiles")
|
smiles: str = Field(None, alias="smiles")
|
||||||
pseudo: bool = Field(False, alias="pseudo")
|
pseudo: bool = Field(False, alias="pseudo")
|
||||||
|
pesLink: str | None = Field(None, alias="pes_link")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_atom_count(obj: Node):
|
def resolve_atom_count(obj: Node):
|
||||||
@ -1707,24 +1720,10 @@ class PathwayNode(Schema):
|
|||||||
# TODO
|
# TODO
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_engineered_intermediate(obj: Node):
|
|
||||||
# TODO
|
|
||||||
return False
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_image(obj: Node):
|
|
||||||
return f"{obj.default_node_label.url}?image=svg"
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def resolve_image_size(obj: Node):
|
def resolve_image_size(obj: Node):
|
||||||
return 400
|
return 400
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def resolve_proposed_intermediate(obj: Node):
|
|
||||||
# TODO
|
|
||||||
return []
|
|
||||||
|
|
||||||
|
|
||||||
class PathwaySchema(Schema):
|
class PathwaySchema(Schema):
|
||||||
aliases: List[str] = Field([], alias="aliases")
|
aliases: List[str] = Field([], alias="aliases")
|
||||||
@ -1976,6 +1975,7 @@ def get_package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
|||||||
|
|
||||||
class CreateNode(Schema):
|
class CreateNode(Schema):
|
||||||
nodeAsSmiles: str
|
nodeAsSmiles: str
|
||||||
|
nodeAsMolFile: str | None = None
|
||||||
nodeName: str | None = None
|
nodeName: str | None = None
|
||||||
nodeReason: str | None = None
|
nodeReason: str | None = None
|
||||||
nodeDepth: str | None = None
|
nodeDepth: str | None = None
|
||||||
@ -2037,7 +2037,14 @@ def add_pathway_node(request, package_uuid, pathway_uuid, n: Form[CreateNode]):
|
|||||||
else:
|
else:
|
||||||
node_depth = -1
|
node_depth = -1
|
||||||
|
|
||||||
node = Node.create(pw, n.nodeAsSmiles, node_depth, n.nodeName, n.nodeReason)
|
node = Node.create(
|
||||||
|
pw,
|
||||||
|
n.nodeAsSmiles,
|
||||||
|
node_depth,
|
||||||
|
molfile=n.nodeAsMolFile,
|
||||||
|
name=n.nodeName,
|
||||||
|
description=n.nodeReason,
|
||||||
|
)
|
||||||
|
|
||||||
return redirect(node.url)
|
return redirect(node.url)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
@ -2392,3 +2399,26 @@ def get_setting(request, setting_uuid):
|
|||||||
return 403, {
|
return 403, {
|
||||||
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
"message": f"Getting Setting with id {setting_uuid} failed due to insufficient rights!"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
########
|
||||||
|
# Util #
|
||||||
|
########
|
||||||
|
class NonPersistent(Schema):
|
||||||
|
smiles: str
|
||||||
|
setting_url: str = Field(..., alias="settingUri")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/util", response={200: Any, 403: Error})
|
||||||
|
def predict(request, np: Form[NonPersistent]):
|
||||||
|
try:
|
||||||
|
from epdb.logic import SPathway
|
||||||
|
|
||||||
|
setting = SettingManager.get_setting_by_url(request.user, np.setting_url)
|
||||||
|
spw = SPathway(prediction_setting=setting, root_nodes=[np.smiles])
|
||||||
|
spw.predict()
|
||||||
|
return spw.to_json()
|
||||||
|
except ValueError:
|
||||||
|
return 403, {
|
||||||
|
"message": f"Getting Setting with id {np.setting_url} failed due to insufficient rights!"
|
||||||
|
}
|
||||||
|
|||||||
@ -1078,10 +1078,8 @@ class PackageManager(object):
|
|||||||
data: Dict[str, Any],
|
data: Dict[str, Any],
|
||||||
owner: User,
|
owner: User,
|
||||||
preserve_uuids=False,
|
preserve_uuids=False,
|
||||||
add_import_timestamp=True,
|
|
||||||
trust_reviewed=False,
|
|
||||||
) -> Package:
|
) -> Package:
|
||||||
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
|
importer = PackageImporter(data, preserve_uuids)
|
||||||
imported_package = importer.do_import()
|
imported_package = importer.do_import()
|
||||||
|
|
||||||
up = UserPackagePermission()
|
up = UserPackagePermission()
|
||||||
@ -1898,6 +1896,12 @@ class SPathway(object):
|
|||||||
"to": to_indices,
|
"to": to_indices,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if edge.rule:
|
||||||
|
e["rule"] = edge.rule.simple_json()
|
||||||
|
|
||||||
|
if edge.probability:
|
||||||
|
e["probability"] = edge.probability
|
||||||
|
|
||||||
edges.append(e)
|
edges.append(e)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -99,11 +99,15 @@ class Command(BaseCommand):
|
|||||||
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
new_license.image_link = f"https://licensebuttons.net/l/{cc_string}/4.0/88x31.png"
|
||||||
new_license.save()
|
new_license.save()
|
||||||
|
|
||||||
def import_package(self, data, owner):
|
def import_package(self, data, owner, all_envipath_user_group):
|
||||||
return PackageManager.import_legacy_package(
|
p = PackageManager.import_legacy_package(
|
||||||
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
|
data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
PackageManager.grant_read(owner, p, all_envipath_user_group)
|
||||||
|
|
||||||
|
return p
|
||||||
|
|
||||||
def create_default_setting(self, owner, packages):
|
def create_default_setting(self, owner, packages):
|
||||||
s = SettingManager.create_setting(
|
s = SettingManager.create_setting(
|
||||||
owner,
|
owner,
|
||||||
@ -198,7 +202,7 @@ class Command(BaseCommand):
|
|||||||
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
|
s.BASE_DIR / "fixtures" / "packages" / "2025-07-18" / p, encoding="utf-8"
|
||||||
).read()
|
).read()
|
||||||
)
|
)
|
||||||
imported_package = self.import_package(package_data, admin)
|
imported_package = self.import_package(package_data, admin, g)
|
||||||
mapping[p.replace(".json", "")] = imported_package
|
mapping[p.replace(".json", "")] = imported_package
|
||||||
|
|
||||||
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
setting = self.create_default_setting(admin, [mapping["EAWAG-BBD"]])
|
||||||
|
|||||||
150
epdb/migrations/0027_alter_compound_aliases_and_more.py
Normal file
150
epdb/migrations/0027_alter_compound_aliases_and_more.py
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
# Generated by Django 6.0.3 on 2026-07-01 20:59
|
||||||
|
|
||||||
|
import django.contrib.postgres.fields
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("epdb", "0026_auto_20260602_1718"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compound",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compound",
|
||||||
|
name="default_structure",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.SET_NULL,
|
||||||
|
related_name="compound_default_structure",
|
||||||
|
to="epdb.compoundstructure",
|
||||||
|
verbose_name="Default Structure",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compound",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compoundstructure",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="compoundstructure",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="edge",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="edge",
|
||||||
|
name="edge_label",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="epdb.reaction",
|
||||||
|
verbose_name="Edge label",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="edge",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="node",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="node",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="pathway",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="pathway",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="reaction",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="reaction",
|
||||||
|
name="medline_references",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(),
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
verbose_name="Medline References",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="reaction",
|
||||||
|
name="rules",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, related_name="reaction_rule", to="epdb.rule", verbose_name="Rule"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="reaction",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rule",
|
||||||
|
name="aliases",
|
||||||
|
field=django.contrib.postgres.fields.ArrayField(
|
||||||
|
base_field=models.TextField(), blank=True, default=list, verbose_name="Aliases"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name="rule",
|
||||||
|
name="scenarios",
|
||||||
|
field=models.ManyToManyField(
|
||||||
|
blank=True, to="epdb.scenario", verbose_name="Attached Scenarios"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
242
epdb/models.py
242
epdb/models.py
@ -31,7 +31,10 @@ from sklearn.model_selection import ShuffleSplit
|
|||||||
|
|
||||||
from bridge.contracts import Property
|
from bridge.contracts import Property
|
||||||
from bridge.dto import RunResult, PropertyPrediction
|
from bridge.dto import RunResult, PropertyPrediction
|
||||||
from epdb.exceptions import InvalidSMILESException
|
from epdb.exceptions import (
|
||||||
|
InvalidMolfileException,
|
||||||
|
InvalidSMILESException,
|
||||||
|
)
|
||||||
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
from utilities.chem import FormatConverter, IndigoUtils, PredictionResult, ProductSet
|
||||||
from utilities.ml import (
|
from utilities.ml import (
|
||||||
ApplicabilityDomainPCA,
|
ApplicabilityDomainPCA,
|
||||||
@ -637,7 +640,7 @@ class EnviPathModel(TimeStampedModel):
|
|||||||
|
|
||||||
class AliasMixin(models.Model):
|
class AliasMixin(models.Model):
|
||||||
aliases = ArrayField(
|
aliases = ArrayField(
|
||||||
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list
|
models.TextField(blank=False, null=False), verbose_name="Aliases", default=list, blank=True
|
||||||
)
|
)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
@ -660,7 +663,9 @@ class AliasMixin(models.Model):
|
|||||||
|
|
||||||
|
|
||||||
class ScenarioMixin(models.Model):
|
class ScenarioMixin(models.Model):
|
||||||
scenarios = models.ManyToManyField("epdb.Scenario", verbose_name="Attached Scenarios")
|
scenarios = models.ManyToManyField(
|
||||||
|
"epdb.Scenario", verbose_name="Attached Scenarios", blank=True
|
||||||
|
)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def set_scenarios(self, scenarios: List["Scenario"]):
|
def set_scenarios(self, scenarios: List["Scenario"]):
|
||||||
@ -792,6 +797,13 @@ class Compound(
|
|||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
|
|
||||||
|
def get_structure_by_smiles(self, smiles: str) -> "CompoundStructure":
|
||||||
|
for struct in self.structures.all():
|
||||||
|
if struct.smiles == smiles:
|
||||||
|
return struct
|
||||||
|
|
||||||
|
raise ValueError(f"No structure with SMILES {smiles} found for {self.get_name()}")
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def structures(self) -> QuerySet:
|
def structures(self) -> QuerySet:
|
||||||
return CompoundStructure.objects.filter(compound=self)
|
return CompoundStructure.objects.filter(compound=self)
|
||||||
@ -860,8 +872,24 @@ class Compound(
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
package: "Package", smiles: str, name: str = None, description: str = None, molfile: str = None, *args, **kwargs
|
package: "Package",
|
||||||
|
smiles: str,
|
||||||
|
molfile: str | None = None,
|
||||||
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
) -> "Compound":
|
) -> "Compound":
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
if smiles is None or smiles.strip() == "":
|
if smiles is None or smiles.strip() == "":
|
||||||
raise InvalidSMILESException("SMILES is required")
|
raise InvalidSMILESException("SMILES is required")
|
||||||
|
|
||||||
@ -894,19 +922,19 @@ class Compound(
|
|||||||
|
|
||||||
return found_compound
|
return found_compound
|
||||||
|
|
||||||
|
|
||||||
qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package)
|
qs = CompoundStructure.objects.filter(smiles=standardized_smiles, compound__package=package)
|
||||||
if subclasses:
|
if subclasses:
|
||||||
qs = qs.not_instance_of(*subclasses)
|
qs = qs.not_instance_of(*subclasses)
|
||||||
|
|
||||||
# Check if we can find the standardized one
|
# Check if we can find the standardized one
|
||||||
if qs.exists():
|
if qs.exists():
|
||||||
# TODO should we add a structure?
|
|
||||||
found_structure = qs.first()
|
found_structure = qs.first()
|
||||||
found_compound = found_structure.compound
|
found_compound = found_structure.compound
|
||||||
|
|
||||||
|
# We've only found the normalized one, create the very structure
|
||||||
|
new_structure = found_compound.add_structure(smiles, molfile=molfile, name=name, description=description)
|
||||||
|
|
||||||
if name:
|
if name:
|
||||||
found_structure.add_alias(name)
|
|
||||||
found_compound.add_alias(name)
|
found_compound.add_alias(name)
|
||||||
|
|
||||||
return found_compound
|
return found_compound
|
||||||
@ -938,7 +966,12 @@ class Compound(
|
|||||||
)
|
)
|
||||||
|
|
||||||
cs = CompoundStructure.create(
|
cs = CompoundStructure.create(
|
||||||
c, smiles, name=name, description=description, normalized_structure=is_standardized
|
c,
|
||||||
|
smiles,
|
||||||
|
molfile=molfile,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
normalized_structure=is_standardized,
|
||||||
)
|
)
|
||||||
|
|
||||||
c.default_structure = cs
|
c.default_structure = cs
|
||||||
@ -951,11 +984,22 @@ class Compound(
|
|||||||
self,
|
self,
|
||||||
smiles: str,
|
smiles: str,
|
||||||
name: str = None,
|
name: str = None,
|
||||||
|
molfile: str = None,
|
||||||
description: str = None,
|
description: str = None,
|
||||||
default_structure: bool = False,
|
default_structure: bool = False,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> "CompoundStructure":
|
) -> "CompoundStructure":
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
if smiles is None or smiles == "":
|
if smiles is None or smiles == "":
|
||||||
raise ValueError("SMILES is required")
|
raise ValueError("SMILES is required")
|
||||||
|
|
||||||
@ -975,16 +1019,28 @@ class Compound(
|
|||||||
)
|
)
|
||||||
|
|
||||||
if is_standardized:
|
if is_standardized:
|
||||||
CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
CompoundStructure.objects.get(smiles=smiles, compound__package=self.package)
|
||||||
|
|
||||||
# Check if we find a direct match for a given SMILES and/or its standardized SMILES
|
# Check if we find a direct match for a given SMILES and/or its standardized SMILES
|
||||||
if CompoundStructure.objects.filter(
|
if CompoundStructure.objects.filter(smiles=smiles, compound__package=self.package).exists():
|
||||||
smiles__in=smiles, compound__package=self.package
|
found_cs = CompoundStructure.objects.get(smiles=smiles, compound__package=self.package)
|
||||||
).exists():
|
|
||||||
return CompoundStructure.objects.get(smiles__in=smiles, compound__package=self.package)
|
if found_cs.molfile is None and (molfile is not None and molfile.strip() != ""):
|
||||||
|
logger.info(
|
||||||
|
f"Setting molfile for found CompoundStructure(uuid={found_cs.uuid}) as it was empty"
|
||||||
|
)
|
||||||
|
found_cs.molfile = molfile
|
||||||
|
found_cs.save()
|
||||||
|
|
||||||
|
return found_cs
|
||||||
|
|
||||||
cs = CompoundStructure.create(
|
cs = CompoundStructure.create(
|
||||||
self, smiles, name=name, description=description, normalized_structure=is_standardized
|
self,
|
||||||
|
smiles,
|
||||||
|
name=name,
|
||||||
|
molfile=molfile,
|
||||||
|
description=description,
|
||||||
|
normalized_structure=is_standardized,
|
||||||
)
|
)
|
||||||
|
|
||||||
if default_structure:
|
if default_structure:
|
||||||
@ -1165,8 +1221,24 @@ class CompoundStructure(
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def create(
|
def create(
|
||||||
compound: Compound, smiles: str, name: str = None, description: str = None, molfile: str = None, *args, **kwargs
|
compound: Compound,
|
||||||
|
smiles: str,
|
||||||
|
molfile: str = None,
|
||||||
|
name: str = None,
|
||||||
|
description: str = None,
|
||||||
|
*args,
|
||||||
|
**kwargs,
|
||||||
):
|
):
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
# Clean for potential XSS
|
# Clean for potential XSS
|
||||||
if name is not None:
|
if name is not None:
|
||||||
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
name = nh3.clean(name, tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
@ -1177,6 +1249,13 @@ class CompoundStructure(
|
|||||||
if name:
|
if name:
|
||||||
found_cs.add_alias(name)
|
found_cs.add_alias(name)
|
||||||
|
|
||||||
|
if found_cs.molfile is None and (molfile is not None and molfile.strip() != ""):
|
||||||
|
logger.info(
|
||||||
|
f"Setting molfile for found CompoundStructure(uuid={found_cs.uuid}) as it was empty"
|
||||||
|
)
|
||||||
|
found_cs.molfile = molfile
|
||||||
|
found_cs.save()
|
||||||
|
|
||||||
return found_cs
|
return found_cs
|
||||||
|
|
||||||
if compound.pk is None:
|
if compound.pk is None:
|
||||||
@ -1194,7 +1273,7 @@ class CompoundStructure(
|
|||||||
cs.compound = compound
|
cs.compound = compound
|
||||||
|
|
||||||
# Check if molfile is present and valid
|
# Check if molfile is present and valid
|
||||||
if molfile is not None and FormatConverter.from_molfile(molfile) is not None:
|
if molfile is not None and molfile.strip() != "":
|
||||||
cs.molfile = molfile
|
cs.molfile = molfile
|
||||||
|
|
||||||
if "normalized_structure" in kwargs:
|
if "normalized_structure" in kwargs:
|
||||||
@ -1658,10 +1737,15 @@ class Reaction(
|
|||||||
products = models.ManyToManyField(
|
products = models.ManyToManyField(
|
||||||
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
|
"epdb.CompoundStructure", verbose_name="Products", related_name="reaction_products"
|
||||||
)
|
)
|
||||||
rules = models.ManyToManyField("epdb.Rule", verbose_name="Rule", related_name="reaction_rule")
|
rules = models.ManyToManyField(
|
||||||
|
"epdb.Rule", verbose_name="Rule", related_name="reaction_rule", blank=True
|
||||||
|
)
|
||||||
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
multi_step = models.BooleanField(verbose_name="Multistep Reaction")
|
||||||
medline_references = ArrayField(
|
medline_references = ArrayField(
|
||||||
models.TextField(blank=False, null=False), null=True, verbose_name="Medline References"
|
models.TextField(blank=False, null=False),
|
||||||
|
null=True,
|
||||||
|
verbose_name="Medline References",
|
||||||
|
blank=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
external_identifiers = GenericRelation("ExternalIdentifier")
|
external_identifiers = GenericRelation("ExternalIdentifier")
|
||||||
@ -1750,7 +1834,7 @@ class Reaction(
|
|||||||
r = Reaction()
|
r = Reaction()
|
||||||
r.package = package
|
r.package = package
|
||||||
|
|
||||||
if r is not None:
|
if name is not None:
|
||||||
r.name = name
|
r.name = name
|
||||||
|
|
||||||
if description is not None and description.strip() != "":
|
if description is not None and description.strip() != "":
|
||||||
@ -1987,6 +2071,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
# add links start -> pseudo
|
# add links start -> pseudo
|
||||||
new_link = {
|
new_link = {
|
||||||
"name": link["name"],
|
"name": link["name"],
|
||||||
|
"plain_name": link["plain_name"],
|
||||||
"id": link["id"],
|
"id": link["id"],
|
||||||
"url": link["url"],
|
"url": link["url"],
|
||||||
"image": link["image"],
|
"image": link["image"],
|
||||||
@ -1996,6 +2081,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
"source": node_url_to_idx[link["start_node_urls"][0]],
|
"source": node_url_to_idx[link["start_node_urls"][0]],
|
||||||
"target": pseudo_idx,
|
"target": pseudo_idx,
|
||||||
"app_domain": link.get("app_domain", None),
|
"app_domain": link.get("app_domain", None),
|
||||||
|
"to_pseudo": True,
|
||||||
}
|
}
|
||||||
adjusted_links.append(new_link)
|
adjusted_links.append(new_link)
|
||||||
|
|
||||||
@ -2003,6 +2089,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
for target in link["end_node_urls"]:
|
for target in link["end_node_urls"]:
|
||||||
new_link = {
|
new_link = {
|
||||||
"name": link["name"],
|
"name": link["name"],
|
||||||
|
"plain_name": link["plain_name"],
|
||||||
"id": link["id"],
|
"id": link["id"],
|
||||||
"url": link["url"],
|
"url": link["url"],
|
||||||
"image": link["image"],
|
"image": link["image"],
|
||||||
@ -2013,6 +2100,7 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
"target": node_url_to_idx[target],
|
"target": node_url_to_idx[target],
|
||||||
"app_domain": link.get("app_domain", None),
|
"app_domain": link.get("app_domain", None),
|
||||||
"multi_step": link["multi_step"],
|
"multi_step": link["multi_step"],
|
||||||
|
"from_pseudo": True,
|
||||||
}
|
}
|
||||||
adjusted_links.append(new_link)
|
adjusted_links.append(new_link)
|
||||||
|
|
||||||
@ -2222,11 +2310,12 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
def add_node(
|
def add_node(
|
||||||
self,
|
self,
|
||||||
smiles: str,
|
smiles: str,
|
||||||
name: Optional[str] = None,
|
molfile: str | None = None,
|
||||||
description: Optional[str] = None,
|
name: str | None = None,
|
||||||
depth: Optional[int] = -1,
|
description: str | None = None,
|
||||||
|
depth: int = -1,
|
||||||
):
|
):
|
||||||
return Node.create(self, smiles, depth, name=name, description=description)
|
return Node.create(self, smiles, depth, molfile=molfile, name=name, description=description)
|
||||||
|
|
||||||
@transaction.atomic
|
@transaction.atomic
|
||||||
def add_edge(
|
def add_edge(
|
||||||
@ -2255,10 +2344,12 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMix
|
|||||||
depth_map[0] = list()
|
depth_map[0] = list()
|
||||||
processed = set()
|
processed = set()
|
||||||
|
|
||||||
data_driven_root_nodes = self.node_set.all().annotate(
|
data_driven_root_nodes = (
|
||||||
prod_cnt=Count('edge_products'),
|
self.node_set.all()
|
||||||
educt_cnt=Count('edge_educts')
|
.annotate(prod_cnt=Count("edge_products"), educt_cnt=Count("edge_educts"))
|
||||||
).filter(prod_cnt=0, educt_cnt__gt=0).distinct()
|
.filter(prod_cnt=0, educt_cnt__gt=0)
|
||||||
|
.distinct()
|
||||||
|
)
|
||||||
|
|
||||||
# Eval QuerySet
|
# Eval QuerySet
|
||||||
root_nodes_by_depth = list(self.root_nodes)
|
root_nodes_by_depth = list(self.root_nodes)
|
||||||
@ -2320,17 +2411,19 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
def _url(self):
|
def _url(self):
|
||||||
return "{}/node/{}".format(self.pathway.url, self.uuid)
|
return "{}/node/{}".format(self.pathway.url, self.uuid)
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self, include_suffix=True):
|
||||||
non_generic_name = True
|
non_generic_name = True
|
||||||
|
|
||||||
if self.name == "no name":
|
if self.name is None or self.name == "no name":
|
||||||
non_generic_name = False
|
non_generic_name = False
|
||||||
|
|
||||||
return (
|
if non_generic_name:
|
||||||
self.name
|
return self.name
|
||||||
if non_generic_name
|
else:
|
||||||
else f"{self.default_node_label.name} (taken from underlying structure)"
|
if include_suffix:
|
||||||
)
|
return f"{self.default_node_label.name} (taken from underlying structure)"
|
||||||
|
else:
|
||||||
|
return self.default_node_label.name
|
||||||
|
|
||||||
def d3_json(self):
|
def d3_json(self):
|
||||||
app_domain_data = self.get_app_domain_assessment_data()
|
app_domain_data = self.get_app_domain_assessment_data()
|
||||||
@ -2351,11 +2444,16 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
"node_label_id": self.default_node_label.url,
|
"node_label_id": self.default_node_label.url,
|
||||||
"image": f"{self.url}?image=svg",
|
"image": f"{self.url}?image=svg",
|
||||||
"image_svg": IndigoUtils.mol_to_svg(
|
"image_svg": IndigoUtils.mol_to_svg(
|
||||||
self.default_node_label.molfile if self.default_node_label.molfile is not None and self.default_node_label.molfile.strip() else self.default_node_label.smiles,
|
self.default_node_label.molfile
|
||||||
width=40, height=40
|
if self.default_node_label.molfile is not None
|
||||||
|
and self.default_node_label.molfile.strip()
|
||||||
|
else self.default_node_label.smiles,
|
||||||
|
width=40,
|
||||||
|
height=40,
|
||||||
),
|
),
|
||||||
"image_type": "svg",
|
"image_type": "svg",
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
|
"plain_name": self.get_name(include_suffix=False),
|
||||||
"smiles": self.default_node_label.smiles,
|
"smiles": self.default_node_label.smiles,
|
||||||
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
|
"scenarios": [{"name": s.get_name(), "url": s.url} for s in self.scenarios.all()],
|
||||||
"app_domain": {
|
"app_domain": {
|
||||||
@ -2366,6 +2464,7 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
},
|
},
|
||||||
"predicted_properties": predicted_properties,
|
"predicted_properties": predicted_properties,
|
||||||
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
"is_engineered_intermediate": self.kv.get("is_engineered_intermediate", False),
|
||||||
|
"proposed": self.is_proposed_intermediate(),
|
||||||
"timeseries": self.get_timeseries_data(),
|
"timeseries": self.get_timeseries_data(),
|
||||||
**structure_data,
|
**structure_data,
|
||||||
}
|
}
|
||||||
@ -2378,35 +2477,53 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
pathway: "Pathway",
|
pathway: "Pathway",
|
||||||
smiles: str,
|
smiles: str,
|
||||||
depth: int,
|
depth: int,
|
||||||
name: Optional[str] = None,
|
molfile: str | None = None,
|
||||||
description: Optional[str] = None,
|
name: str | None = None,
|
||||||
|
description: str | None = None,
|
||||||
):
|
):
|
||||||
|
# Molfile has precendence over SMILES
|
||||||
|
if molfile is not None and molfile.strip() != "":
|
||||||
|
mol = FormatConverter.from_molfile(molfile)
|
||||||
|
|
||||||
|
if mol is None:
|
||||||
|
raise InvalidMolfileException("Given molfile is invalid")
|
||||||
|
else:
|
||||||
|
# Overwrite SMILES from molfile
|
||||||
|
smiles = FormatConverter.to_smiles(mol)
|
||||||
|
|
||||||
stereo_removed = False
|
stereo_removed = False
|
||||||
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
if pathway.predicted and FormatConverter.has_stereo(smiles):
|
||||||
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
smiles = FormatConverter.standardize(smiles, remove_stereo=True)
|
||||||
stereo_removed = True
|
stereo_removed = True
|
||||||
|
|
||||||
c = Compound.create(pathway.package, smiles, name=name, description=description)
|
c = Compound.create(
|
||||||
|
pathway.package, smiles, molfile=molfile, name=name, description=description
|
||||||
|
)
|
||||||
|
|
||||||
if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists():
|
structure = c.get_structure_by_smiles(smiles)
|
||||||
return Node.objects.get(pathway=pathway, default_node_label=c.default_structure)
|
|
||||||
|
if Node.objects.filter(pathway=pathway, default_node_label=structure).exists():
|
||||||
|
return Node.objects.get(pathway=pathway, default_node_label=structure)
|
||||||
|
|
||||||
n = Node()
|
n = Node()
|
||||||
n.stereo_removed = stereo_removed
|
n.stereo_removed = stereo_removed
|
||||||
n.pathway = pathway
|
n.pathway = pathway
|
||||||
n.depth = depth
|
n.depth = depth
|
||||||
|
|
||||||
n.default_node_label = c.default_structure
|
n.default_node_label = structure
|
||||||
n.save()
|
n.save()
|
||||||
|
|
||||||
n.node_labels.add(c.default_structure)
|
n.node_labels.add(structure)
|
||||||
n.save()
|
n.save()
|
||||||
|
|
||||||
return n
|
return n
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def as_svg(self):
|
def as_svg(self):
|
||||||
if self.default_node_label.molfile is not None and self.default_node_label.molfile.strip() != "":
|
if (
|
||||||
|
self.default_node_label.molfile is not None
|
||||||
|
and self.default_node_label.molfile.strip() != ""
|
||||||
|
):
|
||||||
return IndigoUtils.mol_to_svg(self.default_node_label.molfile)
|
return IndigoUtils.mol_to_svg(self.default_node_label.molfile)
|
||||||
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
return IndigoUtils.mol_to_svg(self.default_node_label.smiles)
|
||||||
|
|
||||||
@ -2436,6 +2553,28 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def is_proposed_intermediate(self):
|
||||||
|
collected = defaultdict(dict)
|
||||||
|
for ai in self.additional_information.filter(
|
||||||
|
type__in=["ProposedIntermediate", "TransformationProductImportance", "Confidence"],
|
||||||
|
scenario__isnull=False,
|
||||||
|
):
|
||||||
|
collected[str(ai.scenario.uuid)]["scenarioId"] = ai.scenario.url
|
||||||
|
collected[str(ai.scenario.uuid)]["scenarioName"] = ai.scenario.name
|
||||||
|
|
||||||
|
if ai.type == "ProposedIntermediate":
|
||||||
|
collected[str(ai.scenario.uuid)]["proposed"] = True
|
||||||
|
|
||||||
|
if ai.type == "Confidence":
|
||||||
|
collected[str(ai.scenario.uuid)]["Confidence"] = ai.get().level
|
||||||
|
|
||||||
|
if ai.type == "TransformationProductImportance":
|
||||||
|
collected[str(ai.scenario.uuid)]["Transformation product importance"] = (
|
||||||
|
ai.get().importance.value
|
||||||
|
)
|
||||||
|
|
||||||
|
return list(collected.values())
|
||||||
|
|
||||||
def simple_json(self, include_description=False):
|
def simple_json(self, include_description=False):
|
||||||
res = super().simple_json()
|
res = super().simple_json()
|
||||||
name = res.get("name", None)
|
name = res.get("name", None)
|
||||||
@ -2465,6 +2604,7 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
def d3_json(self):
|
def d3_json(self):
|
||||||
edge_json = {
|
edge_json = {
|
||||||
"name": self.get_name(),
|
"name": self.get_name(),
|
||||||
|
"plain_name": self.get_name(include_suffix=False),
|
||||||
"id": self.url,
|
"id": self.url,
|
||||||
"url": self.url,
|
"url": self.url,
|
||||||
"image": self.url + "?image=svg",
|
"image": self.url + "?image=svg",
|
||||||
@ -2579,17 +2719,19 @@ class Edge(EnviPathModel, AliasMixin, ScenarioMixin, AdditionalInformationMixin)
|
|||||||
|
|
||||||
return res
|
return res
|
||||||
|
|
||||||
def get_name(self):
|
def get_name(self, include_suffix=True):
|
||||||
non_generic_name = True
|
non_generic_name = True
|
||||||
|
|
||||||
if self.name == "no name":
|
if self.name == "no name":
|
||||||
non_generic_name = False
|
non_generic_name = False
|
||||||
|
|
||||||
return (
|
if non_generic_name:
|
||||||
self.name
|
return self.name
|
||||||
if non_generic_name
|
else:
|
||||||
else f"{self.edge_label.name} (taken from underlying reaction)"
|
if include_suffix:
|
||||||
)
|
return f"{self.edge_label.name} (taken from underlying reaction)"
|
||||||
|
else:
|
||||||
|
return self.edge_label.name
|
||||||
|
|
||||||
|
|
||||||
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
class EPModel(PolymorphicModel, EnviPathModel, AdditionalInformationMixin):
|
||||||
|
|||||||
@ -19,7 +19,7 @@ from sentry_sdk import capture_exception
|
|||||||
|
|
||||||
from utilities.chem import FormatConverter, IndigoUtils
|
from utilities.chem import FormatConverter, IndigoUtils
|
||||||
from utilities.decorators import package_permission_required
|
from utilities.decorators import package_permission_required
|
||||||
from .exceptions import InvalidSMILESException
|
from .exceptions import InvalidMolfileException, InvalidSMILESException
|
||||||
|
|
||||||
from .logic import (
|
from .logic import (
|
||||||
EPDBURLParser,
|
EPDBURLParser,
|
||||||
@ -785,6 +785,7 @@ def models(request):
|
|||||||
{"Home": s.SERVER_URL},
|
{"Home": s.SERVER_URL},
|
||||||
{"Model": s.SERVER_URL + "/model"},
|
{"Model": s.SERVER_URL + "/model"},
|
||||||
]
|
]
|
||||||
|
|
||||||
context["entity_type"] = "model"
|
context["entity_type"] = "model"
|
||||||
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
context["api_endpoint"] = f"{s.SERVER_PATH}/api/v1/models/"
|
||||||
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
context["per_page"] = s.API_PAGINATION_DEFAULT_PAGE_SIZE
|
||||||
@ -809,7 +810,7 @@ def models(request):
|
|||||||
"type": "enviformer",
|
"type": "enviformer",
|
||||||
"requires_rule_packages": False,
|
"requires_rule_packages": False,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
}
|
||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
@ -942,13 +943,12 @@ def package_models(request, package_uuid):
|
|||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if s.ENVIFORMER_PRESENT:
|
if s.ENVIFORMER_PRESENT:
|
||||||
context["model_types"]["EnviFormer"] = {
|
context["model_types"]["EnviFormer"] = {
|
||||||
"type": "enviformer",
|
"type": "enviformer",
|
||||||
"requires_rule_packages": False,
|
"requires_rule_packages": False,
|
||||||
"requires_data_packages": True,
|
"requires_data_packages": True,
|
||||||
},
|
}
|
||||||
|
|
||||||
if s.FLAGS.get("PLUGINS", False):
|
if s.FLAGS.get("PLUGINS", False):
|
||||||
for k, v in s.CLASSIFIER_PLUGINS.items():
|
for k, v in s.CLASSIFIER_PLUGINS.items():
|
||||||
@ -1124,8 +1124,10 @@ def package_model(request, package_uuid, model_uuid):
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
# Sort data by prob desc
|
# Sort data by prob desc
|
||||||
res["pred"] = sorted(res["pred"], key=lambda x: x["probability"], reverse=True)
|
res["pred"] = sorted(
|
||||||
|
res["pred"], key=lambda x: x["probability"], reverse=True
|
||||||
|
)
|
||||||
|
|
||||||
return JsonResponse(res, safe=False)
|
return JsonResponse(res, safe=False)
|
||||||
|
|
||||||
@ -1230,9 +1232,7 @@ def package(request, package_uuid):
|
|||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
if request.GET.get("export", False) == "true":
|
if request.GET.get("export", False) == "true":
|
||||||
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
|
filename = f"{current_package.get_name().replace(' ', '_')}_{current_package.uuid}.json"
|
||||||
pack_json = PackageManager.export_package(
|
pack_json = PackageManager.export_package(current_package)
|
||||||
current_package, include_models=False, include_external_identifiers=False
|
|
||||||
)
|
|
||||||
response = JsonResponse(pack_json, content_type="application/json")
|
response = JsonResponse(pack_json, content_type="application/json")
|
||||||
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
response["Content-Disposition"] = f'attachment; filename="{filename}"'
|
||||||
|
|
||||||
@ -1415,12 +1415,18 @@ def package_compounds(request, package_uuid):
|
|||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
compound_name = request.POST.get("compound-name")
|
compound_name = request.POST.get("compound-name")
|
||||||
compound_smiles = request.POST.get("compound-smiles")
|
compound_smiles = request.POST.get("compound-smiles")
|
||||||
|
compound_molfile = request.POST.get("compound-molfile")
|
||||||
compound_description = request.POST.get("compound-description")
|
compound_description = request.POST.get("compound-description")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
current_package, compound_smiles, compound_name, compound_description
|
current_package,
|
||||||
|
compound_smiles,
|
||||||
|
molfile=compound_molfile,
|
||||||
|
name=compound_name,
|
||||||
|
description=compound_description,
|
||||||
)
|
)
|
||||||
except ValueError as e:
|
except (InvalidSMILESException, InvalidMolfileException) as e:
|
||||||
raise BadRequest(str(e))
|
raise BadRequest(str(e))
|
||||||
|
|
||||||
return redirect(c.url)
|
return redirect(c.url)
|
||||||
@ -1546,11 +1552,15 @@ def package_compound_structures(request, package_uuid, compound_uuid):
|
|||||||
elif request.method == "POST":
|
elif request.method == "POST":
|
||||||
structure_name = request.POST.get("structure-name")
|
structure_name = request.POST.get("structure-name")
|
||||||
structure_smiles = request.POST.get("structure-smiles")
|
structure_smiles = request.POST.get("structure-smiles")
|
||||||
|
structure_molfile = request.POST.get("structure-molfile")
|
||||||
structure_description = request.POST.get("structure-description")
|
structure_description = request.POST.get("structure-description")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
cs = current_compound.add_structure(
|
cs = current_compound.add_structure(
|
||||||
structure_smiles, structure_name, structure_description
|
structure_smiles,
|
||||||
|
molfile=structure_molfile,
|
||||||
|
name=structure_name,
|
||||||
|
description=structure_description,
|
||||||
)
|
)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return error(
|
return error(
|
||||||
@ -2121,12 +2131,14 @@ def package_pathways(request, package_uuid):
|
|||||||
else:
|
else:
|
||||||
prediction_setting = current_user.prediction_settings()
|
prediction_setting = current_user.prediction_settings()
|
||||||
|
|
||||||
|
is_predict_mode = pw_mode in {"predict", "incremental"}
|
||||||
|
|
||||||
pw = Pathway.create(
|
pw = Pathway.create(
|
||||||
current_package,
|
current_package,
|
||||||
stand_smiles,
|
stand_smiles if is_predict_mode else smiles,
|
||||||
name=name,
|
name=name,
|
||||||
description=description,
|
description=description,
|
||||||
predicted=pw_mode in {"predict", "incremental"},
|
predicted=is_predict_mode,
|
||||||
)
|
)
|
||||||
|
|
||||||
# set mode
|
# set mode
|
||||||
@ -2368,10 +2380,16 @@ def package_pathway_nodes(request, package_uuid, pathway_uuid):
|
|||||||
node_description = request.POST.get("node-description")
|
node_description = request.POST.get("node-description")
|
||||||
|
|
||||||
node_smiles = request.POST.get("node-smiles").strip()
|
node_smiles = request.POST.get("node-smiles").strip()
|
||||||
|
node_molfile = request.POST.get("node-molfile").strip()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
current_pathway.add_node(node_smiles, name=node_name, description=node_description)
|
current_pathway.add_node(
|
||||||
except InvalidSMILESException as e:
|
node_smiles, molfile=node_molfile, name=node_name, description=node_description
|
||||||
return error(request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid")
|
)
|
||||||
|
except InvalidSMILESException:
|
||||||
|
return error(
|
||||||
|
request, "Node creation failed!", f"Given SMILES ({node_smiles}) is invalid"
|
||||||
|
)
|
||||||
|
|
||||||
return redirect(current_pathway.url)
|
return redirect(current_pathway.url)
|
||||||
|
|
||||||
@ -2479,7 +2497,26 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid):
|
|||||||
|
|
||||||
return JsonResponse({"success": current_node.url})
|
return JsonResponse({"success": current_node.url})
|
||||||
|
|
||||||
return HttpResponseBadRequest()
|
new_node_name = request.POST.get("node-name")
|
||||||
|
new_node_description = request.POST.get("node-description")
|
||||||
|
|
||||||
|
if any([new_node_name, new_node_description]):
|
||||||
|
if new_node_name is not None and new_node_name.strip() != "":
|
||||||
|
new_node_name = nh3.clean(new_node_name.strip(), tags=s.ALLOWED_HTML_TAGS).strip()
|
||||||
|
current_node.name = new_node_name
|
||||||
|
|
||||||
|
if new_node_description is not None and new_node_description.strip() != "":
|
||||||
|
new_node_description = nh3.clean(
|
||||||
|
new_node_description.strip(), tags=s.ALLOWED_HTML_TAGS
|
||||||
|
).strip()
|
||||||
|
current_node.description = new_node_description
|
||||||
|
|
||||||
|
current_node.save()
|
||||||
|
|
||||||
|
return redirect(current_node.url)
|
||||||
|
|
||||||
|
return error(request, "Node update failed!", "No changes were made to the node")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return HttpResponseNotAllowed(["GET", "POST"])
|
return HttpResponseNotAllowed(["GET", "POST"])
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -81,6 +81,7 @@ def migration(request):
|
|||||||
"bt0416-4269",
|
"bt0416-4269",
|
||||||
"bt0432-4254",
|
"bt0432-4254",
|
||||||
"bt0337-4117",
|
"bt0337-4117",
|
||||||
|
"bt0322-3393",
|
||||||
]
|
]
|
||||||
|
|
||||||
if request.method == "GET":
|
if request.method == "GET":
|
||||||
|
|||||||
@ -39,3 +39,7 @@ select.select[multiple] {
|
|||||||
display: block;
|
display: block;
|
||||||
white-space: normal;
|
white-space: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
p a {
|
||||||
|
@apply underline;
|
||||||
|
}
|
||||||
|
|||||||
@ -5,6 +5,126 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
document.addEventListener('alpine:init', () => {
|
||||||
|
|
||||||
|
const basePagination = (
|
||||||
|
items,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
perPage,
|
||||||
|
isReviewed,
|
||||||
|
instanceId
|
||||||
|
) => ({
|
||||||
|
items,
|
||||||
|
currentPage,
|
||||||
|
totalPages,
|
||||||
|
totalItems,
|
||||||
|
perPage,
|
||||||
|
isReviewed,
|
||||||
|
instanceId,
|
||||||
|
|
||||||
|
get paginatedItems() {
|
||||||
|
return this.items;
|
||||||
|
},
|
||||||
|
|
||||||
|
get showingStart() {
|
||||||
|
if (this.totalItems === 0) return 0;
|
||||||
|
return (this.currentPage - 1) * this.perPage + 1;
|
||||||
|
},
|
||||||
|
|
||||||
|
get showingEnd() {
|
||||||
|
if (this.totalItems === 0) return 0;
|
||||||
|
return Math.min((this.currentPage - 1) * this.perPage + this.items.length, this.totalItems);
|
||||||
|
},
|
||||||
|
|
||||||
|
nextPage() {
|
||||||
|
if (this.currentPage < this.totalPages) {
|
||||||
|
this.fetchPage(this.currentPage + 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
prevPage() {
|
||||||
|
if (this.currentPage > 1) {
|
||||||
|
this.fetchPage(this.currentPage - 1);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
goToPage(page) {
|
||||||
|
if (page >= 1 && page <= this.totalPages) {
|
||||||
|
this.fetchPage(page);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
get pageNumbers() {
|
||||||
|
const pages = [];
|
||||||
|
const total = this.totalPages;
|
||||||
|
const current = this.currentPage;
|
||||||
|
|
||||||
|
if (total === 0) {
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (total <= 7) {
|
||||||
|
for (let i = 1; i <= total; i++) {
|
||||||
|
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
pages.push({ page: 1, isEllipsis: false, key: `${this.instanceId}-page-1` });
|
||||||
|
|
||||||
|
let rangeStart;
|
||||||
|
let rangeEnd;
|
||||||
|
|
||||||
|
if (current <= 4) {
|
||||||
|
rangeStart = 2;
|
||||||
|
rangeEnd = 5;
|
||||||
|
} else if (current >= total - 3) {
|
||||||
|
rangeStart = total - 4;
|
||||||
|
rangeEnd = total - 1;
|
||||||
|
} else {
|
||||||
|
rangeStart = current - 1;
|
||||||
|
rangeEnd = current + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeStart > 2) {
|
||||||
|
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-start` });
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = rangeStart; i <= rangeEnd; i++) {
|
||||||
|
pages.push({ page: i, isEllipsis: false, key: `${this.instanceId}-page-${i}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rangeEnd < total - 1) {
|
||||||
|
pages.push({ page: '...', isEllipsis: true, key: `${this.instanceId}-ellipsis-end` });
|
||||||
|
}
|
||||||
|
|
||||||
|
pages.push({ page: total, isEllipsis: false, key: `${this.instanceId}-page-${total}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return pages;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Alpine.data('paginatedList',
|
||||||
|
(items, options = {}) => ({
|
||||||
|
...basePagination(items,1, 0, 0, options.perPage || 50, options.isReviewed || false, options.instanceId || Math.random().toString(36).substring(2, 9),),
|
||||||
|
|
||||||
|
init() {
|
||||||
|
this.fetchPage(1);
|
||||||
|
},
|
||||||
|
|
||||||
|
async fetchPage(page) {
|
||||||
|
this.totalItems = this.items.length;
|
||||||
|
this.totalPages = Math.ceil(this.items.length / this.perPage);
|
||||||
|
this.currentPage = page
|
||||||
|
|
||||||
|
const start = page * this.perPage - this.perPage;
|
||||||
|
const end = page * this.perPage;
|
||||||
|
return this.items.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
Alpine.data('remotePaginatedList', (options = {}) => ({
|
Alpine.data('remotePaginatedList', (options = {}) => ({
|
||||||
items: [],
|
items: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
|
|||||||
245
static/js/pw.js
245
static/js/pw.js
@ -1,18 +1,20 @@
|
|||||||
console.log("loaded pw.js")
|
console.log("loaded pw.js")
|
||||||
|
|
||||||
|
function findPaths(source_idx, links) {
|
||||||
function findPaths(source_idx, target_idx, links) {
|
|
||||||
const resultLinks = new Set();
|
const resultLinks = new Set();
|
||||||
const visited = new Set();
|
const visited = new Set();
|
||||||
|
|
||||||
// Helper function for depth-first search
|
// Helper function for depth-first search
|
||||||
function dfs(current) {
|
function dfs(current) {
|
||||||
if (current === target_idx) {
|
|
||||||
|
if (visited.has(current)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (links.filter(link => link.target.id === current).length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
visited.add(current);
|
visited.add(current);
|
||||||
|
|
||||||
links.forEach(link => {
|
links.forEach(link => {
|
||||||
if (
|
if (
|
||||||
link.target.id === current &&
|
link.target.id === current &&
|
||||||
@ -23,18 +25,13 @@ function findPaths(source_idx, target_idx, links) {
|
|||||||
dfs(link.source.id);
|
dfs(link.source.id);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
visited.delete(current);
|
visited.delete(current);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start DFS
|
// Start DFS
|
||||||
dfs(source_idx);
|
dfs(source_idx);
|
||||||
|
return Array.from(resultLinks);
|
||||||
return Array.from(resultLinks); // Convert Set to Array for result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function predictFromNode(url) {
|
function predictFromNode(url) {
|
||||||
fetch("", {
|
fetch("", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@ -70,6 +67,9 @@ function draw(pathway, elem) {
|
|||||||
const horizontalSpacing = 75; // horizontal space between nodes
|
const horizontalSpacing = 75; // horizontal space between nodes
|
||||||
const depthMap = new Map();
|
const depthMap = new Map();
|
||||||
|
|
||||||
|
// Avoid leaving unconnected Nodes leaving the viewport
|
||||||
|
nodes.forEach(node => {if (node.depth < 0) node.depth = 0;});
|
||||||
|
|
||||||
// Sort nodes by depth first to minimize crossings
|
// Sort nodes by depth first to minimize crossings
|
||||||
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
const sortedNodes = [...nodes].sort((a, b) => a.depth - b.depth);
|
||||||
|
|
||||||
@ -157,6 +157,11 @@ function draw(pathway, elem) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
node.attr("transform", d => `translate(${d.x},${d.y})`);
|
||||||
|
|
||||||
|
linkText
|
||||||
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function dragstarted(event, d) {
|
function dragstarted(event, d) {
|
||||||
@ -259,7 +264,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Wait before showing popup (ms)
|
// Wait before showing popup (ms)
|
||||||
var popupWaitBeforeShow = 1000;
|
var popupWaitBeforeShow = 500;
|
||||||
|
|
||||||
// Custom popover element
|
// Custom popover element
|
||||||
let popoverTimeout = null;
|
let popoverTimeout = null;
|
||||||
@ -491,7 +496,7 @@ function draw(pathway, elem) {
|
|||||||
if (n.stereo_removed) {
|
if (n.stereo_removed) {
|
||||||
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
popupContent += "<span class='alert alert-warning alert-soft'>Removed stereochemistry for prediction</span>";
|
||||||
}
|
}
|
||||||
popupContent += "<a href='" + n.url + "'>" + n.name + "</a><br>";
|
popupContent += "<a href='" + n.url + "'>" + n.plain_name + "</a><br>";
|
||||||
popupContent += "Depth " + n.depth + "<br>"
|
popupContent += "Depth " + n.depth + "<br>"
|
||||||
|
|
||||||
if (appDomainViewEnabled) {
|
if (appDomainViewEnabled) {
|
||||||
@ -531,6 +536,21 @@ function draw(pathway, elem) {
|
|||||||
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
popupContent += '<b>Half-lives and related scenarios:</b><br>'
|
||||||
for (var s of n.scenarios) {
|
for (var s of n.scenarios) {
|
||||||
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
popupContent += "<a href='" + s.url + "'>" + s.name + "</a><br>";
|
||||||
|
for (var prop in n.proposed) {
|
||||||
|
if (n.proposed[prop].scenarioId === s.url) {
|
||||||
|
if("proposed" in n.proposed[prop]){
|
||||||
|
popupContent += "This compound is a proposed intermediate" + "<br>";
|
||||||
|
}
|
||||||
|
if("Confidence" in n.proposed[prop]){
|
||||||
|
popupContent += "Confidence: " + n.proposed[prop]["Confidence"] + "<br>";
|
||||||
|
}
|
||||||
|
if("Transformation product importance" in n.proposed[prop]){
|
||||||
|
popupContent += "Transformation product importance: " + n.proposed[prop]["Transformation product importance"] + "<br>";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
popupContent += "<br>";
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -543,7 +563,7 @@ function draw(pathway, elem) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function edge_popup(e) {
|
function edge_popup(e) {
|
||||||
popupContent = "<a href='" + e.url + "'>" + e.name + "</a><br><br>";
|
popupContent = "<a href='" + e.url + "'>" + e.plain_name + "</a><br><br>";
|
||||||
|
|
||||||
if (e.reaction.rules) {
|
if (e.reaction.rules) {
|
||||||
for (var rule of e.reaction.rules) {
|
for (var rule of e.reaction.rules) {
|
||||||
@ -594,7 +614,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
// Add background rectangle FIRST to enable pan/zoom on empty space
|
// Add background rectangle FIRST to enable pan/zoom on empty space
|
||||||
// This must be inserted before zoomable group so it's behind everything
|
// This must be inserted before zoomable group so it's behind everything
|
||||||
svg.insert("rect", "#zoomable")
|
const rect = svg.insert("rect", "#zoomable")
|
||||||
.attr("x", 0)
|
.attr("x", 0)
|
||||||
.attr("y", 0)
|
.attr("y", 0)
|
||||||
.attr("width", width)
|
.attr("width", width)
|
||||||
@ -603,6 +623,30 @@ function draw(pathway, elem) {
|
|||||||
.attr("pointer-events", "all")
|
.attr("pointer-events", "all")
|
||||||
.style("cursor", "grab");
|
.style("cursor", "grab");
|
||||||
|
|
||||||
|
// Click on empty SVG space (not on a node or link)
|
||||||
|
rect.on("click", function(event) {
|
||||||
|
// Only treat it as a background click if the direct target is the rect itself
|
||||||
|
// (clicks on nodes/links bubble up but originate from different elements)
|
||||||
|
const target = event.target;
|
||||||
|
const isNode = target.closest && (
|
||||||
|
target.closest("circle") !== null ||
|
||||||
|
target.closest("image") !== null ||
|
||||||
|
target.closest(".node") !== null
|
||||||
|
);
|
||||||
|
const isLink = target.closest && (
|
||||||
|
target.closest("path.link") !== null ||
|
||||||
|
target.closest("path.link_no_arrow") !== null
|
||||||
|
);
|
||||||
|
if (!isNode && !isLink) {
|
||||||
|
// console.log("Clicked outside a node or link");
|
||||||
|
// Clear all highlights
|
||||||
|
d3.selectAll("circle").classed("highlighted", false);
|
||||||
|
d3.selectAll("path").classed("highlighted", false);
|
||||||
|
d3.selectAll("path").classed("inedge", false);
|
||||||
|
d3.selectAll("path").classed("outedge", false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Zoom Funktion aktivieren
|
// Zoom Funktion aktivieren
|
||||||
const zoom = d3.zoom()
|
const zoom = d3.zoom()
|
||||||
.scaleExtent([0.5, 5])
|
.scaleExtent([0.5, 5])
|
||||||
@ -649,49 +693,83 @@ function draw(pathway, elem) {
|
|||||||
.force("center", d3.forceCenter(width / 2, height / 4))
|
.force("center", d3.forceCenter(width / 2, height / 4))
|
||||||
.on("tick", ticked);
|
.on("tick", ticked);
|
||||||
|
|
||||||
// Kanten zeichnen
|
// Draw Edges
|
||||||
const link = zoomable.append("g")
|
const linkGroup = zoomable.append("g")
|
||||||
.selectAll("path")
|
.selectAll("g")
|
||||||
.data(links)
|
.data(links)
|
||||||
.enter().append("path")
|
.enter()
|
||||||
// Check if target is pseudo and draw marker only if not pseudo
|
.append("g")
|
||||||
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
.attr("class", "link-group");
|
||||||
.attr("marker-end", d => {
|
|
||||||
if (d.target.pseudo) return '';
|
const link = linkGroup
|
||||||
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
.append("path")
|
||||||
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
})
|
.attr("marker-end", d => d.target.pseudo ? "" : "url(#arrow)")
|
||||||
.attr("fill", "none")
|
.attr("fill", "none")
|
||||||
.on("click", function(event, d) {
|
|
||||||
|
// Check if target is pseudo and draw marker only if not pseudo
|
||||||
|
.attr("class", d => d.target.pseudo ? "link_no_arrow" : "link")
|
||||||
|
.attr("marker-end", d => {
|
||||||
|
if (d.target.pseudo) return '';
|
||||||
|
if (d.source.id === d.target.id) return 'url(#curve-arrow)'; // Use curve arrow for self-loops
|
||||||
|
return d.multi_step ? 'url(#doublearrow)' : 'url(#arrow)';
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.attr("fill", "none")
|
||||||
|
.on("click", function(event, d) {
|
||||||
const wasHighlighted = d3.select(this).classed("highlighted");
|
const wasHighlighted = d3.select(this).classed("highlighted");
|
||||||
|
d3.selectAll("path").classed("highlighted", false);
|
||||||
d3.selectAll("path").classed("highlighted", false);
|
|
||||||
|
|
||||||
if (!wasHighlighted) {
|
if (!wasHighlighted) {
|
||||||
const toHighlight = [];
|
const toHighlight = [];
|
||||||
toHighlight.push(d.el);
|
toHighlight.push(d.el);
|
||||||
|
|
||||||
if (d.source.pseudo || d.target.pseudo) {
|
if (d.source.pseudo || d.target.pseudo) {
|
||||||
if (d.target.pseudo) {
|
if (d.target.pseudo) {
|
||||||
d3.selectAll("path").each(e => {
|
d3.selectAll("path").each(e => {
|
||||||
if (e !== undefined && e.source.id === d.target.id) {
|
if (e !== undefined && e.source.id === d.target.id) {
|
||||||
toHighlight.push(e.el);
|
toHighlight.push(e.el);
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
d3.selectAll("path").each(e => {
|
|
||||||
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
|
|
||||||
toHighlight.push(e.el);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
d3.selectAll("path").each(e => {
|
||||||
|
if (e !== undefined && (e.target.id === d.source.id || e.source.id === d.source.id)) {
|
||||||
|
toHighlight.push(e.el);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (const e of toHighlight) {
|
for (const e of toHighlight) {
|
||||||
d3.select(e).classed("highlighted", true);
|
d3.select(e).classed("highlighted", true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
|
function reaction_name(d) {
|
||||||
|
const to_pseudo = d?.to_pseudo || false;
|
||||||
|
|
||||||
|
if (to_pseudo) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffix = "";
|
||||||
|
if ("reaction_probability" in d && d["reaction_probability"] !== null) {
|
||||||
|
suffix = " (p= " + d["reaction_probability"].toFixed(2) + ")"
|
||||||
|
}
|
||||||
|
return d.plain_name + suffix;
|
||||||
|
}
|
||||||
|
|
||||||
|
const linkText = linkGroup
|
||||||
|
.append("text")
|
||||||
|
.attr("class", "link-label")
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("dy", -6)
|
||||||
|
.style("font-size", "4px")
|
||||||
|
.style("pointer-events", "none")
|
||||||
|
.style("display", "none")
|
||||||
|
.text(d => reaction_name(d))
|
||||||
|
.attr("x", d => (d.source.x + d.target.x) / 2)
|
||||||
|
.attr("y", d => (d.source.y + d.target.y) / 2);
|
||||||
|
|
||||||
// add element to links array
|
// add element to links array
|
||||||
link.each(function (d) {
|
link.each(function (d) {
|
||||||
@ -700,7 +778,7 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
pop_add(link, "Reaction", edge_popup);
|
pop_add(link, "Reaction", edge_popup);
|
||||||
|
|
||||||
// Knoten zeichnen
|
// Draw Nodes
|
||||||
const node = zoomable.append("g")
|
const node = zoomable.append("g")
|
||||||
.selectAll("g")
|
.selectAll("g")
|
||||||
.data(nodes)
|
.data(nodes)
|
||||||
@ -708,44 +786,41 @@ function draw(pathway, elem) {
|
|||||||
.call(d3.drag()
|
.call(d3.drag()
|
||||||
.on("start", dragstarted)
|
.on("start", dragstarted)
|
||||||
.on("drag", dragged)
|
.on("drag", dragged)
|
||||||
.on("end", dragended))
|
.on("end", dragended)
|
||||||
|
)
|
||||||
.on("click", function (event, d) {
|
.on("click", function (event, d) {
|
||||||
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
|
const wasHighlighted = d3.select(this).select("circle").classed("highlighted");
|
||||||
|
d3.selectAll('circle.highlighted').classed('highlighted', false);
|
||||||
|
d3.selectAll("path").classed("inedge", false);
|
||||||
|
d3.selectAll("path").classed("outedge", false);
|
||||||
|
|
||||||
d3.selectAll('circle.highlighted').classed('highlighted', false);
|
if (!wasHighlighted) {
|
||||||
d3.selectAll("path").classed("inedge", false);
|
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
||||||
d3.selectAll("path").classed("outedge", false);
|
const inEdges = findPaths(d.id, links);
|
||||||
|
const outEdges = []
|
||||||
if (!wasHighlighted) {
|
// Colorize out edges green
|
||||||
d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted"));
|
for (const l of links) {
|
||||||
|
if (l.source.id === d.id) {
|
||||||
const inEdges = findPaths(d.id, 3, links);
|
outEdges.push(l);
|
||||||
const outEdges = []
|
if (l.target.pseudo) {
|
||||||
|
for (const l2 of links) {
|
||||||
// Colorize out edges green
|
if (l.target.id === l2.source.id) {
|
||||||
for (const l of links) {
|
outEdges.push(l2);
|
||||||
if (l.source.id === d.id) {
|
|
||||||
outEdges.push(l);
|
|
||||||
if (l.target.pseudo) {
|
|
||||||
for (const l2 of links) {
|
|
||||||
if (l.target.id === l2.source.id) {
|
|
||||||
outEdges.push(l2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
for (const e of inEdges) {
|
|
||||||
d3.select(e.el).classed("inedge", true);
|
|
||||||
}
|
|
||||||
for (const e of outEdges) {
|
|
||||||
d3.select(e.el).classed("outedge", true);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const e of inEdges) {
|
||||||
|
d3.select(e.el).classed("inedge", true);
|
||||||
|
}
|
||||||
|
|
||||||
})
|
for (const e of outEdges) {
|
||||||
|
d3.select(e.el).classed("outedge", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Kreise für die Knoten hinzufügen
|
// Kreise für die Knoten hinzufügen
|
||||||
node.append("circle")
|
node.append("circle")
|
||||||
@ -819,14 +894,14 @@ function draw(pathway, elem) {
|
|||||||
|
|
||||||
function serializeSVG(svgElement) {
|
function serializeSVG(svgElement) {
|
||||||
|
|
||||||
svgElement.querySelectorAll("line.link").forEach(line => {
|
svgElement.querySelectorAll("path.link").forEach(line => {
|
||||||
const style = getComputedStyle(line);
|
const style = getComputedStyle(line);
|
||||||
line.setAttribute("stroke", style.stroke);
|
line.setAttribute("stroke", style.stroke);
|
||||||
line.setAttribute("stroke-width", style.strokeWidth);
|
line.setAttribute("stroke-width", style.strokeWidth);
|
||||||
line.setAttribute("fill", style.fill);
|
line.setAttribute("fill", style.fill);
|
||||||
});
|
});
|
||||||
|
|
||||||
svgElement.querySelectorAll("line.link_no_arrow").forEach(line => {
|
svgElement.querySelectorAll("path.link_no_arrow").forEach(line => {
|
||||||
const style = getComputedStyle(line);
|
const style = getComputedStyle(line);
|
||||||
line.setAttribute("stroke", style.stroke);
|
line.setAttribute("stroke", style.stroke);
|
||||||
line.setAttribute("stroke-width", style.strokeWidth);
|
line.setAttribute("stroke-width", style.strokeWidth);
|
||||||
|
|||||||
@ -6,7 +6,6 @@
|
|||||||
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
x-data="modalForm({ state: { selectedEdge: '', imageUrl: '' } })"
|
||||||
@modal-opened.window="
|
@modal-opened.window="
|
||||||
const links = d3.selectAll('path.highlighted');
|
const links = d3.selectAll('path.highlighted');
|
||||||
|
|
||||||
if (!links.empty()) {
|
if (!links.empty()) {
|
||||||
const el = links.node();
|
const el = links.node();
|
||||||
const selectElement = document.getElementById('delete_pathway_edge_edges');
|
const selectElement = document.getElementById('delete_pathway_edge_edges');
|
||||||
@ -18,9 +17,7 @@
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectElement.dispatchEvent(new Event('change'));
|
selectElement.dispatchEvent(new Event('change'));
|
||||||
|
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
@close="reset()"
|
@close="reset()"
|
||||||
|
|||||||
@ -6,17 +6,14 @@
|
|||||||
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
x-data="modalForm({ state: { selectedNode: '', imageUrl: '' } })"
|
||||||
@modal-opened.window="
|
@modal-opened.window="
|
||||||
const el = d3.select('circle.highlighted').node();
|
const el = d3.select('circle.highlighted').node();
|
||||||
|
|
||||||
if (el !== null) {
|
if (el !== null) {
|
||||||
const selectElement = document.getElementById('delete_pathway_node_nodes');
|
const selectElement = document.getElementById('delete_pathway_node_nodes');
|
||||||
|
|
||||||
for (let option of selectElement.options) {
|
for (let option of selectElement.options) {
|
||||||
if (option.value === el.__data__.url) {
|
if (option.value === el.__data__.url) {
|
||||||
option.selected = true;
|
option.selected = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectElement.dispatchEvent(new Event('change'));
|
selectElement.dispatchEvent(new Event('change'));
|
||||||
}
|
}
|
||||||
"
|
"
|
||||||
|
|||||||
@ -56,8 +56,10 @@
|
|||||||
<ul class="menu bg-base-200 rounded-box">
|
<ul class="menu bg-base-200 rounded-box">
|
||||||
{% for um in group.user_member.all %}
|
{% for um in group.user_member.all %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}" class="hover:bg-base-300"
|
<a
|
||||||
>{{ um.username }}
|
href="{% if user.is_superuser %}{{ um.url }}{% else %}{{ "#" }}{% endif %}"
|
||||||
|
class="hover:bg-base-300"
|
||||||
|
>{{ um.username }}
|
||||||
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
{% if not um.is_active %}<i>(inactive)</i>{% endif %}</a
|
||||||
>
|
>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@ -55,6 +55,14 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
{% if node.description and node.description != "no description" %}
|
||||||
|
<div class="collapse-arrow bg-base-200 collapse">
|
||||||
|
<input type="checkbox" checked />
|
||||||
|
<div class="collapse-title text-xl font-medium">Description</div>
|
||||||
|
<div class="collapse-content">{{ node.description }}</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
{% epdb_slot_templates "epdb.objects.node.viz" as viz_templates %}
|
||||||
|
|
||||||
{% for tpl in viz_templates %}
|
{% for tpl in viz_templates %}
|
||||||
|
|||||||
@ -21,12 +21,13 @@
|
|||||||
.link {
|
.link {
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
/* marker-end: url(#arrow); */
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.link_no_arrow {
|
.link_no_arrow {
|
||||||
stroke: #999;
|
stroke: #999;
|
||||||
stroke-opacity: 0.6;
|
stroke-opacity: 0.6;
|
||||||
|
stroke-width: 1.5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.node image {
|
.node image {
|
||||||
@ -77,12 +78,10 @@
|
|||||||
stroke: red;
|
stroke: red;
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.outedge {
|
.outedge {
|
||||||
stroke: green;
|
stroke: green;
|
||||||
stroke-width: 3px;
|
stroke-width: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
<script src="{% static 'js/pw.js' %}"></script>
|
<script src="{% static 'js/pw.js' %}"></script>
|
||||||
|
|
||||||
@ -122,7 +121,7 @@
|
|||||||
<div class="collapse-title text-xl font-medium">
|
<div class="collapse-title text-xl font-medium">
|
||||||
Graphical Representation
|
Graphical Representation
|
||||||
</div>
|
</div>
|
||||||
<div class="collapse-content ">
|
<div class="collapse-content">
|
||||||
<div class="bg-base-100 mb-2 rounded-lg p-2">
|
<div class="bg-base-100 mb-2 rounded-lg p-2">
|
||||||
<div class="navbar bg-base-100 rounded-lg">
|
<div class="navbar bg-base-100 rounded-lg">
|
||||||
<div class="flex-1">
|
<div class="flex-1">
|
||||||
@ -179,6 +178,52 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
class="dropdown-content menu bg-base-100 rounded-box z-50 w-60 p-2"
|
||||||
>
|
>
|
||||||
|
<li>
|
||||||
|
<a id="compound-names-toggle-button" class="cursor-pointer">
|
||||||
|
<svg
|
||||||
|
id="compound-names-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-eye"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
Compound Names
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a id="reaction-names-toggle-button" class="cursor-pointer">
|
||||||
|
<svg
|
||||||
|
id="reaction-names-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="16"
|
||||||
|
height="16"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="lucide lucide-eye"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"
|
||||||
|
/>
|
||||||
|
<circle cx="12" cy="12" r="3" />
|
||||||
|
</svg>
|
||||||
|
Reaction Names
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
{% if pathway.setting.model.app_domain %}
|
{% if pathway.setting.model.app_domain %}
|
||||||
<li>
|
<li>
|
||||||
<a id="app-domain-toggle-button" class="cursor-pointer">
|
<a id="app-domain-toggle-button" class="cursor-pointer">
|
||||||
@ -204,6 +249,7 @@
|
|||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if 1 == 0 %}
|
||||||
<li>
|
<li>
|
||||||
<a id="timeseries-toggle-button" class="cursor-pointer">
|
<a id="timeseries-toggle-button" class="cursor-pointer">
|
||||||
<svg
|
<svg
|
||||||
@ -254,6 +300,7 @@
|
|||||||
Show Predicted Properties
|
Show Predicted Properties
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -497,6 +544,10 @@
|
|||||||
{{ pathway.d3_json|json_script:"pathway" }}
|
{{ pathway.d3_json|json_script:"pathway" }}
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
// Global switch for compound names view
|
||||||
|
var compoundNamesViewEnabled = false;
|
||||||
|
// Gloabl switch for reaction names view
|
||||||
|
var reactionNamesViewEnabled = false;
|
||||||
// Global switch for app domain view
|
// Global switch for app domain view
|
||||||
var appDomainViewEnabled = false;
|
var appDomainViewEnabled = false;
|
||||||
// Global switch for timeseries view
|
// Global switch for timeseries view
|
||||||
@ -532,6 +583,58 @@
|
|||||||
descContent.innerHTML = newDesc;
|
descContent.innerHTML = newDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const compoundNamesBtn = document.getElementById(
|
||||||
|
"compound-names-toggle-button",
|
||||||
|
);
|
||||||
|
if (compoundNamesBtn) {
|
||||||
|
compoundNamesBtn.addEventListener("click", function () {
|
||||||
|
compoundNamesViewEnabled = !compoundNamesViewEnabled;
|
||||||
|
const icon = document.getElementById("compound-names-icon");
|
||||||
|
if (compoundNamesViewEnabled) {
|
||||||
|
// Change to eye-off icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
if (x.name) {
|
||||||
|
d3.select(x.el)
|
||||||
|
.append("text")
|
||||||
|
.text((d) => d.plain_name)
|
||||||
|
.attr("text-anchor", "middle")
|
||||||
|
.attr("y", -20)
|
||||||
|
.style("font-size", "4px");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Change back to eye icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||||
|
nodes.forEach((x) => {
|
||||||
|
d3.select(x.el).select("text").remove();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const reactionNamesBtn = document.getElementById(
|
||||||
|
"reaction-names-toggle-button",
|
||||||
|
);
|
||||||
|
if (reactionNamesBtn) {
|
||||||
|
reactionNamesBtn.addEventListener("click", function () {
|
||||||
|
reactionNamesViewEnabled = !reactionNamesViewEnabled;
|
||||||
|
const icon = document.getElementById("reaction-names-icon");
|
||||||
|
if (reactionNamesViewEnabled) {
|
||||||
|
// Change to eye-off icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M9.88 9.88a3 3 0 1 0 4.24 4.24"/><path d="M10.73 5.08A10.43 10.43 0 0 1 12 5c7 0 10 7 10 7a13.16 13.16 0 0 1-1.67 2.68"/><path d="M6.61 6.61A13.526 13.526 0 0 0 2 12s3 7 10 7a9.74 9.74 0 0 0 5.39-1.61"/><line x1="2" x2="22" y1="2" y2="22"/>';
|
||||||
|
d3.selectAll(".link-label").style("display", null);
|
||||||
|
} else {
|
||||||
|
// Change back to eye icon
|
||||||
|
icon.innerHTML =
|
||||||
|
'<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"/><circle cx="12" cy="12" r="3"/>';
|
||||||
|
d3.selectAll(".link-label").style("display", "none");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// App domain toggle
|
// App domain toggle
|
||||||
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
const appDomainBtn = document.getElementById("app-domain-toggle-button");
|
||||||
if (appDomainBtn) {
|
if (appDomainBtn) {
|
||||||
|
|||||||
@ -24,6 +24,76 @@
|
|||||||
<td>Setting Name</td>
|
<td>Setting Name</td>
|
||||||
<td>{{ setting_to_render.name }}</td>
|
<td>{{ setting_to_render.name }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>Setting URL</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ setting_to_render.url }}" class="link link-primary">{{ setting_to_render.url }}</a>
|
||||||
|
<div
|
||||||
|
x-data="{
|
||||||
|
value: '{{ setting_to_render.url }}',
|
||||||
|
copied: false,
|
||||||
|
|
||||||
|
async copy() {
|
||||||
|
await navigator.clipboard.writeText(this.value)
|
||||||
|
this.copied = true
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
this.copied = false
|
||||||
|
}, 1500)
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
class="join"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="btn btn-ghost"
|
||||||
|
:data-tip="copied ? 'Copied!' : 'Copy'"
|
||||||
|
@click="copy"
|
||||||
|
aria-label="Copy to clipboard"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
x-show="!copied"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
width="12"
|
||||||
|
height="12"
|
||||||
|
x="8"
|
||||||
|
y="8"
|
||||||
|
rx="2"
|
||||||
|
ry="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
x-show="copied"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-4 w-4"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="M5 13l4 4L19 7"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% if setting_to_render.description %}
|
{% if setting_to_render.description %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>Setting Description</td>
|
<td>Setting Description</td>
|
||||||
|
|||||||
@ -212,6 +212,8 @@
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
const button = this;
|
const button = this;
|
||||||
button.disabled = true;
|
button.disabled = true;
|
||||||
|
|
||||||
|
// Set text depending on mode
|
||||||
if (document.getElementById("predict-submit-button").innerText === "Build") {
|
if (document.getElementById("predict-submit-button").innerText === "Build") {
|
||||||
button.textContent = "Building...";
|
button.textContent = "Building...";
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
from epdb.exceptions import InvalidSMILESException, InvalidMolfileException
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Compound, User, CompoundStructure
|
from epdb.models import Compound, User, CompoundStructure
|
||||||
|
|
||||||
@ -17,6 +19,10 @@ class CompoundTest(TestCase):
|
|||||||
cls.user = User.objects.get(username="anonymous")
|
cls.user = User.objects.get(username="anonymous")
|
||||||
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
|
cls.package = PackageManager.create_package(cls.user, "Anon Test Package", "No Desc")
|
||||||
|
|
||||||
|
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
|
||||||
|
cls.VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
|
||||||
|
cls.INVALID_MOLFILE = "this is not a valid molfile"
|
||||||
|
|
||||||
def test_smoke(self):
|
def test_smoke(self):
|
||||||
c = Compound.create(
|
c = Compound.create(
|
||||||
self.package,
|
self.package,
|
||||||
@ -33,13 +39,13 @@ class CompoundTest(TestCase):
|
|||||||
self.assertEqual(c.description, "No Desc")
|
self.assertEqual(c.description, "No Desc")
|
||||||
|
|
||||||
def test_missing_smiles(self):
|
def test_missing_smiles(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
|
_ = Compound.create(self.package, smiles=None, name="Afoxolaner", description="No Desc")
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
|
_ = Compound.create(self.package, smiles="", name="Afoxolaner", description="No Desc")
|
||||||
|
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
|
_ = Compound.create(self.package, smiles=" ", name="Afoxolaner", description="No Desc")
|
||||||
|
|
||||||
def test_smiles_are_trimmed(self):
|
def test_smiles_are_trimmed(self):
|
||||||
@ -96,7 +102,7 @@ class CompoundTest(TestCase):
|
|||||||
self.assertEqual(len(self.package.compounds), 1)
|
self.assertEqual(len(self.package.compounds), 1)
|
||||||
|
|
||||||
def test_wrong_smiles(self):
|
def test_wrong_smiles(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Compound.create(
|
_ = Compound.create(
|
||||||
self.package,
|
self.package,
|
||||||
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
smiles="C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F",
|
||||||
@ -188,3 +194,153 @@ class CompoundTest(TestCase):
|
|||||||
|
|
||||||
c1.set_default_structure(c2)
|
c1.set_default_structure(c2)
|
||||||
self.assertNotEqual(default_structure, c2)
|
self.assertNotEqual(default_structure, c2)
|
||||||
|
|
||||||
|
def test_create_structure_from_molfile(self):
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Compound",
|
||||||
|
description="Created from molfile",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Structure",
|
||||||
|
description="Structure from molfile",
|
||||||
|
)
|
||||||
|
|
||||||
|
# The SMILES must have been derived from the molfile, not the placeholder
|
||||||
|
self.assertNotEqual(cs.smiles, "PLACEHOLDER")
|
||||||
|
self.assertIsNotNone(cs.smiles)
|
||||||
|
self.assertTrue(len(cs.smiles) > 0)
|
||||||
|
|
||||||
|
def test_create_structure_from_molfile_stores_molfile(self):
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="c1c(C(=O)O)ccc([N+]([O-])=O)c1",
|
||||||
|
name="Molfile Store Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = c.default_structure
|
||||||
|
|
||||||
|
# O=C(O)C1=CC=C([N+](=O)[O-])C=C1 will be overwritten with
|
||||||
|
# c1c(C(=O)O)ccc([N+]([O-])=O)c1 and molfile will be set
|
||||||
|
# on the existing structure
|
||||||
|
_ = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Structure with Molfile",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Fetch fresh from DB to confirm persistence
|
||||||
|
cs_db = CompoundStructure.objects.get(pk=cs.pk)
|
||||||
|
self.assertIsNotNone(cs_db.molfile)
|
||||||
|
self.assertNotEqual(cs_db.molfile.strip(), "")
|
||||||
|
|
||||||
|
def test_create_structure_from_invalid_molfile_raises(self):
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Invalid Molfile Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
with self.assertRaises(InvalidMolfileException):
|
||||||
|
CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=self.INVALID_MOLFILE,
|
||||||
|
name="Bad Structure",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_molfile_takes_precedence_over_smiles(self):
|
||||||
|
"""When both molfile and smiles are supplied, molfile must win."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Precedence Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="C", # intentionally wrong / different SMILES
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Wins",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
# SMILES should be derived from molfile, not the supplied "C"
|
||||||
|
self.assertNotEqual(cs.smiles, "C")
|
||||||
|
|
||||||
|
def test_empty_molfile_falls_back_to_smiles(self):
|
||||||
|
"""An empty or whitespace-only molfile should be ignored and SMILES used instead."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Empty Molfile Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=" ", # whitespace only – should be ignored
|
||||||
|
name="Fallback to SMILES",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||||||
|
|
||||||
|
def test_none_molfile_falls_back_to_smiles(self):
|
||||||
|
"""None as molfile should be ignored and SMILES used instead."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="None Molfile Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
molfile=None,
|
||||||
|
name="Fallback None Molfile",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(cs.smiles, "O=C(O)C1=CC=C([N+](=O)[O-])C=C1")
|
||||||
|
|
||||||
|
def test_molfile_deduplication(self):
|
||||||
|
"""Creating a structure twice from the same molfile should return the existing object."""
|
||||||
|
c = Compound.create(
|
||||||
|
self.package,
|
||||||
|
smiles="O=C(O)C1=CC=C([N+](=O)[O-])C=C1",
|
||||||
|
name="Molfile Dedup Test",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs1 = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Structure",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
cs2 = CompoundStructure.create(
|
||||||
|
compound=c,
|
||||||
|
smiles="PLACEHOLDER",
|
||||||
|
molfile=self.VALID_MOLFILE,
|
||||||
|
name="Molfile Structure",
|
||||||
|
description="No Desc",
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(cs1.pk, cs2.pk)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
from django.conf import settings as s
|
from django.conf import settings as s
|
||||||
from django.test import TestCase, override_settings
|
from django.test import TestCase, override_settings
|
||||||
|
|
||||||
|
from epdb.exceptions import InvalidSMILESException
|
||||||
from epdb.logic import PackageManager
|
from epdb.logic import PackageManager
|
||||||
from epdb.models import Compound, User, Reaction, Rule
|
from epdb.models import Compound, User, Reaction, Rule
|
||||||
|
|
||||||
@ -163,7 +164,7 @@ class ReactionTest(TestCase):
|
|||||||
self.assertEqual(len(self.package.reactions), 1)
|
self.assertEqual(len(self.package.reactions), 1)
|
||||||
|
|
||||||
def test_wrong_smiles(self):
|
def test_wrong_smiles(self):
|
||||||
with self.assertRaises(ValueError):
|
with self.assertRaises(InvalidSMILESException):
|
||||||
_ = Reaction.create(
|
_ = Reaction.create(
|
||||||
package=self.package,
|
package=self.package,
|
||||||
name="Eawag BBD reaction r0001",
|
name="Eawag BBD reaction r0001",
|
||||||
|
|||||||
@ -11,6 +11,9 @@ from epdb.models import Compound, Scenario, ExternalDatabase
|
|||||||
class CompoundViewTest(TestCase):
|
class CompoundViewTest(TestCase):
|
||||||
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
|
fixtures = ["test_fixtures_incl_model.jsonl.gz"]
|
||||||
|
|
||||||
|
# A valid V2000 molfile for 4-Nitrobenzoic acid (O=C(O)C1=CC=C([N+](=O)[O-])C=C1)
|
||||||
|
VALID_MOLFILE = """\n Mrv2211 01012500002D\n\n 12 12 0 0 0 0 999 V2000\n 1.4289 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -0.8250 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.0000 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.0625 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 -1.6500 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 0.4125 0.0000 C 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 1.2375 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1.4289 0.8250 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 0.7145 -2.8875 0.0000 N 0 3 0 0 0 0 0 0 0 0 0 0\n 0.0000 -3.3000 0.0000 O 0 5 0 0 0 0 0 0 0 0 0 0\n 1.4289 -3.3000 0.0000 O 0 0 0 0 0 0 0 0 0 0 0 0\n 1 2 2 0 0 0 0\n 2 3 1 0 0 0 0\n 3 4 2 0 0 0 0\n 4 5 1 0 0 0 0\n 5 6 2 0 0 0 0\n 6 1 1 0 0 0 0\n 2 7 1 0 0 0 0\n 7 8 2 0 0 0 0\n 7 9 1 0 0 0 0\n 5 10 1 0 0 0 0\n 10 11 1 0 0 0 0\n 10 12 2 0 0 0 0\nM CHG 2 10 1 11 -1\nM END\n"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
super(CompoundViewTest, cls).setUpClass()
|
super(CompoundViewTest, cls).setUpClass()
|
||||||
@ -396,3 +399,108 @@ class CompoundViewTest(TestCase):
|
|||||||
|
|
||||||
c = Compound.objects.get(url=compound_url)
|
c = Compound.objects.get(url=compound_url)
|
||||||
self.assertEqual(len(c.aliases), 0)
|
self.assertEqual(len(c.aliases), 0)
|
||||||
|
|
||||||
|
def test_create_compound_via_molfile(self):
|
||||||
|
"""POSTing a valid molfile without a SMILES should create a compound successfully."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Created from molfile",
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
compound_url = response.url
|
||||||
|
|
||||||
|
c = Compound.objects.get(url=compound_url)
|
||||||
|
|
||||||
|
self.assertEqual(c.name, "4-Nitrobenzoic acid")
|
||||||
|
self.assertEqual(c.description, "Created from molfile")
|
||||||
|
# SMILES should have been extracted from molfile, so it must be non-empty
|
||||||
|
self.assertIsNotNone(c.default_structure.smiles)
|
||||||
|
self.assertNotEqual(c.default_structure.smiles.strip(), "")
|
||||||
|
|
||||||
|
def test_create_compound_molfile_takes_precedence_over_smiles(self):
|
||||||
|
"""When both molfile and SMILES are submitted, the molfile should win."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Molfile precedence test",
|
||||||
|
"compound-smiles": "C", # intentionally wrong/different
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
compound_url = response.url
|
||||||
|
|
||||||
|
c = Compound.objects.get(url=compound_url)
|
||||||
|
|
||||||
|
# The resulting SMILES must NOT be the dummy "C" supplied via compound-smiles
|
||||||
|
self.assertNotEqual(c.default_structure.smiles, "C")
|
||||||
|
|
||||||
|
def test_create_compound_via_invalid_molfile_returns_error(self):
|
||||||
|
"""POSTing an invalid molfile should not create a compound and should return an error response."""
|
||||||
|
initial_count = self.user1_default_package.compounds.count()
|
||||||
|
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "Bad Molfile Compound",
|
||||||
|
"compound-description": "Should fail",
|
||||||
|
"compound-molfile": "this is not a molfile at all",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# The view should signal an error (non-2xx or a redirect to an error page, not 302 to a new compound)
|
||||||
|
self.assertNotEqual(response.status_code, 302)
|
||||||
|
# No new compound should have been created
|
||||||
|
self.assertEqual(self.user1_default_package.compounds.count(), initial_count)
|
||||||
|
|
||||||
|
def test_create_compound_via_empty_molfile_falls_back_to_smiles(self):
|
||||||
|
"""Submitting an empty molfile with a valid SMILES should fall back to SMILES."""
|
||||||
|
response = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "Fallback SMILES Compound",
|
||||||
|
"compound-description": "Empty molfile fallback",
|
||||||
|
"compound-smiles": "C(CCl)Cl",
|
||||||
|
"compound-molfile": " ", # whitespace only
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
compound_url = response.url
|
||||||
|
|
||||||
|
c = Compound.objects.get(url=compound_url)
|
||||||
|
self.assertEqual(c.default_structure.smiles, "C(CCl)Cl")
|
||||||
|
|
||||||
|
def test_create_compound_via_molfile_deduplication(self):
|
||||||
|
"""Submitting the same molfile twice should return the existing compound."""
|
||||||
|
response1 = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Molfile dedup test",
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response1.status_code, 302)
|
||||||
|
compound_url_1 = response1.url
|
||||||
|
|
||||||
|
response2 = self.client.post(
|
||||||
|
reverse("compounds"),
|
||||||
|
{
|
||||||
|
"compound-name": "4-Nitrobenzoic acid",
|
||||||
|
"compound-description": "Molfile dedup test",
|
||||||
|
"compound-molfile": self.VALID_MOLFILE,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response2.status_code, 302)
|
||||||
|
self.assertEqual(response2.url, compound_url_1)
|
||||||
|
self.assertEqual(self.user1_default_package.compounds.count(), 1)
|
||||||
|
|||||||
@ -46,6 +46,14 @@ class ModelViewTest(TestCase):
|
|||||||
)
|
)
|
||||||
|
|
||||||
expected = [
|
expected = [
|
||||||
|
{
|
||||||
|
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
|
||||||
|
"probability": 0.75,
|
||||||
|
"btrule": {
|
||||||
|
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
||||||
|
"name": "bt0001-3568",
|
||||||
|
},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
|
"products": [["O=C(O)C1=CC(CO)=CC=C1", "CCNCC"]],
|
||||||
"probability": 0.25,
|
"probability": 0.25,
|
||||||
@ -62,14 +70,6 @@ class ModelViewTest(TestCase):
|
|||||||
"name": "bt0243-4301",
|
"name": "bt0243-4301",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"products": [["CCN(CC)C(=O)C1=CC(C=O)=CC=C1"]],
|
|
||||||
"probability": 0.75,
|
|
||||||
"btrule": {
|
|
||||||
"url": "http://localhost:8000/package/1869d3f0-60bb-41fd-b6f8-afa75ffb09d3/simple-ambit-rule/2f2e0c39-e109-4836-959f-2bda2524f022",
|
|
||||||
"name": "bt0001-3568",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
|
|
||||||
actual = response.json()["pred"]
|
actual = response.json()["pred"]
|
||||||
|
|||||||
@ -72,8 +72,10 @@ class PackageViewTest(TestCase):
|
|||||||
|
|
||||||
def test_import_package(self):
|
def test_import_package(self):
|
||||||
file = SimpleUploadedFile(
|
file = SimpleUploadedFile(
|
||||||
"Fixture_Package.json",
|
"EAWAG-BBD_32de3cf4-e3e6-4168-956e-32fa5ddb0ce1.json",
|
||||||
open(s.FIXTURE_DIRS[0] / "Fixture_Package.json", "rb").read(),
|
open(
|
||||||
|
s.FIXTURE_DIRS[0] / "EAWAG-BBD_32de3cf4-e3e6-4168-956e-32fa5ddb0ce1.json", "rb"
|
||||||
|
).read(),
|
||||||
content_type="application/json",
|
content_type="application/json",
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -86,10 +88,11 @@ class PackageViewTest(TestCase):
|
|||||||
|
|
||||||
p = Package.objects.get(url=package_url)
|
p = Package.objects.get(url=package_url)
|
||||||
|
|
||||||
self.assertEqual(p.pathways.count(), 22)
|
self.assertEqual(p.pathways.count(), 219)
|
||||||
self.assertEqual(p.rules.count(), 45)
|
self.assertEqual(p.rules.count(), 498)
|
||||||
self.assertEqual(p.compounds.count(), 223)
|
self.assertEqual(p.compounds.count(), 1399)
|
||||||
self.assertEqual(p.reactions.count(), 212)
|
self.assertEqual(p.reactions.count(), 1480)
|
||||||
|
self.assertEqual(p.scenarios.count(), 1914)
|
||||||
|
|
||||||
upp = UserPackagePermission.objects.get(package=p, user=self.user1)
|
upp = UserPackagePermission.objects.get(package=p, user=self.user1)
|
||||||
self.assertEqual(upp.permission, Permission.ALL[0])
|
self.assertEqual(upp.permission, Permission.ALL[0])
|
||||||
|
|||||||
1484
utilities/misc.py
1484
utilities/misc.py
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user