[Feature] External Identifier/References

Co-authored-by: Tim Lorsbach <tim@lorsba.ch>
Reviewed-on: enviPath/enviPy#139
This commit is contained in:
2025-10-02 00:40:00 +13:00
parent 3f5bb76633
commit 7ad4112343
19 changed files with 9257 additions and 19 deletions

View File

@ -16,7 +16,9 @@ from .models import (
Node,
Edge,
Scenario,
Setting
Setting,
ExternalDatabase,
ExternalIdentifier
)
@ -43,6 +45,7 @@ class EPAdmin(admin.ModelAdmin):
class PackageAdmin(EPAdmin):
pass
class MLRelativeReasoningAdmin(EPAdmin):
pass
@ -87,6 +90,14 @@ class SettingAdmin(EPAdmin):
pass
class ExternalDatabaseAdmin(admin.ModelAdmin):
pass
class ExternalIdentifierAdmin(admin.ModelAdmin):
pass
admin.site.register(User, UserAdmin)
admin.site.register(UserPackagePermission, UserPackagePermissionAdmin)
admin.site.register(Group, GroupAdmin)
@ -103,3 +114,5 @@ admin.site.register(Node, NodeAdmin)
admin.site.register(Edge, EdgeAdmin)
admin.site.register(Setting, SettingAdmin)
admin.site.register(Scenario, ScenarioAdmin)
admin.site.register(ExternalDatabase, ExternalDatabaseAdmin)
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)

View File

@ -116,7 +116,7 @@ class Command(BaseCommand):
'full_name': 'KEGG Reaction Database',
'description': 'Database of biochemical reactions',
'base_url': 'https://www.genome.jp',
'url_pattern': 'https://www.genome.jp/entry/reaction+{id}'
'url_pattern': 'https://www.genome.jp/entry/{id}'
},
{
'name': 'UniProt',

View File

@ -0,0 +1,57 @@
from csv import DictReader
from django.core.management.base import BaseCommand
from epdb.models import *
class Command(BaseCommand):
STR_TO_MODEL = {
'Compound': Compound,
'CompoundStructure': CompoundStructure,
'Reaction': Reaction,
}
STR_TO_DATABASE = {
'ChEBI': ExternalDatabase.objects.get(name='ChEBI'),
'RHEA': ExternalDatabase.objects.get(name='RHEA'),
'KEGG Reaction': ExternalDatabase.objects.get(name='KEGG Reaction'),
'PubChem Compound': ExternalDatabase.objects.get(name='PubChem Compound'),
'PubChem Substance': ExternalDatabase.objects.get(name='PubChem Substance'),
}
def add_arguments(self, parser):
parser.add_argument(
'--data',
type=str,
help='Path of the ID Mapping file.',
required=True,
)
parser.add_argument(
'--replace-host',
type=str,
help='Replace https://envipath.org/ with this host, e.g. http://localhost:8000/',
)
@transaction.atomic
def handle(self, *args, **options):
with open(options['data']) as fh:
reader = DictReader(fh)
for row in reader:
clz = self.STR_TO_MODEL[row['model']]
url = row['url']
if options['replace_host']:
url = url.replace('https://envipath.org/', options['replace_host'])
instance = clz.objects.get(url=url)
db = self.STR_TO_DATABASE[row['identifier_type']]
ExternalIdentifier.objects.create(
content_object=instance,
database=db,
identifier_value=row['identifier_value'],
url=db.url_pattern.format(id=row['identifier_value']),
is_primary=False
)

View File

@ -277,6 +277,53 @@ class ExternalDatabase(TimeStampedModel):
return self.url_pattern.format(id=identifier_value)
return None
@staticmethod
def get_databases():
return {
'compound': [
{
'database': ExternalDatabase.objects.get(name='PubChem Compound'),
'placeholder': 'PubChem Compound ID e.g. 12345',
}, {
'database': ExternalDatabase.objects.get(name='PubChem Substance'),
'placeholder': 'PubChem Substance ID e.g. 12345',
}, {
'database': ExternalDatabase.objects.get(name='KEGG Reaction'),
'placeholder': 'KEGG ID including entity Prefix e.g. C12345',
}, {
'database': ExternalDatabase.objects.get(name='ChEBI'),
'placeholder': 'ChEBI ID without prefix e.g. 12345',
},
],
'structure': [
{
'database': ExternalDatabase.objects.get(name='PubChem Compound'),
'placeholder': 'PubChem Compound ID e.g. 12345',
}, {
'database': ExternalDatabase.objects.get(name='PubChem Substance'),
'placeholder': 'PubChem Substance ID e.g. 12345',
}, {
'database': ExternalDatabase.objects.get(name='KEGG Reaction'),
'placeholder': 'KEGG ID including entity Prefix e.g. C12345',
}, {
'database': ExternalDatabase.objects.get(name='ChEBI'),
'placeholder': 'ChEBI ID without prefix e.g. 12345',
},
],
'reaction': [
{
'database': ExternalDatabase.objects.get(name='KEGG Reaction'),
'placeholder': 'KEGG ID including entity Prefix e.g. C12345',
}, {
'database': ExternalDatabase.objects.get(name='RHEA'),
'placeholder': 'RHEA ID without Prefix e.g. 12345',
}, {
'database': ExternalDatabase.objects.get(name='UniProt'),
'placeholder': 'Query ID for UniPro e.g. rhea:12345',
}
]
}
class ExternalIdentifier(TimeStampedModel):
uuid = models.UUIDField(default=uuid4, editable=False, unique=True)

View File

@ -17,7 +17,7 @@ from utilities.misc import HTMLGenerator
from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager, EPDBURLParser
from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \
EPModel, EnviFormer, MLRelativeReasoning, RuleBasedRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \
UserPackagePermission, Permission, License, User, Edge
UserPackagePermission, Permission, License, User, Edge, ExternalDatabase, ExternalIdentifier
logger = logging.getLogger(__name__)
@ -194,6 +194,7 @@ def get_base_context(request, for_user=None) -> Dict[str, Any]:
'available_settings': SettingManager.get_all_settings(current_user),
'enabled_features': s.FLAGS,
'debug': s.DEBUG,
'external_databases': ExternalDatabase.get_databases(),
},
}
@ -249,6 +250,9 @@ 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)
if source_package == target_package:
raise ValueError(f"Can't copy object {source_object_url} to the same package!")
parser = EPDBURLParser(source_object_url)
# if the url won't contain a package or is a plain package
@ -892,7 +896,11 @@ def package(request, package_uuid):
if not object_to_copy:
return error(request, 'No object to copy', 'There was no object to copy.')
copied_object = copy_object(current_user, current_package, object_to_copy)
try:
copied_object = copy_object(current_user, current_package, object_to_copy)
except ValueError as e:
return JsonResponse({'error': f"Can't copy object {object_to_copy} to the same package!"}, status=400)
return JsonResponse({'success': copied_object.url})
else:
return HttpResponseBadRequest()
@ -1043,8 +1051,8 @@ def package_compound(request, package_uuid, compound_uuid):
set_scenarios(current_user, current_compound, selected_scenarios)
return redirect(current_compound.url)
new_compound_name = request.POST.get('compound-name')
new_compound_description = request.POST.get('compound-description')
new_compound_name = request.POST.get('compound-name', '').strip()
new_compound_description = request.POST.get('compound-description', '').strip()
if new_compound_name:
current_compound.name = new_compound_name
@ -1055,6 +1063,20 @@ def package_compound(request, package_uuid, compound_uuid):
if any([new_compound_name, new_compound_description]):
current_compound.save()
return redirect(current_compound.url)
selected_database = request.POST.get('selected-database', '').strip()
external_identifier = request.POST.get('identifier', '').strip()
if selected_database and external_identifier:
db = ExternalDatabase.objects.get(id=int(selected_database))
ExternalIdentifier.objects.create(
content_object=current_compound,
database=db,
identifier_value=external_identifier,
url=db.url_pattern.format(id=external_identifier),
is_primary=False
)
return redirect(current_compound.url)
else:
return HttpResponseBadRequest()
@ -1146,12 +1168,39 @@ def package_compound_structure(request, package_uuid, compound_uuid, structure_u
else:
return HttpResponseBadRequest()
new_structure_name = request.POST.get('compound-structure-name', '').strip()
new_structure_description = request.POST.get('compound-structure-description', '').strip()
if new_structure_name:
current_structure.name = new_structure_name
if new_structure_description:
current_structure.description = new_structure_description
if any([new_structure_name, new_structure_description]):
current_structure.save()
return redirect(current_structure.url)
if 'selected-scenarios' in request.POST:
selected_scenarios = request.POST.getlist('selected-scenarios')
set_scenarios(current_user, current_structure, selected_scenarios)
return redirect(current_structure.url)
selected_database = request.POST.get('selected-database', '').strip()
external_identifier = request.POST.get('identifier', '').strip()
if selected_database and external_identifier:
db = ExternalDatabase.objects.get(id=int(selected_database))
ExternalIdentifier.objects.create(
content_object=current_structure,
database=db,
identifier_value=external_identifier,
url=db.url_pattern.format(id=external_identifier),
is_primary=False
)
return redirect(current_structure.url)
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(['GET', ])
@ -1382,8 +1431,8 @@ def package_reaction(request, package_uuid, reaction_uuid):
set_scenarios(current_user, current_reaction, selected_scenarios)
return redirect(current_reaction.url)
new_reaction_name = request.POST.get('reaction-name')
new_reaction_description = request.POST.get('reaction-description')
new_reaction_name = request.POST.get('reaction-name', '').strip()
new_reaction_description = request.POST.get('reaction-description', '').strip()
if new_reaction_name:
current_reaction.name = new_reaction_name
@ -1394,8 +1443,22 @@ def package_reaction(request, package_uuid, reaction_uuid):
if any([new_reaction_name, new_reaction_description]):
current_reaction.save()
return redirect(current_reaction.url)
else:
return HttpResponseBadRequest()
selected_database = request.POST.get('selected-database', '').strip()
external_identifier = request.POST.get('identifier', '').strip()
if selected_database and external_identifier:
db = ExternalDatabase.objects.get(id=int(selected_database))
ExternalIdentifier.objects.create(
content_object=current_reaction,
database=db,
identifier_value=external_identifier,
url=db.url_pattern.format(id=external_identifier),
is_primary=False
)
return redirect(current_reaction.url)
return HttpResponseBadRequest()
else:
return HttpResponseNotAllowed(['GET', 'POST'])