forked from enviPath/enviPy
Copy Objects between Packages (#59)
Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#59
This commit is contained in:
128
epdb/logic.py
128
epdb/logic.py
@ -1,6 +1,6 @@
|
||||
import re
|
||||
import logging
|
||||
from typing import Union, List, Optional, Set, Dict
|
||||
from typing import Union, List, Optional, Set, Dict, Any
|
||||
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import transaction
|
||||
@ -13,6 +13,132 @@ from utilities.chem import FormatConverter
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EPDBURLParser:
|
||||
|
||||
UUID_PATTERN = r'[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}'
|
||||
|
||||
MODEL_PATTERNS = {
|
||||
'epdb.User': re.compile(rf'^.*/user/{UUID_PATTERN}'),
|
||||
'epdb.Group': re.compile(rf'^.*/group/{UUID_PATTERN}'),
|
||||
'epdb.Package': re.compile(rf'^.*/package/{UUID_PATTERN}'),
|
||||
'epdb.Compound': re.compile(rf'^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}'),
|
||||
'epdb.CompoundStructure': re.compile(rf'^.*/package/{UUID_PATTERN}/compound/{UUID_PATTERN}/structure/{UUID_PATTERN}'),
|
||||
'epdb.Rule': re.compile(rf'^.*/package/{UUID_PATTERN}/(?:simple-ambit-rule|simple-rdkit-rule|parallel-rule|sequential-rule|rule)/{UUID_PATTERN}'),
|
||||
'epdb.Reaction': re.compile(rf'^.*/package/{UUID_PATTERN}/reaction/{UUID_PATTERN}$'),
|
||||
'epdb.Pathway': re.compile(rf'^.*/package/{UUID_PATTERN}/pathway/{UUID_PATTERN}'),
|
||||
'epdb.Node': re.compile(rf'^.*/package/{UUID_PATTERN}/pathway/{UUID_PATTERN}/node/{UUID_PATTERN}'),
|
||||
'epdb.Edge': re.compile(rf'^.*/package/{UUID_PATTERN}/pathway/{UUID_PATTERN}/edge/{UUID_PATTERN}'),
|
||||
'epdb.Scenario': re.compile(rf'^.*/package/{UUID_PATTERN}/scenario/{UUID_PATTERN}'),
|
||||
'epdb.EPModel': re.compile(rf'^.*/package/{UUID_PATTERN}/model/{UUID_PATTERN}'),
|
||||
'epdb.Setting': re.compile(rf'^.*/setting/{UUID_PATTERN}'),
|
||||
}
|
||||
|
||||
def __init__(self, url: str):
|
||||
self.url = url
|
||||
self._matches = {}
|
||||
self._analyze_url()
|
||||
|
||||
def _analyze_url(self):
|
||||
for model_path, pattern in self.MODEL_PATTERNS.items():
|
||||
match = pattern.findall(self.url)
|
||||
if match:
|
||||
self._matches[model_path] = match[0]
|
||||
|
||||
def _get_model_class(self, model_path: str):
|
||||
try:
|
||||
from django.apps import apps
|
||||
app_label, model_name = model_path.split('.')[-2:]
|
||||
return apps.get_model(app_label, model_name)
|
||||
except (ImportError, LookupError, ValueError):
|
||||
raise ValueError(f"Model {model_path} does not exist!")
|
||||
|
||||
def _get_object_by_url(self, model_path: str, url: str):
|
||||
model_class = self._get_model_class(model_path)
|
||||
return model_class.objects.get(url=url)
|
||||
|
||||
def is_package_url(self) -> bool:
|
||||
return bool(re.compile(rf'^.*/package/{self.UUID_PATTERN}$').findall(self.url))
|
||||
|
||||
def contains_package_url(self):
|
||||
return bool(self.MODEL_PATTERNS['epdb.Package'].findall(self.url)) and not self.is_package_url()
|
||||
|
||||
def is_user_url(self) -> bool:
|
||||
return bool(self.MODEL_PATTERNS['epdb.User'].findall(self.url))
|
||||
|
||||
def is_group_url(self) -> bool:
|
||||
return bool(self.MODEL_PATTERNS['epdb.Group'].findall(self.url))
|
||||
|
||||
def is_setting_url(self) -> bool:
|
||||
return bool(self.MODEL_PATTERNS['epdb.Setting'].findall(self.url))
|
||||
|
||||
def get_object(self) -> Optional[Any]:
|
||||
# Define priority order from most specific to least specific
|
||||
priority_order = [
|
||||
# 3rd level
|
||||
'epdb.CompoundStructure',
|
||||
'epdb.Node',
|
||||
'epdb.Edge',
|
||||
# 2nd level
|
||||
'epdb.Compound',
|
||||
'epdb.Rule',
|
||||
'epdb.Reaction',
|
||||
'epdb.Scenario',
|
||||
'epdb.EPModel',
|
||||
'epdb.Pathway',
|
||||
# 1st level
|
||||
'epdb.Package',
|
||||
'epdb.Setting',
|
||||
'epdb.Group',
|
||||
'epdb.User',
|
||||
]
|
||||
|
||||
for model_path in priority_order:
|
||||
if model_path in self._matches:
|
||||
url = self._matches[model_path]
|
||||
return self._get_object_by_url(model_path, url)
|
||||
|
||||
raise ValueError(f"No object found for URL {self.url}")
|
||||
|
||||
def get_objects(self) -> List[Any]:
|
||||
"""
|
||||
Get all Django model objects along the URL path in hierarchical order.
|
||||
Returns objects from parent to child (e.g., Package -> Compound -> Structure).
|
||||
"""
|
||||
objects = []
|
||||
|
||||
hierarchy_order = [
|
||||
# 1st level
|
||||
'epdb.Package',
|
||||
'epdb.Setting',
|
||||
'epdb.Group',
|
||||
'epdb.User',
|
||||
# 2nd level
|
||||
'epdb.Compound',
|
||||
'epdb.Rule',
|
||||
'epdb.Reaction',
|
||||
'epdb.Scenario',
|
||||
'epdb.EPModel',
|
||||
'epdb.Pathway',
|
||||
# 3rd level
|
||||
'epdb.CompoundStructure',
|
||||
'epdb.Node',
|
||||
'epdb.Edge',
|
||||
]
|
||||
|
||||
for model_path in hierarchy_order:
|
||||
if model_path in self._matches:
|
||||
url = self._matches[model_path]
|
||||
objects.append(self._get_object_by_url(model_path, url))
|
||||
|
||||
return objects
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"EPDBURLParser(url='{self.url}')"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"EPDBURLParser(url='{self.url}', matches={list(self._matches.keys())})"
|
||||
|
||||
|
||||
class UserManager(object):
|
||||
user_pattern = re.compile(r".*/user/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}")
|
||||
|
||||
|
||||
@ -35,7 +35,6 @@ def populate_url(apps, schema_editor):
|
||||
]
|
||||
for model in MODELS:
|
||||
obj_cls = apps.get_model("epdb", model)
|
||||
print(f"Populating url for {model}")
|
||||
for obj in obj_cls.objects.all():
|
||||
obj.url = assemble_url(obj)
|
||||
if obj.url is None:
|
||||
@ -49,7 +48,7 @@ def assemble_url(obj):
|
||||
case 'User':
|
||||
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
|
||||
case 'Group':
|
||||
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
|
||||
return '{}/group/{}'.format(s.SERVER_URL, obj.uuid)
|
||||
case 'Package':
|
||||
return '{}/package/{}'.format(s.SERVER_URL, obj.uuid)
|
||||
case 'Compound':
|
||||
|
||||
@ -26,7 +26,6 @@ def populate_url(apps, schema_editor):
|
||||
]
|
||||
for model in MODELS:
|
||||
obj_cls = apps.get_model("epdb", model)
|
||||
print(f"Populating url for {model}")
|
||||
for obj in obj_cls.objects.all():
|
||||
obj.url = assemble_url(obj)
|
||||
if obj.url is None:
|
||||
@ -40,7 +39,7 @@ def assemble_url(obj):
|
||||
case 'User':
|
||||
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
|
||||
case 'Group':
|
||||
return '{}/user/{}'.format(s.SERVER_URL, obj.uuid)
|
||||
return '{}/group/{}'.format(s.SERVER_URL, obj.uuid)
|
||||
case 'Package':
|
||||
return '{}/package/{}'.format(s.SERVER_URL, obj.uuid)
|
||||
case 'Compound':
|
||||
|
||||
260
epdb/models.py
260
epdb/models.py
@ -533,15 +533,16 @@ class AliasMixin(models.Model):
|
||||
@transaction.atomic
|
||||
def add_alias(self, new_alias, set_as_default=False):
|
||||
if set_as_default:
|
||||
self.aliases.add(self.name)
|
||||
self.aliases.append(self.name)
|
||||
self.name = new_alias
|
||||
|
||||
if new_alias in self.aliases:
|
||||
self.aliases.remove(new_alias)
|
||||
else:
|
||||
if new_alias not in self.aliases:
|
||||
self.aliases.add(new_alias)
|
||||
self.aliases.append(new_alias)
|
||||
|
||||
self.aliases = sorted(list(set(self.aliases)), key=lambda x: x.lower())
|
||||
self.save()
|
||||
|
||||
class Meta:
|
||||
@ -762,6 +763,64 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdentifierMixin
|
||||
|
||||
return cs
|
||||
|
||||
@transaction.atomic
|
||||
def copy(self, target: 'Package', mapping: Dict):
|
||||
if self in mapping:
|
||||
return mapping[self]
|
||||
|
||||
new_compound = Compound.objects.create(
|
||||
package=target,
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
kv=self.kv.copy() if self.kv else {}
|
||||
)
|
||||
mapping[self] = new_compound
|
||||
|
||||
# Copy compound structures
|
||||
for structure in self.structures.all():
|
||||
if structure not in mapping:
|
||||
new_structure = CompoundStructure.objects.create(
|
||||
compound=new_compound,
|
||||
smiles=structure.smiles,
|
||||
canonical_smiles=structure.canonical_smiles,
|
||||
inchikey=structure.inchikey,
|
||||
normalized_structure=structure.normalized_structure,
|
||||
name=structure.name,
|
||||
description=structure.description,
|
||||
kv=structure.kv.copy() if structure.kv else {}
|
||||
)
|
||||
mapping[structure] = new_structure
|
||||
|
||||
# Copy external identifiers for structure
|
||||
for ext_id in structure.external_identifiers.all():
|
||||
ExternalIdentifier.objects.create(
|
||||
content_object=new_structure,
|
||||
database=ext_id.database,
|
||||
identifier_value=ext_id.identifier_value,
|
||||
url=ext_id.url,
|
||||
is_primary=ext_id.is_primary
|
||||
)
|
||||
|
||||
if self.default_structure:
|
||||
new_compound.default_structure = mapping.get(self.default_structure)
|
||||
new_compound.save()
|
||||
|
||||
for a in self.aliases:
|
||||
new_compound.add_alias(a)
|
||||
new_compound.save()
|
||||
|
||||
# Copy external identifiers for compound
|
||||
for ext_id in self.external_identifiers.all():
|
||||
ExternalIdentifier.objects.create(
|
||||
content_object=new_compound,
|
||||
database=ext_id.database,
|
||||
identifier_value=ext_id.identifier_value,
|
||||
url=ext_id.url,
|
||||
is_primary=ext_id.is_primary
|
||||
)
|
||||
|
||||
return new_compound
|
||||
|
||||
class Meta:
|
||||
unique_together = [('uuid', 'package')]
|
||||
|
||||
@ -817,6 +876,14 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
|
||||
|
||||
return cs
|
||||
|
||||
@transaction.atomic
|
||||
def copy(self, target: 'Package', mapping: Dict):
|
||||
if self in mapping:
|
||||
return mapping[self]
|
||||
|
||||
self.compound.copy(target, mapping)
|
||||
return mapping[self]
|
||||
|
||||
@property
|
||||
def as_svg(self, width: int = 800, height: int = 400):
|
||||
return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height)
|
||||
@ -856,22 +923,54 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
return cls.create(*args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
# @property
|
||||
# def related_pathways(self):
|
||||
# reaction_ids = self.related_reactions.values_list('id', flat=True)
|
||||
# pathways = Edge.objects.filter(edge_label__in=reaction_ids).values_list('pathway', flat=True)
|
||||
# return Pathway.objects.filter(package=self.package, id__in=set(pathways)).order_by('name')
|
||||
#
|
||||
# @property
|
||||
# def related_reactions(self):
|
||||
# return (
|
||||
# Reaction.objects.filter(package=self.package, rules__in=[self])
|
||||
# |
|
||||
# Reaction.objects.filter(package=self.package, rules__in=[self])
|
||||
# ).order_by('name')
|
||||
#
|
||||
#
|
||||
@transaction.atomic
|
||||
def copy(self, target: 'Package', mapping: Dict):
|
||||
"""Copy a rule to the target package."""
|
||||
if self in mapping:
|
||||
return mapping[self]
|
||||
|
||||
# Get the specific rule type and copy accordingly
|
||||
rule_type = type(self)
|
||||
|
||||
if rule_type == SimpleAmbitRule:
|
||||
new_rule = SimpleAmbitRule.objects.create(
|
||||
package=target,
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
smirks=self.smirks,
|
||||
reactant_filter_smarts=self.reactant_filter_smarts,
|
||||
product_filter_smarts=self.product_filter_smarts,
|
||||
kv=self.kv.copy() if self.kv else {}
|
||||
)
|
||||
elif rule_type == SimpleRDKitRule:
|
||||
new_rule = SimpleRDKitRule.objects.create(
|
||||
package=target,
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
reaction_smarts=self.reaction_smarts,
|
||||
kv=self.kv.copy() if self.kv else {}
|
||||
)
|
||||
elif rule_type == ParallelRule:
|
||||
new_rule = ParallelRule.objects.create(
|
||||
package=target,
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
kv=self.kv.copy() if self.kv else {}
|
||||
)
|
||||
# Copy simple rules relationships
|
||||
for simple_rule in self.simple_rules.all():
|
||||
copied_simple_rule = simple_rule.copy(target, mapping)
|
||||
new_rule.simple_rules.add(copied_simple_rule)
|
||||
elif rule_type == SequentialRule:
|
||||
raise ValueError("SequentialRule copy not implemented!")
|
||||
else:
|
||||
raise ValueError(f"Unknown rule type: {rule_type}")
|
||||
|
||||
mapping[self] = new_rule
|
||||
|
||||
return new_rule
|
||||
|
||||
|
||||
class SimpleRule(Rule):
|
||||
pass
|
||||
|
||||
@ -1141,6 +1240,50 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin, ReactionIdentifierMixin
|
||||
r.save()
|
||||
return r
|
||||
|
||||
@transaction.atomic
|
||||
def copy(self, target: 'Package', mapping: Dict ) -> 'Reaction':
|
||||
"""Copy a reaction to the target package."""
|
||||
if self in mapping:
|
||||
return mapping[self]
|
||||
|
||||
# Create new reaction
|
||||
new_reaction = Reaction.objects.create(
|
||||
package=target,
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
multi_step=self.multi_step,
|
||||
medline_references=self.medline_references,
|
||||
kv=self.kv.copy() if self.kv else {}
|
||||
)
|
||||
mapping[self] = new_reaction
|
||||
|
||||
# Copy educts (reactant compounds)
|
||||
for educt in self.educts.all():
|
||||
copied_educt = educt.copy(target, mapping)
|
||||
new_reaction.educts.add(copied_educt)
|
||||
|
||||
# Copy products
|
||||
for product in self.products.all():
|
||||
copied_product = product.copy(target, mapping)
|
||||
new_reaction.products.add(copied_product)
|
||||
|
||||
# Copy rules
|
||||
for rule in self.rules.all():
|
||||
copied_rule = rule.copy(target, mapping)
|
||||
new_reaction.rules.add(copied_rule)
|
||||
|
||||
# Copy external identifiers
|
||||
for ext_id in self.external_identifiers.all():
|
||||
ExternalIdentifier.objects.create(
|
||||
content_object=new_reaction,
|
||||
database=ext_id.database,
|
||||
identifier_value=ext_id.identifier_value,
|
||||
url=ext_id.url,
|
||||
is_primary=ext_id.is_primary
|
||||
)
|
||||
|
||||
return new_reaction
|
||||
|
||||
def smirks(self):
|
||||
return f"{'.'.join([cs.smiles for cs in self.educts.all()])}>>{'.'.join([cs.smiles for cs in self.products.all()])}"
|
||||
|
||||
@ -1375,6 +1518,87 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
|
||||
return pw
|
||||
|
||||
@transaction.atomic
|
||||
def copy(self, target: 'Package', mapping: Dict) -> 'Pathway':
|
||||
|
||||
if self in mapping:
|
||||
return mapping[self]
|
||||
|
||||
# Start copying the pathway
|
||||
new_pathway = Pathway.objects.create(
|
||||
package=target,
|
||||
name=self.name,
|
||||
description=self.description,
|
||||
setting=self.setting, # TODO copy settings?
|
||||
kv=self.kv.copy() if self.kv else {}
|
||||
)
|
||||
|
||||
# # Copy aliases if they exist
|
||||
# if hasattr(self, 'aliases'):
|
||||
# new_pathway.aliases.set(self.aliases.all())
|
||||
#
|
||||
# # Copy scenarios if they exist
|
||||
# if hasattr(self, 'scenarios'):
|
||||
# new_pathway.scenarios.set(self.scenarios.all())
|
||||
|
||||
# Copy all nodes first
|
||||
for node in self.nodes.all():
|
||||
# Copy the compound structure for the node label
|
||||
copied_structure = None
|
||||
if node.default_node_label:
|
||||
copied_compound = node.default_node_label.compound.copy(target, mapping)
|
||||
# Find the corresponding structure in the copied compound
|
||||
for structure in copied_compound.structures.all():
|
||||
if structure.smiles == node.default_node_label.smiles:
|
||||
copied_structure = structure
|
||||
break
|
||||
|
||||
new_node = Node.objects.create(
|
||||
pathway=new_pathway,
|
||||
default_node_label=copied_structure,
|
||||
depth=node.depth,
|
||||
name=node.name,
|
||||
description=node.description,
|
||||
kv=node.kv.copy() if node.kv else {}
|
||||
)
|
||||
mapping[node] = new_node
|
||||
|
||||
# Copy node labels (many-to-many relationship)
|
||||
for label in node.node_labels.all():
|
||||
copied_label_compound = label.compound.copy(target, mapping)
|
||||
for structure in copied_label_compound.structures.all():
|
||||
if structure.smiles == label.smiles:
|
||||
new_node.node_labels.add(structure)
|
||||
break
|
||||
|
||||
# Copy all edges
|
||||
for edge in self.edges.all():
|
||||
# Copy the reaction for edge label if it exists
|
||||
copied_reaction = None
|
||||
if edge.edge_label:
|
||||
copied_reaction = edge.edge_label.copy(target, mapping)
|
||||
|
||||
new_edge = Edge.objects.create(
|
||||
pathway=new_pathway,
|
||||
edge_label=copied_reaction,
|
||||
name=edge.name,
|
||||
description=edge.description,
|
||||
kv=edge.kv.copy() if edge.kv else {}
|
||||
)
|
||||
|
||||
# Copy start and end nodes relationships
|
||||
for start_node in edge.start_nodes.all():
|
||||
if start_node in mapping:
|
||||
new_edge.start_nodes.add(mapping[start_node])
|
||||
|
||||
for end_node in edge.end_nodes.all():
|
||||
if end_node in mapping:
|
||||
new_edge.end_nodes.add(mapping[end_node])
|
||||
|
||||
mapping[self] = new_pathway
|
||||
|
||||
return new_pathway
|
||||
|
||||
@transaction.atomic
|
||||
def add_node(self, smiles: str, name: Optional[str] = None, description: Optional[str] = None):
|
||||
return Node.create(self, smiles, 0)
|
||||
|
||||
@ -13,7 +13,7 @@ from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from utilities.chem import FormatConverter, IndigoUtils
|
||||
from utilities.decorators import package_permission_required
|
||||
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager
|
||||
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager, EPDBURLParser
|
||||
from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \
|
||||
EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \
|
||||
UserPackagePermission, Permission, License, User, Edge
|
||||
@ -211,6 +211,32 @@ def set_scenarios(current_user, attach_object, scenario_urls: List[str]):
|
||||
|
||||
attach_object.set_scenarios(scens)
|
||||
|
||||
|
||||
def copy_object(current_user, target_package: 'Package', source_object_url: str):
|
||||
# Ensures that source is readable
|
||||
source_package = PackageManager.get_package_by_url(current_user, source_object_url)
|
||||
|
||||
parser = EPDBURLParser(source_object_url)
|
||||
|
||||
# if the url won't contain a package or is a plain package
|
||||
if not parser.contains_package_url():
|
||||
raise ValueError(f"Object {source_object_url} can't be copied!")
|
||||
|
||||
# Gets the most specific object
|
||||
source_object = parser.get_object()
|
||||
|
||||
if hasattr(source_object, 'copy'):
|
||||
mapping = dict()
|
||||
copy = source_object.copy(target_package, mapping)
|
||||
|
||||
if s.DEBUG:
|
||||
for k, v in mapping.items():
|
||||
logger.debug(f"Mapping {k.url} to {v.url}")
|
||||
|
||||
return copy
|
||||
|
||||
raise ValueError(f"Object {source_object} can't be copied!")
|
||||
|
||||
def index(request):
|
||||
context = get_base_context(request)
|
||||
context['title'] = 'enviPath - Home'
|
||||
@ -764,6 +790,14 @@ def package(request, package_uuid):
|
||||
for g in Group.objects.filter(public=True):
|
||||
PackageManager.update_permissions(current_user, current_package, g, Permission.READ[0])
|
||||
return redirect(current_package.url)
|
||||
elif hidden == 'copy':
|
||||
object_to_copy = request.POST.get('object_to_copy')
|
||||
|
||||
if not object_to_copy:
|
||||
return error(request, 'Invalid target package.', 'Please select a target package.')
|
||||
|
||||
copied_object = copy_object(current_user, current_package, object_to_copy)
|
||||
return JsonResponse({'success': copied_object.url})
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
|
||||
|
||||
@ -11,6 +11,12 @@
|
||||
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||
</li>
|
||||
{% if meta.can_edit %}
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Compound</a>
|
||||
|
||||
@ -8,10 +8,16 @@
|
||||
<i class="glyphicon glyphicon-plus"></i> Add Reaction</a>
|
||||
</li>
|
||||
<li role="separator" class="divider"></li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#download_pathway_modal">
|
||||
<i class="glyphicon glyphicon-floppy-save"></i> Download Pathway</a>
|
||||
</li>
|
||||
{% if meta.can_edit %}
|
||||
<li role="separator" class="divider"></li>
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#edit_pathway_modal">
|
||||
|
||||
@ -7,6 +7,12 @@
|
||||
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||
</li>
|
||||
{% if meta.can_edit %}
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Reaction</a>
|
||||
|
||||
@ -7,6 +7,12 @@
|
||||
<a role="button" data-toggle="modal" data-target="#set_scenario_modal">
|
||||
<i class="glyphicon glyphicon-plus"></i> Set Scenarios</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>
|
||||
<a role="button" data-toggle="modal" data-target="#generic_copy_object_modal">
|
||||
<i class="glyphicon glyphicon-duplicate"></i> Copy</a>
|
||||
</li>
|
||||
{% if meta.can_edit %}
|
||||
<li>
|
||||
<a class="button" data-toggle="modal" data-target="#generic_delete_modal">
|
||||
<i class="glyphicon glyphicon-trash"></i> Delete Rule</a>
|
||||
|
||||
61
templates/modals/objects/generic_copy_object_modal.html
Normal file
61
templates/modals/objects/generic_copy_object_modal.html
Normal file
@ -0,0 +1,61 @@
|
||||
{% load static %}
|
||||
<!-- Copy Object -->
|
||||
<div id="generic_copy_object_modal" class="modal" tabindex="-1">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3 class="modal-title">Copy {{ object_type|capfirst }}</h3>
|
||||
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="generic-copy-object-modal-form" accept-charset="UTF-8" data-remote="true" method="post">
|
||||
{% csrf_token %}
|
||||
<label for="target-package">Select the Target Package you want to copy this {{ object_type }}
|
||||
into</label>
|
||||
<select id="target-package" name="target-package" data-actions-box='true' class="form-control"
|
||||
data-width='100%'>
|
||||
<option disabled selected>Select Target Package</option>
|
||||
{% for p in meta.writeable_packages %}
|
||||
<option value="{{ p.url }}">{{ p.name }}</option>`
|
||||
{% endfor %}
|
||||
</select>
|
||||
<input type="hidden" name="hidden" value="copy">
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-primary" id="generic-copy-object-modal-form-submit">
|
||||
Copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
$(function () {
|
||||
|
||||
$('#generic-copy-object-modal-form-submit').click(function (e) {
|
||||
e.preventDefault();
|
||||
|
||||
const packageUrl = $('#target-package').find(":selected").val();
|
||||
|
||||
if (packageUrl === 'Select Target Package' || packageUrl === '' || packageUrl === null || packageUrl === undefined) {
|
||||
return;
|
||||
}
|
||||
const formData = {
|
||||
hidden: 'copy',
|
||||
object_to_copy: '{{ current_object.url }}',
|
||||
}
|
||||
|
||||
$.post(packageUrl, formData, function (response) {
|
||||
if (response.success) {
|
||||
window.location.href = response.success;
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
})
|
||||
</script>
|
||||
@ -5,6 +5,7 @@
|
||||
{% block action_modals %}
|
||||
{% include "modals/objects/edit_rule_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
|
||||
@ -6,6 +6,7 @@
|
||||
{% include "modals/objects/edit_compound_modal.html" %}
|
||||
{% include "modals/objects/add_structure_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
|
||||
@ -82,6 +82,7 @@
|
||||
{% include "modals/objects/add_pathway_node_modal.html" %}
|
||||
{% include "modals/objects/add_pathway_edge_modal.html" %}
|
||||
{% include "modals/objects/download_pathway_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/edit_pathway_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/delete_pathway_node_modal.html" %}
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
{% block action_modals %}
|
||||
{% include "modals/objects/edit_reaction_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
{% block action_modals %}
|
||||
{% include "modals/objects/edit_rule_modal.html" %}
|
||||
{% include "modals/objects/generic_set_scenario_modal.html" %}
|
||||
{% include "modals/objects/generic_copy_object_modal.html" %}
|
||||
{% include "modals/objects/generic_delete_modal.html" %}
|
||||
{% endblock action_modals %}
|
||||
|
||||
|
||||
200
tests/test_copy_objects.py
Normal file
200
tests/test_copy_objects.py
Normal file
@ -0,0 +1,200 @@
|
||||
import json
|
||||
|
||||
from django.test import TestCase
|
||||
|
||||
from epdb.logic import PackageManager
|
||||
from epdb.models import Compound, User, CompoundStructure, Reaction, Rule, MLRelativeReasoning, Pathway
|
||||
|
||||
|
||||
class CopyTest(TestCase):
|
||||
fixtures = ["test_fixture.cleaned.json"]
|
||||
|
||||
def setUp(self):
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
super(CopyTest, cls).setUpClass()
|
||||
cls.user = User.objects.get(username='anonymous')
|
||||
cls.package = PackageManager.create_package(cls.user, 'Source Package', 'No Desc')
|
||||
cls.AFOXOLANER = Compound.create(
|
||||
cls.package,
|
||||
smiles='C1C(=NOC1(C2=CC(=CC(=C2)Cl)C(F)(F)F)C(F)(F)F)C3=CC=C(C4=CC=CC=C43)C(=O)NCC(=O)NCC(F)(F)F',
|
||||
name='Afoxolaner',
|
||||
description='Test compound for copying'
|
||||
)
|
||||
|
||||
cls.FOUR_NITROBENZOIC_ACID = Compound.create(
|
||||
cls.package,
|
||||
smiles='[O-][N+](=O)c1ccc(C(=O)[O-])cc1', # Normalized: O=C(O)C1=CC=C([N+](=O)[O-])C=C1',
|
||||
name='Test Compound',
|
||||
description='Compound with multiple structures'
|
||||
)
|
||||
|
||||
cls.ETHANOL = Compound.create(
|
||||
cls.package,
|
||||
smiles='CCO',
|
||||
name='Ethanol',
|
||||
description='Simple alcohol'
|
||||
)
|
||||
cls.target_package = PackageManager.create_package(cls.user, 'Target Package', 'No Desc')
|
||||
|
||||
cls.reaction_educt = Compound.create(
|
||||
cls.package,
|
||||
smiles='C(CCl)Cl',
|
||||
name='1,2-Dichloroethane',
|
||||
description='Eawag BBD compound c0001'
|
||||
).default_structure
|
||||
|
||||
cls.reaction_product = Compound.create(
|
||||
cls.package,
|
||||
smiles='C(CO)Cl',
|
||||
name='2-Chloroethanol',
|
||||
description='Eawag BBD compound c0005'
|
||||
).default_structure
|
||||
|
||||
cls.REACTION = Reaction.create(
|
||||
package=cls.package,
|
||||
name='Eawag BBD reaction r0001',
|
||||
educts=[cls.reaction_educt],
|
||||
products=[cls.reaction_product],
|
||||
multi_step=False
|
||||
)
|
||||
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls):
|
||||
pass
|
||||
|
||||
def tearDown(self):
|
||||
pass
|
||||
|
||||
|
||||
def test_compound_copy_basic(self):
|
||||
"""Test basic compound copying functionality"""
|
||||
mapping = dict()
|
||||
copied_compound = self.AFOXOLANER.copy(self.target_package, mapping)
|
||||
|
||||
self.assertNotEqual(self.AFOXOLANER.uuid, copied_compound.uuid)
|
||||
self.assertEqual(self.AFOXOLANER.name, copied_compound.name)
|
||||
self.assertEqual(self.AFOXOLANER.description, copied_compound.description)
|
||||
self.assertEqual(copied_compound.package, self.target_package)
|
||||
self.assertEqual(self.AFOXOLANER.package, self.package)
|
||||
self.assertEqual(self.AFOXOLANER.default_structure.smiles, copied_compound.default_structure.smiles)
|
||||
|
||||
def test_compound_copy_with_multiple_structures(self):
|
||||
"""Test copying a compound with multiple structures"""
|
||||
|
||||
original_structure_count = self.FOUR_NITROBENZOIC_ACID.structures.count()
|
||||
|
||||
mapping = dict()
|
||||
# Copy the compound
|
||||
copied_compound = self.FOUR_NITROBENZOIC_ACID.copy(self.target_package, mapping)
|
||||
|
||||
# Verify all structures were copied
|
||||
self.assertEqual(copied_compound.structures.count(), original_structure_count)
|
||||
|
||||
# Verify default_structure is properly set
|
||||
self.assertIsNotNone(copied_compound.default_structure)
|
||||
self.assertEqual(
|
||||
copied_compound.default_structure.smiles,
|
||||
self.FOUR_NITROBENZOIC_ACID.default_structure.smiles
|
||||
)
|
||||
|
||||
def test_compound_copy_preserves_aliases(self):
|
||||
"""Test that compound copying preserves aliases"""
|
||||
# Create a compound and add aliases
|
||||
original_compound = self.ETHANOL
|
||||
|
||||
# Add aliases if the method exists
|
||||
if hasattr(original_compound, 'add_alias'):
|
||||
original_compound.add_alias('Ethyl alcohol')
|
||||
original_compound.add_alias('Grain alcohol')
|
||||
|
||||
mapping = dict()
|
||||
copied_compound = original_compound.copy(self.target_package, mapping)
|
||||
|
||||
# Verify aliases were copied if they exist
|
||||
if hasattr(original_compound, 'aliases') and hasattr(copied_compound, 'aliases'):
|
||||
original_aliases = original_compound.aliases
|
||||
copied_aliases = copied_compound.aliases
|
||||
self.assertEqual(original_aliases, copied_aliases)
|
||||
|
||||
def test_compound_copy_preserves_external_identifiers(self):
|
||||
"""Test that external identifiers are preserved during copying"""
|
||||
original_compound = self.ETHANOL
|
||||
|
||||
# Add external identifiers if the methods exist
|
||||
if hasattr(original_compound, 'add_cas_number'):
|
||||
original_compound.add_cas_number('64-17-5')
|
||||
if hasattr(original_compound, 'add_pubchem_compound_id'):
|
||||
original_compound.add_pubchem_compound_id('702')
|
||||
|
||||
mapping = dict()
|
||||
copied_compound = original_compound.copy(self.target_package, mapping)
|
||||
|
||||
# Verify external identifiers were copied
|
||||
original_ext_ids = original_compound.external_identifiers.all()
|
||||
copied_ext_ids = copied_compound.external_identifiers.all()
|
||||
|
||||
self.assertEqual(original_ext_ids.count(), copied_ext_ids.count())
|
||||
|
||||
# Check that identifier values match
|
||||
original_values = set(ext_id.identifier_value for ext_id in original_ext_ids)
|
||||
copied_values = set(ext_id.identifier_value for ext_id in copied_ext_ids)
|
||||
self.assertEqual(original_values, copied_values)
|
||||
|
||||
def test_compound_copy_structure_properties(self):
|
||||
"""Test that structure properties are properly copied"""
|
||||
original_compound = self.ETHANOL
|
||||
|
||||
mapping = dict()
|
||||
copied_compound = original_compound.copy(self.target_package, mapping)
|
||||
|
||||
# Verify structure properties
|
||||
original_structure = original_compound.default_structure
|
||||
copied_structure = copied_compound.default_structure
|
||||
|
||||
self.assertEqual(original_structure.smiles, copied_structure.smiles)
|
||||
self.assertEqual(original_structure.canonical_smiles, copied_structure.canonical_smiles)
|
||||
self.assertEqual(original_structure.inchikey, copied_structure.inchikey)
|
||||
self.assertEqual(original_structure.normalized_structure, copied_structure.normalized_structure)
|
||||
|
||||
# Verify they are different objects
|
||||
self.assertNotEqual(original_structure.uuid, copied_structure.uuid)
|
||||
self.assertEqual(copied_structure.compound, copied_compound)
|
||||
|
||||
def test_reaction_copy_basic(self):
|
||||
"""Test basic reaction copying functionality"""
|
||||
mapping = dict()
|
||||
copied_reaction = self.REACTION.copy(self.target_package, mapping)
|
||||
|
||||
self.assertNotEqual(self.REACTION.uuid, copied_reaction.uuid)
|
||||
self.assertEqual(self.REACTION.name, copied_reaction.name)
|
||||
self.assertEqual(self.REACTION.description, copied_reaction.description)
|
||||
self.assertEqual(self.REACTION.multi_step, copied_reaction.multi_step)
|
||||
self.assertEqual(copied_reaction.package, self.target_package)
|
||||
self.assertEqual(self.REACTION.package, self.package)
|
||||
|
||||
|
||||
def test_reaction_copy_structures(self):
|
||||
"""Test basic reaction copying functionality"""
|
||||
mapping = dict()
|
||||
copied_reaction = self.REACTION.copy(self.target_package, mapping)
|
||||
|
||||
for orig_educt, copy_educt in zip(self.REACTION.educts.all(), copied_reaction.educts.all()):
|
||||
self.assertNotEqual(orig_educt.uuid, copy_educt.uuid)
|
||||
self.assertEqual(orig_educt.name, copy_educt.name)
|
||||
self.assertEqual(orig_educt.description, copy_educt.description)
|
||||
self.assertEqual(copy_educt.compound.package, self.target_package)
|
||||
self.assertEqual(orig_educt.compound.package, self.package)
|
||||
self.assertEqual(orig_educt.smiles, copy_educt.smiles)
|
||||
|
||||
for orig_product, copy_product in zip(self.REACTION.products.all(), copied_reaction.products.all()):
|
||||
self.assertNotEqual(orig_product.uuid, copy_product.uuid)
|
||||
self.assertEqual(orig_product.name, copy_product.name)
|
||||
self.assertEqual(orig_product.description, copy_product.description)
|
||||
self.assertEqual(copy_product.compound.package, self.target_package)
|
||||
self.assertEqual(orig_product.compound.package, self.package)
|
||||
self.assertEqual(orig_product.smiles, copy_product.smiles)
|
||||
|
||||
Reference in New Issue
Block a user