forked from enviPath/enviPy
[Feature] Package Export/Import (#116)
Fixes #90 Fixes #91 Fixes #115 Fixes #104 Co-authored-by: Tim Lorsbach <tim@lorsba.ch> Reviewed-on: enviPath/enviPy#116
This commit is contained in:
@ -10,6 +10,7 @@ from django.conf import settings as s
|
||||
from epdb.models import User, Package, UserPackagePermission, GroupPackagePermission, Permission, Group, Setting, \
|
||||
EPModel, UserSettingPermission, Rule, Pathway, Node, Edge, Compound, Reaction, CompoundStructure
|
||||
from utilities.chem import FormatConverter
|
||||
from utilities.misc import PackageImporter, PackageExporter
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -324,17 +325,6 @@ class PackageManager(object):
|
||||
return True
|
||||
return False
|
||||
|
||||
# @staticmethod
|
||||
# def get_package_permission(user: 'User', package: Union[str, 'Package']):
|
||||
# if PackageManager.administrable(user, package):
|
||||
# return Permission.ALL[0]
|
||||
# elif PackageManager.writable(user, package):
|
||||
# return Permission.WRITE[0]
|
||||
# elif PackageManager.readable(user, package):
|
||||
# return Permission.READ[0]
|
||||
# else:
|
||||
# return None
|
||||
|
||||
@staticmethod
|
||||
def has_package_permission(user: 'User', package: Union[str, 'Package'], permission: str):
|
||||
|
||||
@ -491,7 +481,7 @@ class PackageManager(object):
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def import_package(data: dict, owner: User, keep_ids=False, add_import_timestamp=True, trust_reviewed=False):
|
||||
def import_legacy_package(data: dict, owner: User, keep_ids=False, add_import_timestamp=True, trust_reviewed=False):
|
||||
from uuid import UUID, uuid4
|
||||
from datetime import datetime
|
||||
from collections import defaultdict
|
||||
@ -872,6 +862,28 @@ class PackageManager(object):
|
||||
|
||||
return pack
|
||||
|
||||
@staticmethod
|
||||
@transaction.atomic
|
||||
def import_pacakge(data: Dict[str, Any], owner: User, preserve_uuids=False, add_import_timestamp=True,
|
||||
trust_reviewed=False) -> Package:
|
||||
|
||||
importer = PackageImporter(data, preserve_uuids, add_import_timestamp, trust_reviewed)
|
||||
imported_package = importer.do_import()
|
||||
|
||||
up = UserPackagePermission()
|
||||
up.user = owner
|
||||
up.package = imported_package
|
||||
up.permission = up.ALL[0]
|
||||
up.save()
|
||||
|
||||
return imported_package
|
||||
|
||||
@staticmethod
|
||||
def export_package(package: Package, include_models: bool = False,
|
||||
include_external_identifiers: bool = True) -> Dict[str, Any]:
|
||||
return PackageExporter(package).do_export()
|
||||
|
||||
|
||||
class SettingManager(object):
|
||||
setting_pattern = re.compile(r".*/setting/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$")
|
||||
|
||||
|
||||
@ -12,21 +12,24 @@ class Command(BaseCommand):
|
||||
|
||||
def create_users(self):
|
||||
|
||||
if not User.objects.filter(email='anon@lorsba.ch').exists():
|
||||
anon = UserManager.create_user("anonymous", "anon@lorsba.ch", "SuperSafe", is_active=True,
|
||||
add_to_group=False, set_setting=False)
|
||||
# Anonymous User
|
||||
if not User.objects.filter(email='anon@envipath.com').exists():
|
||||
anon = UserManager.create_user("anonymous", "anon@envipath.com", "SuperSafe",
|
||||
is_active=True, add_to_group=False, set_setting=False)
|
||||
else:
|
||||
anon = User.objects.get(email='anon@lorsba.ch')
|
||||
anon = User.objects.get(email='anon@envipath.com')
|
||||
|
||||
if not User.objects.filter(email='admin@lorsba.ch').exists():
|
||||
admin = UserManager.create_user("admin", "admin@lorsba.ch", "SuperSafe", is_active=True, add_to_group=False,
|
||||
set_setting=False)
|
||||
# Admin User
|
||||
if not User.objects.filter(email='admin@envipath.com').exists():
|
||||
admin = UserManager.create_user("admin", "admin@envipath.com", "SuperSafe",
|
||||
is_active=True, add_to_group=False, set_setting=False)
|
||||
admin.is_staff = True
|
||||
admin.is_superuser = True
|
||||
admin.save()
|
||||
else:
|
||||
admin = User.objects.get(email='admin@lorsba.ch')
|
||||
admin = User.objects.get(email='admin@envipath.com')
|
||||
|
||||
# System Group
|
||||
g = GroupManager.create_group(admin, 'enviPath Users', 'All enviPath Users')
|
||||
g.public = True
|
||||
g.save()
|
||||
@ -40,25 +43,25 @@ class Command(BaseCommand):
|
||||
admin.default_group = g
|
||||
admin.save()
|
||||
|
||||
if not User.objects.filter(email='jebus@lorsba.ch').exists():
|
||||
jebus = UserManager.create_user("jebus", "jebus@lorsba.ch", "SuperSafe", is_active=True, add_to_group=False,
|
||||
set_setting=False)
|
||||
jebus.is_staff = True
|
||||
jebus.is_superuser = True
|
||||
jebus.save()
|
||||
if not User.objects.filter(email='user0@envipath.com').exists():
|
||||
user0 = UserManager.create_user("user0", "user0@envipath.com", "SuperSafe",
|
||||
is_active=True, add_to_group=False, set_setting=False)
|
||||
user0.is_staff = True
|
||||
user0.is_superuser = True
|
||||
user0.save()
|
||||
else:
|
||||
jebus = User.objects.get(email='jebus@lorsba.ch')
|
||||
user0 = User.objects.get(email='user0@envipath.com')
|
||||
|
||||
g.user_member.add(jebus)
|
||||
g.user_member.add(user0)
|
||||
g.save()
|
||||
|
||||
jebus.default_group = g
|
||||
jebus.save()
|
||||
user0.default_group = g
|
||||
user0.save()
|
||||
|
||||
return anon, admin, g, jebus
|
||||
return anon, admin, g, user0
|
||||
|
||||
def import_package(self, data, owner):
|
||||
return PackageManager.import_package(data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True)
|
||||
return PackageManager.import_legacy_package(data, owner, keep_ids=True, add_import_timestamp=False, trust_reviewed=True)
|
||||
|
||||
def create_default_setting(self, owner, packages):
|
||||
s = SettingManager.create_setting(
|
||||
@ -108,13 +111,6 @@ class Command(BaseCommand):
|
||||
'base_url': 'https://www.rhea-db.org',
|
||||
'url_pattern': 'https://www.rhea-db.org/rhea/{id}'
|
||||
},
|
||||
{
|
||||
'name': 'CAS',
|
||||
'full_name': 'Chemical Abstracts Service Registry',
|
||||
'description': 'Registry of chemical substances',
|
||||
'base_url': 'https://www.cas.org',
|
||||
'url_pattern': None # CAS doesn't have a free public URL pattern
|
||||
},
|
||||
{
|
||||
'name': 'KEGG Reaction',
|
||||
'full_name': 'KEGG Reaction Database',
|
||||
@ -122,13 +118,6 @@ class Command(BaseCommand):
|
||||
'base_url': 'https://www.genome.jp',
|
||||
'url_pattern': 'https://www.genome.jp/entry/reaction+{id}'
|
||||
},
|
||||
{
|
||||
'name': 'MetaCyc',
|
||||
'full_name': 'MetaCyc Metabolic Pathway Database',
|
||||
'description': 'Database of metabolic pathways and enzymes',
|
||||
'base_url': 'https://metacyc.org',
|
||||
'url_pattern': None
|
||||
},
|
||||
{
|
||||
'name': 'UniProt',
|
||||
'full_name': 'MetaCyc Metabolic Pathway Database',
|
||||
@ -147,7 +136,9 @@ class Command(BaseCommand):
|
||||
@transaction.atomic
|
||||
def handle(self, *args, **options):
|
||||
# Create users
|
||||
anon, admin, g, jebus = self.create_users()
|
||||
anon, admin, g, user0 = self.create_users()
|
||||
|
||||
self.populate_common_external_databases()
|
||||
|
||||
# Import Packages
|
||||
packages = [
|
||||
@ -169,7 +160,7 @@ class Command(BaseCommand):
|
||||
setting.save()
|
||||
setting.make_global_default()
|
||||
|
||||
for u in [anon, jebus]:
|
||||
for u in [anon, user0]:
|
||||
u.default_setting = setting
|
||||
u.save()
|
||||
|
||||
@ -200,6 +191,6 @@ class Command(BaseCommand):
|
||||
ml_model.build_model()
|
||||
# ml_model.evaluate_model()
|
||||
|
||||
# If available create EnviFormerModel
|
||||
# If available, create EnviFormerModel
|
||||
if s.ENVIFORMER_PRESENT:
|
||||
enviFormer_model = EnviFormer.create(pack, 'EnviFormer - T0.5', 'EnviFormer Model with Threshold 0.5', 0.5)
|
||||
|
||||
@ -24,4 +24,4 @@ class Command(BaseCommand):
|
||||
def handle(self, *args, **options):
|
||||
owner = User.objects.get(username=options['owner'])
|
||||
package_data = json.load(open(options['data']))
|
||||
PackageManager.import_package(package_data, owner)
|
||||
PackageManager.import_legacy_package(package_data, owner)
|
||||
@ -578,32 +578,38 @@ class Package(EnviPathModel):
|
||||
license = models.ForeignKey('epdb.License', on_delete=models.SET_NULL, blank=True, null=True,
|
||||
verbose_name='License')
|
||||
|
||||
def delete(self, *args, **kwargs):
|
||||
# explicitly handle related Rules
|
||||
for r in self.rules.all():
|
||||
r.delete()
|
||||
super().delete(*args, **kwargs)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} (pk={self.pk})"
|
||||
|
||||
@property
|
||||
def compounds(self):
|
||||
return Compound.objects.filter(package=self)
|
||||
return self.compound_set.all()
|
||||
|
||||
@property
|
||||
def rules(self):
|
||||
return Rule.objects.filter(package=self)
|
||||
return self.rule_set.all()
|
||||
|
||||
@property
|
||||
def reactions(self):
|
||||
return Reaction.objects.filter(package=self)
|
||||
return self.reaction_set.all()
|
||||
|
||||
@property
|
||||
def pathways(self) -> 'Pathway':
|
||||
return Pathway.objects.filter(package=self)
|
||||
return self.pathway_set.all()
|
||||
|
||||
@property
|
||||
def scenarios(self):
|
||||
return Scenario.objects.filter(package=self)
|
||||
return self.scenario_set.all()
|
||||
|
||||
@property
|
||||
def models(self):
|
||||
return EPModel.objects.filter(package=self)
|
||||
return self.epmodel_set.all()
|
||||
|
||||
def _url(self):
|
||||
return '{}/package/{}'.format(s.SERVER_URL, self.uuid)
|
||||
@ -911,7 +917,6 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin, ChemicalIdenti
|
||||
class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin):
|
||||
package = models.ForeignKey('epdb.Package', verbose_name='Package', on_delete=models.CASCADE, db_index=True)
|
||||
|
||||
# I think this only affects Django Admin which we are barely using
|
||||
# # https://github.com/django-polymorphic/django-polymorphic/issues/229
|
||||
# _non_polymorphic = models.Manager()
|
||||
#
|
||||
@ -1128,6 +1133,7 @@ class ParallelRule(Rule):
|
||||
return res
|
||||
|
||||
|
||||
|
||||
class SequentialRule(Rule):
|
||||
simple_rules = models.ManyToManyField('epdb.SimpleRule', verbose_name='Simple rules',
|
||||
through='SequentialRuleOrdering')
|
||||
@ -1959,7 +1965,7 @@ class RuleBasedRelativeReasoning(PackageBasedModel):
|
||||
rbrr.package = package
|
||||
|
||||
if name is None or name.strip() == '':
|
||||
name = f"MLRelativeReasoning {RuleBasedRelativeReasoning.objects.filter(package=package).count() + 1}"
|
||||
name = f"RuleBasedRelativeReasoning {RuleBasedRelativeReasoning.objects.filter(package=package).count() + 1}"
|
||||
|
||||
rbrr.name = name
|
||||
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
|
||||
from django.conf import settings as s
|
||||
from django.db import transaction
|
||||
from django.db.models.signals import pre_delete
|
||||
from django.db.models.signals import pre_delete, post_delete
|
||||
from django.dispatch import receiver
|
||||
|
||||
from epdb.models import Node, Edge
|
||||
from epdb.models import Node, Edge, EPModel
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@receiver(pre_delete, sender=Node)
|
||||
@ -18,3 +25,14 @@ def delete_orphan_edges(sender, instance, **kwargs):
|
||||
# check if the node that is about to be deleted is the only start node
|
||||
if edge.end_nodes.count() == 1:
|
||||
edge.delete()
|
||||
|
||||
|
||||
@receiver(post_delete, sender=EPModel)
|
||||
def delete_epmodel_files(sender, instance, **kwargs):
|
||||
# Delete the files on disk for the deleted model
|
||||
mod_uuid = str(instance.uuid)
|
||||
|
||||
for f in os.listdir(s.MODEL_DIR):
|
||||
if f.startswith(mod_uuid):
|
||||
logger.info(f"Deleting {os.path.join(s.MODEL_DIR, f)}")
|
||||
shutil.rmtree(os.path.join(s.MODEL_DIR, f))
|
||||
|
||||
@ -284,18 +284,21 @@ def packages(request):
|
||||
|
||||
if hidden := request.POST.get('hidden', None):
|
||||
|
||||
if hidden == 'import-legacy-package-json':
|
||||
if hidden in ['import-legacy-package-json', 'import-package-json']:
|
||||
f = request.FILES['file']
|
||||
|
||||
try:
|
||||
file_data = f.read().decode("utf-8")
|
||||
data = json.loads(file_data)
|
||||
|
||||
pack = PackageManager.import_package(data, current_user)
|
||||
if hidden == 'import-legacy-package-json':
|
||||
pack = PackageManager.import_legacy_package(data, current_user)
|
||||
else:
|
||||
pack = PackageManager.import_pacakge(data, current_user)
|
||||
|
||||
return redirect(pack.url)
|
||||
except UnicodeDecodeError:
|
||||
return error(request, 'Invalid encoding.', f'Invalid encoding, must be UTF-8')
|
||||
|
||||
else:
|
||||
return HttpResponseBadRequest()
|
||||
else:
|
||||
@ -799,6 +802,15 @@ def package(request, package_uuid):
|
||||
|
||||
if request.method == 'GET':
|
||||
|
||||
if request.GET.get("export", False) == "true":
|
||||
filename = f"{current_package.name.replace(' ', '_')}_{current_package.uuid}.json"
|
||||
pack_json = PackageManager.export_package(current_package, include_models=False,
|
||||
include_external_identifiers=False)
|
||||
response = JsonResponse(pack_json, content_type='application/json')
|
||||
response['Content-Disposition'] = f'attachment; filename="{filename}"'
|
||||
|
||||
return response
|
||||
|
||||
context = get_base_context(request)
|
||||
context['title'] = f'enviPath - {current_package.name}'
|
||||
|
||||
|
||||
Reference in New Issue
Block a user