diff --git a/envipath/settings.py b/envipath/settings.py index ebdd3622..2ca661de 100644 --- a/envipath/settings.py +++ b/envipath/settings.py @@ -15,6 +15,7 @@ from pathlib import Path from dotenv import load_dotenv from envipy_plugins import Classifier, Property, Descriptor from sklearn.ensemble import RandomForestClassifier +from sklearn.tree import DecisionTreeClassifier # Build paths inside the project like this: BASE_DIR / 'subdir'. BASE_DIR = Path(__file__).resolve().parent.parent @@ -256,14 +257,29 @@ CELERY_RESULT_BACKEND = 'redis://localhost:6379/1' CELERY_ACCEPT_CONTENT = ['json'] CELERY_TASK_SERIALIZER = 'json' -DEFAULT_MODELS_PARAMS = { - 'base_clf': RandomForestClassifier(n_estimators=100, - max_features='log2', - random_state=42, - criterion='entropy', - ccp_alpha=0.0, - max_depth=3, - min_samples_leaf=1), +DEFAULT_RF_MODEL_PARAMS = { + 'base_clf': RandomForestClassifier( + n_estimators=100, + max_features='log2', + random_state=42, + criterion='entropy', + ccp_alpha=0.0, + max_depth=3, + min_samples_leaf=1 + ), + 'num_chains': 10, +} + +DEFAULT_DT_MODEL_PARAMS = { + 'base_clf': DecisionTreeClassifier( + criterion='entropy', + max_depth=3, + min_samples_split=5, + min_samples_leaf=5, + max_features='sqrt', + class_weight='balanced', + random_state=42 + ), 'num_chains': 10, } diff --git a/epdb/logic.py b/epdb/logic.py index c4c541e6..6d9397ac 100644 --- a/epdb/logic.py +++ b/epdb/logic.py @@ -7,13 +7,21 @@ from django.db import transaction from django.conf import settings as s from epdb.models import User, Package, UserPackagePermission, GroupPackagePermission, Permission, Group, Setting, \ - EPModel, UserSettingPermission, Rule, Pathway, Node, Edge + EPModel, UserSettingPermission, Rule, Pathway, Node, Edge, Compound, Reaction, CompoundStructure +from utilities.chem import FormatConverter logger = logging.getLogger(__name__) + 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}") + @staticmethod - def create_user(username, email, password, *args, **kwargs): + def is_user_url(url: str): + return bool(re.findall(UserManager.user_pattern, url)) + + @staticmethod + def create_user(username, email, password, set_setting=True, add_to_group=True, *args, **kwargs): # avoid circular import :S from .tasks import send_registration_mail @@ -34,6 +42,17 @@ class UserManager(object): # send email for verification send_registration_mail.delay(u.pk) + if set_setting: + u.default_setting = Setting.objects.get(global_default=True) + u.save() + + if add_to_group: + g = Group.objects.get(public=True, name='enviPath Users') + g.user_member.add(u) + g.save() + u.default_group = g + u.save() + return u @staticmethod @@ -54,7 +73,16 @@ class UserManager(object): uuid = user_url.strip().split('/')[-1] return get_user_model().objects.get(uuid=uuid) + @staticmethod + def writable(current_user, user): + return (current_user == user) or user.is_superuser + class GroupManager(object): + group_pattern = re.compile(r".*/group/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}") + + @staticmethod + def is_group_url(url: str): + return bool(re.findall(GroupManager.group_pattern, url)) @staticmethod def create_group(current_user, name, description): @@ -110,9 +138,17 @@ class GroupManager(object): group.save() + @staticmethod + def writable(user, group): + return (user == group.owner) or user.is_superuser + class PackageManager(object): - package_pattern = re.compile(r".*/package/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$") + package_pattern = re.compile(r".*/package/[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}") + + @staticmethod + def is_package_url(url: str): + return bool(re.findall(PackageManager.package_pattern, url)) @staticmethod def get_reviewed_packages(): @@ -120,7 +156,6 @@ class PackageManager(object): @staticmethod def readable(user, package): - # TODO Owner! if UserPackagePermission.objects.filter(package=package, user=user).exists() or \ GroupPackagePermission.objects.filter(package=package, group__in=GroupManager.get_groups(user)) or \ package.reviewed is True or \ @@ -131,14 +166,21 @@ class PackageManager(object): @staticmethod def writable(user, package): - # TODO Owner! - if UserPackagePermission.objects.filter(package=package, user=user, permission=Permission.WRITE).exists() or \ - GroupPackagePermission.objects.filter(package=package, group__in=GroupManager.get_groups(user), - permission=Permission.WRITE) or \ + if UserPackagePermission.objects.filter(package=package, user=user, permission=Permission.WRITE[0]).exists() or \ + GroupPackagePermission.objects.filter(package=package, group__in=GroupManager.get_groups(user), permission=Permission.WRITE[0]).exists() or \ + UserPackagePermission.objects.filter(package=package, user=user, permission=Permission.ALL[0]).exists() or \ user.is_superuser: return True return False + @staticmethod + def get_package_lp(package_url): + match = re.findall(PackageManager.package_pattern, package_url) + if match: + package_id = match[0].split('/')[-1] + return Package.objects.get(uuid=package_id) + return None + @staticmethod def get_package_by_url(user, package_url): match = re.findall(PackageManager.package_pattern, package_url) @@ -229,8 +271,12 @@ class PackageManager(object): @staticmethod @transaction.atomic def update_permissions(caller: User, package: Package, grantee: Union[User, Group], new_perm: Optional[str]): - if not PackageManager.writable(caller, package): - raise ValueError(f"User {caller} is not allowed to modify permissions on {package}") + caller_perm = None + if not caller.is_superuser: + caller_perm = UserPackagePermission.objects.get(user=caller, package=package).permission + + if caller_perm != Permission.ALL[0] and not caller.is_superuser: + raise ValueError(f"Only owner are allowed to modify permissions") data = { 'package': package, @@ -629,8 +675,6 @@ class PackageManager(object): return pack - - 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}$") @@ -648,7 +692,8 @@ class SettingManager(object): def get_setting_by_id(user, setting_id): s = Setting.objects.get(uuid=setting_id) - if s.global_default or s.public or s.owner == user or user.is_superuser: + if s.global_default or s.public or user.is_superuser or \ + UserSettingPermission.objects.filter(user=user, setting=s).exists(): return s raise ValueError( @@ -697,6 +742,116 @@ class SettingManager(object): def set_default_setting(user: User, setting: Setting): pass +class SearchManager(object): + + + @staticmethod + def search(packages: Union[Package, List[Package]], searchterm: str, mode: str): + match mode: + case 'text': + return SearchManager._search_text(packages, searchterm) + case 'default': + return SearchManager._search_default_smiles(packages, searchterm) + case 'exact': + return SearchManager._search_exact_smiles(packages, searchterm) + case 'canonical': + return SearchManager._search_canonical_smiles(packages, searchterm) + case 'inchikey': + return SearchManager._search_inchikey(packages, searchterm) + case _: + raise ValueError(f"Unknown search mode {mode}!") + + @staticmethod + def _search_inchikey(packages: Union[Package, List[Package]], searchterm: str): + from django.db.models import Q + + search_cond = Q(inchikey=searchterm) + compound_qs = Compound.objects.filter(Q(package__in=packages) & Q(compoundstructure__inchikey=searchterm)).distinct() + compound_structure_qs = CompoundStructure.objects.filter(Q(compound__package__in=packages) & search_cond) + reactions_qs = Reaction.objects.filter(Q(package__in=packages) & (Q(educts__inchikey=searchterm) | Q(products__inchikey=searchterm))).distinct() + pathway_qs = Pathway.objects.filter(Q(package__in=packages) & (Q(edge__edge_label__educts__inchikey=searchterm) | Q(edge__edge_label__products__inchikey=searchterm))).distinct() + + return { + 'Compounds': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_qs], + 'Compound Structures': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_structure_qs], + 'Reactions': [{'name': r.name, 'description': r.description, 'url': r.url} for r in reactions_qs], + 'Pathways': [{'name': p.name, 'description': p.description, 'url': p.url} for p in pathway_qs], + } + + @staticmethod + def _search_exact_smiles(packages: Union[Package, List[Package]], searchterm: str): + from django.db.models import Q + + search_cond = Q(smiles=searchterm) + compound_qs = Compound.objects.filter(Q(package__in=packages) & Q(compoundstructure__smiles=searchterm)).distinct() + compound_structure_qs = CompoundStructure.objects.filter(Q(compound__package__in=packages) & search_cond) + reactions_qs = Reaction.objects.filter(Q(package__in=packages) & (Q(educts__smiles=searchterm) | Q(products__smiles=searchterm))).distinct() + pathway_qs = Pathway.objects.filter(Q(package__in=packages) & (Q(edge__edge_label__educts__smiles=searchterm) | Q(edge__edge_label__products__smiles=searchterm))).distinct() + + return { + 'Compounds': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_qs], + 'Compound Structures': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_structure_qs], + 'Reactions': [{'name': r.name, 'description': r.description, 'url': r.url} for r in reactions_qs], + 'Pathways': [{'name': p.name, 'description': p.description, 'url': p.url} for p in pathway_qs], + } + + @staticmethod + def _search_default_smiles(packages: Union[Package, List[Package]], searchterm: str): + from django.db.models import Q + + inchi_front = FormatConverter.InChIKey(searchterm)[:14] + + search_cond = Q(inchikey__startswith=inchi_front) + compound_qs = Compound.objects.filter(Q(package__in=packages) & Q(compoundstructure__inchikey__startswith=inchi_front)).distinct() + compound_structure_qs = CompoundStructure.objects.filter(Q(compound__package__in=packages) & search_cond) + reactions_qs = Reaction.objects.filter(Q(package__in=packages) & (Q(educts__inchikey__startswith=inchi_front) | Q(products__inchikey__startswith=inchi_front))).distinct() + pathway_qs = Pathway.objects.filter(Q(package__in=packages) & (Q(edge__edge_label__educts__inchikey__startswith=inchi_front) | Q(edge__edge_label__products__inchikey__startswith=inchi_front))).distinct() + + return { + 'Compounds': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_qs], + 'Compound Structures': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_structure_qs], + 'Reactions': [{'name': r.name, 'description': r.description, 'url': r.url} for r in reactions_qs], + 'Pathways': [{'name': p.name, 'description': p.description, 'url': p.url} for p in pathway_qs], + } + + @staticmethod + def _search_canonical_smiles(packages: Union[Package, List[Package]], searchterm: str): + from django.db.models import Q + + search_cond = Q(canonical_smiles=searchterm) + compound_qs = Compound.objects.filter(Q(package__in=packages) & Q(compoundstructure__canonical_smiles=searchterm)).distinct() + compound_structure_qs = CompoundStructure.objects.filter(Q(compound__package__in=packages) & search_cond) + reactions_qs = Reaction.objects.filter(Q(package__in=packages) & (Q(educts__canonical_smiles=searchterm) | Q(products__canonical_smiles=searchterm))).distinct() + pathway_qs = Pathway.objects.filter(Q(package__in=packages) & (Q(edge__edge_label__educts__canonical_smiles=searchterm) | Q(edge__edge_label__products__canonical_smiles=searchterm))).distinct() + + return { + 'Compounds': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_qs], + 'Compound Structures': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_structure_qs], + 'Reactions': [{'name': r.name, 'description': r.description, 'url': r.url} for r in reactions_qs], + 'Pathways': [{'name': p.name, 'description': p.description, 'url': p.url} for p in pathway_qs], + } + + @staticmethod + def _search_text(packages: Union[Package, List[Package]], searchterm: str): + from django.db.models import Q + + search_cond = (Q(name__icontains=searchterm) | Q(description__icontains=searchterm)) + cond = Q(package__in=packages) & search_cond + compound_qs = Compound.objects.filter(cond) + compound_structure_qs = CompoundStructure.objects.filter(Q(compound__package__in=packages) & search_cond) + rule_qs = Rule.objects.filter(cond) + reactions_qs = Reaction.objects.filter(cond) + pathway_qs = Pathway.objects.filter(cond) + + res = { + 'Compounds': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_qs], + 'Compound Structures': [{'name': c.name, 'description': c.description, 'url': c.url} for c in compound_structure_qs], + 'Rules': [{'name': r.name, 'description': r.description, 'url': r.url} for r in rule_qs], + 'Reactions': [{'name': r.name, 'description': r.description, 'url': r.url} for r in reactions_qs], + 'Pathways': [{'name': p.name, 'description': p.description, 'url': p.url} for p in pathway_qs], + } + return res + class SNode(object): @@ -719,7 +874,7 @@ class SNode(object): class SEdge(object): def __init__(self, educts: Union[SNode, List[SNode]], products: Union[SNode | List[SNode]], - rule: Optional['Rule'] = None): + rule: Optional['Rule'] = None, probability: Optional[float] = None): if not isinstance(educts, list): educts = [educts] @@ -727,6 +882,7 @@ class SEdge(object): self.educts = educts self.products = products self.rule = rule + self.probability = probability def __hash__(self): full_hash = 0 @@ -799,11 +955,45 @@ class SPathway(object): elif isinstance(n, SNode): self.root_nodes.append(n) - self.queue = list() self.smiles_to_node: Dict[str, SNode] = dict(**{n.smiles: n for n in self.root_nodes}) self.edges: Set['SEdge'] = set() self.done = False + @staticmethod + def from_pathway(pw: 'Pathway', persist: bool = True): + """ Initializes a SPathway with a state given by a Pathway """ + spw = SPathway(root_nodes=pw.root_nodes, persist=pw if persist else None, prediction_setting=pw.setting) + # root_nodes are already added in __init__, add remaining nodes + for n in pw.nodes: + snode = SNode(n.default_node_label.smiles, n.depth) + if snode.smiles not in spw.smiles_to_node: + spw.smiles_to_node[snode.smiles] = snode + spw.snode_persist_lookup[snode] = n + + for e in pw.edges: + sub = [] + prod = [] + + for n in e.start_nodes.all(): + sub.append(spw.smiles_to_node[n.default_node_label.smiles]) + + for n in e.end_nodes.all(): + prod.append(spw.smiles_to_node[n.default_node_label.smiles]) + + rule = None + if e.edge_label.rules.all(): + rule = e.edge_label.rules.all().first() + + prob = None + if e.kv.get('probability'): + prob = float(e.kv['probability']) + + sedge = SEdge(sub, prod, rule=rule, probability=prob) + spw.edges.add(sedge) + spw.sedge_persist_lookup[sedge] = e + + return spw + def num_nodes(self): return len(self.smiles_to_node.keys()) @@ -830,8 +1020,18 @@ class SPathway(object): return sorted(res, key=lambda x: hash(x)) - def predict_step(self, from_depth: int = 0): - substrates = self._get_nodes_for_depth(from_depth) + def predict_step(self, from_depth: int = None, from_node: 'Node' = None): + substrates: List[SNode] = [] + + if from_depth is not None: + substrates = self._get_nodes_for_depth(from_depth) + elif from_node is not None: + for k,v in self.snode_persist_lookup.items(): + if from_node == v: + substrates = [k] + break + else: + raise ValueError("Neither from_depth nor from_node_url specified") new_tp = False if substrates: @@ -849,13 +1049,19 @@ class SPathway(object): node = self.smiles_to_node[c] cand_nodes.append(node) - edge = SEdge(sub, cand_nodes, cand_set.rule) + edge = SEdge(sub, cand_nodes, rule=cand_set.rule, probability=cand_set.probability) self.edges.add(edge) - else: + + # In case no substrates are found, we're done. + # For "predict from node" we're always done + if len(substrates) == 0 or from_node is not None: self.done = True + # Check if we need to write back data to database if new_tp and self.persist: self._sync_to_pathway() + # call save to update internal modified field + self.persist.save() def _sync_to_pathway(self): logger.info("Updating Pathway with SPathway") @@ -876,6 +1082,11 @@ class SPathway(object): product_nodes.append(self.snode_persist_lookup[snode]) e = Edge.create(self.persist, educt_nodes, product_nodes, sedge.rule) + + if sedge.probability: + e.kv.update({'probability': sedge.probability}) + e.save() + self.sedge_persist_lookup[sedge] = e logger.info("Update done!") diff --git a/epdb/management/commands/bootstrap.py b/epdb/management/commands/bootstrap.py index b28e6620..6ca3e18e 100644 --- a/epdb/management/commands/bootstrap.py +++ b/epdb/management/commands/bootstrap.py @@ -13,12 +13,14 @@ 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) + anon = UserManager.create_user("anonymous", "anon@lorsba.ch", "SuperSafe", is_active=True, + add_to_group=False, set_setting=False) else: anon = User.objects.get(email='anon@lorsba.ch') if not User.objects.filter(email='admin@lorsba.ch').exists(): - admin = UserManager.create_user("admin", "admin@lorsba.ch", "SuperSafe", is_active=True) + admin = UserManager.create_user("admin", "admin@lorsba.ch", "SuperSafe", is_active=True, add_to_group=False, + set_setting=False) admin.is_staff = True admin.is_superuser = True admin.save() @@ -26,6 +28,9 @@ class Command(BaseCommand): admin = User.objects.get(email='admin@lorsba.ch') g = GroupManager.create_group(admin, 'enviPath Users', 'All enviPath Users') + g.public = True + g.save() + g.user_member.add(anon) g.save() @@ -36,7 +41,8 @@ class Command(BaseCommand): admin.save() if not User.objects.filter(email='jebus@lorsba.ch').exists(): - jebus = UserManager.create_user("jebus", "jebus@lorsba.ch", "SuperSafe", is_active=True) + 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() @@ -94,6 +100,9 @@ class Command(BaseCommand): setting.make_global_default() for u in [anon, jebus]: + u.default_setting = setting + u.save() + usp = UserSettingPermission() usp.user = u usp.setting = setting @@ -119,7 +128,7 @@ class Command(BaseCommand): X, y = ml_model.build_dataset() ml_model.build_model(X, y) - ml_model.evaluate_model() + # ml_model.evaluate_model() # If available create EnviFormerModel if s.ENVIFORMER_PRESENT: diff --git a/epdb/models.py b/epdb/models.py index c19de2ba..41546ff7 100644 --- a/epdb/models.py +++ b/epdb/models.py @@ -14,7 +14,7 @@ from django.contrib.auth.hashers import make_password, check_password from django.contrib.auth.models import AbstractUser from django.contrib.postgres.fields import ArrayField from django.db import models, transaction -from django.db.models import JSONField +from django.db.models import JSONField, Count, Q from django.utils import timezone from django.utils.functional import cached_property from model_utils.models import TimeStampedModel @@ -43,8 +43,6 @@ class User(AbstractUser): on_delete=models.SET_NULL, related_name='default_group') default_setting = models.ForeignKey('epdb.Setting', on_delete=models.SET_NULL, verbose_name='The users default settings', null=True, blank=False) - # TODO remove - groups = models.ManyToManyField("Group", verbose_name='groups') USERNAME_FIELD = "email" REQUIRED_FIELDS = ['username'] @@ -87,6 +85,7 @@ class Group(TimeStampedModel): uuid = models.UUIDField(null=False, blank=False, verbose_name='UUID of this object', unique=True, default=uuid4) name = models.TextField(blank=False, null=False, verbose_name='Group name') owner = models.ForeignKey("User", verbose_name='Group Owner', on_delete=models.CASCADE) + public = models.BooleanField(verbose_name='Public Group', default=False) description = models.TextField(blank=False, null=False, verbose_name='Descriptions', default='no description') user_member = models.ManyToManyField("User", verbose_name='User members', related_name='users_in_group') group_member = models.ManyToManyField("Group", verbose_name='Group member', related_name='groups_in_group') @@ -314,7 +313,7 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin): @transaction.atomic def create(package: Package, smiles: str, name: str = None, description: str = None, *args, **kwargs) -> 'Compound': - if smiles is None or smiles == '': + if smiles is None or smiles.strip() == '': raise ValueError('SMILES is required') smiles = smiles.strip() @@ -338,12 +337,14 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin): c = Compound() c.package = package - # For name and description we have defaults so only set them if they carry a real value - if name is not None and name != '': - c.name = name + if name is None or name.strip() == '': + name = f"Compound {Compound.objects.filter(package=package).count() + 1}" - if description is not None and description != '': - c.description = description + c.name = name + + # We have a default here only set the value if it carries some payload + if description is not None and description.strip() != '': + c.description = description.strip() c.save() @@ -403,25 +404,27 @@ class Compound(EnviPathModel, AliasMixin, ScenarioMixin): class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin): compound = models.ForeignKey('epdb.Compound', on_delete=models.CASCADE, db_index=True) smiles = models.TextField(blank=False, null=False, verbose_name='SMILES') + canonical_smiles = models.TextField(blank=False, null=False, verbose_name='Canonical SMILES') + inchikey = models.TextField(max_length=27, blank=False, null=False, verbose_name="InChIKey") normalized_structure = models.BooleanField(null=False, blank=False, default=False) + def save(self, *args, **kwargs): + # Compute these fields only on initial save call + if self.pk is None: + try: + # Generate canonical SMILES + self.canonical_smiles = FormatConverter.canonicalize(self.smiles) + # Generate InChIKey + self.inchikey = FormatConverter.InChIKey(self.smiles) + except Exception as e: + logger.error(f"Could compute canonical SMILES/InChIKey from {self.smiles}, error: {e}") + + super().save(*args, **kwargs) + @property def url(self): return '{}/structure/{}'.format(self.compound.url, self.uuid) - # @property - # def related_pathways(self): - # pathways = Node.objects.filter(node_labels__in=[self]).values_list('pathway', flat=True) - # return Pathway.objects.filter(package=self.compound.package, id__in=set(pathways)).order_by('name') - - # @property - # def related_reactions(self): - # return ( - # Reaction.objects.filter(package=self.compound.package, educts__in=[self]) - # | - # Reaction.objects.filter(package=self.compound.package, products__in=[self]) - # ).order_by('name') - @staticmethod @transaction.atomic def create(compound: Compound, smiles: str, name: str = None, description: str = None, *args, **kwargs): @@ -448,19 +451,9 @@ class CompoundStructure(EnviPathModel, AliasMixin, ScenarioMixin): return cs - # TODO add find method - @property - def InChIKey(self): - return FormatConverter.InChIKey(self.smiles) - - @property - def canonical_smiles(self): - return FormatConverter.canonicalize(self.smiles) - - @property - def as_svg(self): - return IndigoUtils.mol_to_svg(self.smiles) + def as_svg(self, width: int = 800, height: int = 400): + return IndigoUtils.mol_to_svg(self.smiles, width=width, height=height) class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin): @@ -492,19 +485,9 @@ class Rule(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin): @staticmethod @transaction.atomic - def create(package: Package, rule_type: str, name: str = None, description: str = None, *args, **kwargs): - r = Rule.cls_for_type(rule_type)() - r.package = package - r.name = name - r.description = description - - # As we are setting params this way the "k" has to match the property name - for k, v in kwargs.items(): - setattr(r, k, v) - - r.save() - return r - + def create(rule_type: str, *args, **kwargs): + cls = Rule.cls_for_type(rule_type) + return cls.create(*args, **kwargs) # # @property @@ -533,6 +516,54 @@ class SimpleAmbitRule(SimpleRule): reactant_filter_smarts = models.TextField(null=True, verbose_name='Reactant Filter SMARTS') product_filter_smarts = models.TextField(null=True, verbose_name='Product Filter SMARTS') + @staticmethod + @transaction.atomic + def create(package: Package, name: str = None, description: str = None, smirks: str = None, + reactant_filter_smarts: str = None, product_filter_smarts: str = None): + + if smirks is None or smirks.strip() == '': + raise ValueError('SMIRKS is required!') + + smirks = smirks.strip() + + if not FormatConverter.is_valid_smirks(smirks): + raise ValueError(f'SMIRKS "{smirks}" is invalid!') + + query = SimpleAmbitRule.objects.filter(package=package, smirks=smirks) + + if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != '': + query = query.filter(reactant_filter_smarts=reactant_filter_smarts) + + if product_filter_smarts is not None and product_filter_smarts.strip() != '': + query = query.filter(product_filter_smarts=product_filter_smarts) + + if query.exists(): + if query.count() > 1: + logger.error(f'More than one rule matched this one! {query}') + return query.first() + + r = SimpleAmbitRule() + r.package = package + + if name is None or name.strip() == '': + name = f'Rule {Rule.objects.filter(package=package).count() + 1}' + + r.name = name + + if description is not None and description.strip() != '': + r.description = description + + r.smirks = smirks + + if reactant_filter_smarts is not None and reactant_filter_smarts.strip() != '': + r.reactant_filter_smarts = reactant_filter_smarts + + if product_filter_smarts is not None and product_filter_smarts.strip() != '': + r.product_filter_smarts = product_filter_smarts + + r.save() + return r + @property def url(self): return '{}/simple-ambit-rule/{}'.format(self.package.url, self.uuid) @@ -642,7 +673,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin): def create(package: Package, name: str = None, description: str = None, educts: Union[List[str], List[CompoundStructure]] = None, products: Union[List[str], List[CompoundStructure]] = None, - rule: Rule = None, multi_step: bool = True): + rules: Union[Rule|List[Rule]] = None, multi_step: bool = True): _educts = [] _products = [] @@ -662,18 +693,61 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin): _products += products else: - raise ValueError("") + raise ValueError("Found mixed types for educts and/or products!") + + if len(_educts) == 0 or len(_products) == 0: + raise ValueError("No educts or products specified!") + + if rules is None: + rules = [] + + if isinstance(rules, Rule): + rules = [rules] + + + query = Reaction.objects.annotate( + educt_count=Count('educts', filter=Q(educts__in=_educts), distinct=True), + product_count=Count('products', filter=Q(products__in=_products), distinct=True), + ) + + # The annotate/filter wont work if rules is an empty list + if rules: + query = query.annotate( + rule_count=Count('rules', filter=Q(rules__in=rules), distinct=True) + ).filter(rule_count=len(rules)) + else: + query = query.annotate( + rule_count=Count('rules', distinct=True) + ).filter(rule_count=0) + + existing_reaction_qs = query.filter( + educt_count=len(_educts), + product_count=len(_products), + multi_step=multi_step, + package=package + ) + + if existing_reaction_qs.exists(): + if existing_reaction_qs.count() > 1: + logger.error(f'Found more than one reaction for given input! {existing_reaction_qs}') + return existing_reaction_qs.first() r = Reaction() r.package = package - r.name = name - r.description = description + + if name is not None and name.strip() != '': + r.name = name + + if description is not None and name.strip() != '': + r.description = description + r.multi_step = multi_step r.save() - if rule: - r.rules.add(rule) + if rules: + for rule in rules: + r.rules.add(rule) for educt in _educts: r.educts.add(educt) @@ -700,6 +774,7 @@ class Reaction(EnviPathModel, AliasMixin, ScenarioMixin): class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): package = models.ForeignKey('epdb.Package', verbose_name='Package', on_delete=models.CASCADE, db_index=True) + setting = models.ForeignKey('epdb.Setting', verbose_name='Setting', on_delete=models.CASCADE, null=True, blank=True) @property def root_nodes(self): @@ -709,6 +784,12 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): def nodes(self): return Node.objects.filter(pathway=self) + def get_node(self, node_url): + for n in self.nodes: + if n.url == node_url: + return n + return None + @property def edges(self): return Edge.objects.filter(pathway=self) @@ -717,6 +798,26 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): def url(self): return '{}/pathway/{}'.format(self.package.url, self.uuid) + # Mode + def is_built(self): + return self.kv.get('mode', 'build') == 'predicted' + + def is_predicted(self): + return self.kv.get('mode', 'build') == 'predicted' + + def is_predicted(self): + return self.kv.get('mode', 'build') == 'predicted' + + # Status + def completed(self): + return self.kv.get('status', 'completed') == 'completed' + + def running(self): + return self.kv.get('status', 'completed') == 'running' + + def failed(self): + return self.kv.get('status', 'completed') == 'failed' + def d3_json(self): # Ideally it would be something like this but # to reduce crossing in edges do a DFS @@ -770,7 +871,11 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): new_link = { 'name': link['name'], 'id': link['id'], + 'url': link['url'], + 'image': link['image'], 'reaction': link['reaction'], + 'reaction_probability': link['reaction_probability'], + 'scenarios': link['scenarios'], 'source': node_url_to_idx[link['start_node_urls'][0]], 'target': pseudo_idx } @@ -781,7 +886,11 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): new_link = { 'name': link['name'], 'id': link['id'], + 'url': link['url'], + 'image': link['image'], 'reaction': link['reaction'], + 'reaction_probability': link['reaction_probability'], + 'scenarios': link['scenarios'], 'source': pseudo_idx, 'target': node_url_to_idx[target] } @@ -797,9 +906,9 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): "completed": "true", "description": self.description, "id": self.url, - "isIncremental": False, - "isPredicted": False, - "lastModified": 1447842835894, + "isIncremental": self.kv.get('mode') == 'incremental', + "isPredicted": self.kv.get('mode') == 'predicted', + "lastModified": self.modified.strftime('%Y-%m-%d %H:%M:%S'), "pathwayName": self.name, "reviewStatus": "reviewed" if self.package.reviewed else 'unreviewed', "scenarios": [], @@ -813,18 +922,38 @@ class Pathway(EnviPathModel, AliasMixin, ScenarioMixin): @staticmethod @transaction.atomic - def create(package, name, description, smiles): + def create(package: 'Package', smiles: str, name: Optional[str] = None, description: Optional[str] = None): pw = Pathway() pw.package = package - pw.name = name - pw.description = description - pw.save() - # create root node - Node.create(pw, smiles, 0) + if name is None: + name = f"Pathway {Pathway.objects.filter(package=package).count() + 1}" + + pw.name = name + + if description is not None: + pw.description = description + + pw.save() + try: + # create root node + Node.create(pw, smiles, 0) + except ValueError as e: + # Node creation failed, most likely due to an invalid smiles + # delete this pathway... + pw.delete() + raise e return pw + @transaction.atomic + def add_node(self, smiles: str, name: Optional[str] = None, description: Optional[str] = None): + return Node.create(self, smiles, 0) + + @transaction.atomic + def add_edge(self, start_nodes: List['Node'], end_nodes: List['Node'], rule: Optional['Rule'] = None, + name: Optional[str] = None, description: Optional[str] = None): + return Edge.create(self, start_nodes, end_nodes, rule, name=name, description=description) class Node(EnviPathModel, AliasMixin, ScenarioMixin): pathway = models.ForeignKey('epdb.Pathway', verbose_name='belongs to', on_delete=models.CASCADE, db_index=True) @@ -848,14 +977,14 @@ class Node(EnviPathModel, AliasMixin, ScenarioMixin): "imageSize": 490, # TODO "name": self.default_node_label.name, "smiles": self.default_node_label.smiles, + "scenarios": [{'name': s.name, 'url': s.url} for s in self.scenarios.all()], } @staticmethod - def create(pathway, smiles, depth): - c = Compound.create(pathway.package, smiles) + def create(pathway: 'Pathway', smiles: str, depth: int, name: Optional[str] = None, description: Optional[str] = None): + c = Compound.create(pathway.package, smiles, name=name, description=description) if Node.objects.filter(pathway=pathway, default_node_label=c.default_structure).exists(): - print("found node") return Node.objects.get(pathway=pathway, default_node_label=c.default_structure) n = Node() @@ -886,34 +1015,21 @@ class Edge(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin): return '{}/edge/{}'.format(self.pathway.url, self.uuid) def d3_json(self): - # { - # "ecNumbers": [ - # { - # "ecName": "DDT 2,3-dioxygenase", - # "ecNumber": "1.14.12.-" - # } - # ], - # "id": "https://envipath.org/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1/pathway/3f58e4d4-1c63-4b30-bf31-7ae4b98899fe/edge/ff193e7b-f010-43d4-acb3-45f34d938824", - # "idreaction": "https://envipath.org/package/32de3cf4-e3e6-4168-956e-32fa5ddb0ce1/reaction/e11419cd-6b46-470b-8a06-a08d62281734", - # "multistep": "false", - # "name": "Eawag BBD reaction r0450", - # "pseudo": False, - # "scenarios": [], - # "source": 0, - # "target": 4 - # } - return { 'name': self.name, 'id': self.url, - 'reaction': self.edge_label.url if self.edge_label else None, + 'url': self.url, + 'image': self.url + '?image=svg', + 'reaction': {'name': self.edge_label.name, 'url': self.edge_label.url } if self.edge_label else None, + 'reaction_probability': self.kv.get('probability'), # TODO 'start_node_urls': [x.url for x in self.start_nodes.all()], 'end_node_urls': [x.url for x in self.end_nodes.all()], + "scenarios": [{'name': s.name, 'url': s.url} for s in self.scenarios.all()], } @staticmethod - def create(pathway, start_nodes, end_nodes, rule: Optional[Rule] = None, name: Optional[str] = None, + def create(pathway, start_nodes: List[Node], end_nodes: List[Node], rule: Optional[Rule] = None, name: Optional[str] = None, description: Optional[str] = None): e = Edge() e.pathway = pathway @@ -934,13 +1050,17 @@ class Edge(PolymorphicModel, EnviPathModel, AliasMixin, ScenarioMixin): r = Reaction.create(pathway.package, name=name, description=description, educts=[n.default_node_label for n in e.start_nodes.all()], products=[n.default_node_label for n in e.end_nodes.all()], - rule=rule, multi_step=False + rules=rule, multi_step=False ) e.edge_label = r e.save() return e + @property + def as_svg(self): + return self.edge_label.as_svg if self.edge_label else None + class EPModel(PolymorphicModel, EnviPathModel): package = models.ForeignKey('epdb.Package', verbose_name='Package', on_delete=models.CASCADE, db_index=True) @@ -976,6 +1096,9 @@ class MLRelativeReasoning(EPModel): eval_results = JSONField(null=True, blank=True, default=dict) + def status(self): + return self.PROGRESS_STATUS_CHOICES[self.model_status] + @staticmethod @transaction.atomic def create(package, name, description, rule_packages, data_packages, eval_packages, threshold): @@ -1201,7 +1324,7 @@ class MLRelativeReasoning(EPModel): self.save() mod = SparseLabelECC( - **s.DEFAULT_MODELS_PARAMS + **s.DEFAULT_DT_MODEL_PARAMS ) mod.fit(X, y) @@ -1247,7 +1370,7 @@ class MLRelativeReasoning(EPModel): y_train, y_test = y[train_index], y[test_index] model = SparseLabelECC( - **s.DEFAULT_MODELS_PARAMS + **s.DEFAULT_DT_MODEL_PARAMS ) model.fit(X_train, y_train) @@ -1500,11 +1623,15 @@ class Setting(EnviPathModel): max_nodes = models.IntegerField(null=False, blank=False, verbose_name='Setting Max Number of Nodes', default=30) rule_packages = models.ManyToManyField("Package", verbose_name="Setting Rule Packages", - related_name="setting_rule_packages") + related_name="setting_rule_packages", blank=True) model = models.ForeignKey('EPModel', verbose_name='Setting EPModel', on_delete=models.SET_NULL, null=True, blank=True) model_threshold = models.FloatField(null=True, blank=True, verbose_name='Setting Model Threshold', default=0.25) + @property + def url(self): + return '{}/setting/{}'.format(s.SERVER_URL, self.uuid) + @cached_property def applicable_rules(self): """ diff --git a/epdb/tasks.py b/epdb/tasks.py index e724d259..2dcb5b65 100644 --- a/epdb/tasks.py +++ b/epdb/tasks.py @@ -1,4 +1,6 @@ import logging +from typing import Optional + from celery.signals import worker_process_init from celery import shared_task from epdb.models import Pathway, Node, Edge, EPModel, Setting @@ -40,11 +42,40 @@ def evaluate_model(model_pk: int): @shared_task(queue='predict') -def predict(pw_pk: int, pred_setting_pk: int): +def predict(pw_pk: int, pred_setting_pk: int, limit: Optional[int] = None, node_pk: Optional[int] = None) -> Pathway: pw = Pathway.objects.get(id=pw_pk) setting = Setting.objects.get(id=pred_setting_pk) - spw = SPathway(prediction_setting=setting, persist=pw) - level = 0 - while not spw.done: - spw.predict_step(from_depth=level) - level += 1 + + pw.kv.update(**{'status': 'running'}) + pw.save() + + try: + # regular prediction + if limit is not None: + spw = SPathway(prediction_setting=setting, persist=pw) + level = 0 + while not spw.done: + spw.predict_step(from_depth=level) + level += 1 + + # break in case we are in incremental model + if limit != -1: + if level >= limit: + break + + elif node_pk is not None: + n = Node.objects.get(id=node_pk, pathway=pw) + spw = SPathway.from_pathway(pw) + spw.predict_step(from_node=n) + else: + raise ValueError("Neither limit nor node_pk given!") + + + + except Exception as e: + pw.kv.update({'status': 'failed'}) + pw.save() + raise e + + pw.kv.update(**{'status': 'completed'}) + pw.save() \ No newline at end of file diff --git a/epdb/templatetags/__init__.py b/epdb/templatetags/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/epdb/templatetags/envipytags.py b/epdb/templatetags/envipytags.py new file mode 100644 index 00000000..ce2fa9d3 --- /dev/null +++ b/epdb/templatetags/envipytags.py @@ -0,0 +1,7 @@ +from django import template + +register = template.Library() + +@register.filter +def classname(obj): + return obj.__class__.__name__ \ No newline at end of file diff --git a/epdb/urls.py b/epdb/urls.py index 8430aef0..86839530 100644 --- a/epdb/urls.py +++ b/epdb/urls.py @@ -13,6 +13,8 @@ urlpatterns = [ # Home re_path(r'^$', v.index, name='index'), + # re_path(r'^login', v.login, name='login'), + # Top level urls re_path(r'^package$', v.packages, name='packages'), re_path(r'^compound$', v.compounds, name='compounds'), @@ -53,11 +55,11 @@ urlpatterns = [ re_path(rf'^package/(?P{UUID})/pathway$', v.package_pathways, name='package pathway list'), re_path(rf'^package/(?P{UUID})/pathway/(?P{UUID})$', v.package_pathway, name='package pathway detail'), # Pathway Nodes - # re_path(rf'^package/(?P{UUID})/pathway(?P{UUID})/node$', v.package_pathway_nodes, name='package pathway node list'), + re_path(rf'^package/(?P{UUID})/pathway/(?P{UUID})/node$', v.package_pathway_nodes, name='package pathway node list'), re_path(rf'^package/(?P{UUID})/pathway/(?P{UUID})/node/(?P{UUID})$', v.package_pathway_node, name='package pathway node detail'), # Pathway Edges - # re_path(rf'^package/(?P{UUID})/pathway(?P{UUID})/edge$', v.package_pathway_edges, name='package pathway edge list'), - # re_path(rf'^package/(?P{UUID})/pathway(?P{UUID})/edge/(?P{UUID})$', v.package_pathway_edge, name='package pathway edge detail'), + re_path(rf'^package/(?P{UUID})/pathway/(?P{UUID})/edge$', v.package_pathway_edges, name='package pathway edge list'), + re_path(rf'^package/(?P{UUID})/pathway/(?P{UUID})/edge/(?P{UUID})$', v.package_pathway_edge, name='package pathway edge detail'), # Scenario re_path(rf'^package/(?P{UUID})/scenario$', v.package_scenarios, name='package scenario list'), re_path(rf'^package/(?P{UUID})/scenario/(?P{UUID})$', v.package_scenario, name='package scenario detail'), diff --git a/epdb/views.py b/epdb/views.py index e3a3d327..9973611e 100644 --- a/epdb/views.py +++ b/epdb/views.py @@ -1,5 +1,6 @@ import json import logging +from functools import wraps from typing import List, Dict, Any from django.conf import settings as s @@ -7,29 +8,71 @@ from django.contrib.auth import get_user_model from django.http import JsonResponse, HttpResponse, HttpResponseNotAllowed, HttpResponseBadRequest from django.shortcuts import render, redirect from django.views.decorators.csrf import csrf_exempt +from django.contrib.auth.decorators import login_required from utilities.chem import FormatConverter, IndigoUtils -from .logic import GroupManager, PackageManager, UserManager, SettingManager +from .logic import GroupManager, PackageManager, UserManager, SettingManager, SearchManager from .models import Package, GroupPackagePermission, Group, CompoundStructure, Compound, Reaction, Rule, Pathway, Node, \ EPModel, EnviFormer, MLRelativeReasoning, RuleBaseRelativeReasoning, Scenario, SimpleAmbitRule, APIToken, \ - UserPackagePermission, Permission, License, User + UserPackagePermission, Permission, License, User, Edge logger = logging.getLogger(__name__) def log_post_params(request): - for k, v in request.POST.items(): - logger.debug(f"{k}\t{v}") + if s.DEBUG: + for k, v in request.POST.items(): + logger.debug(f"{k}\t{v}") + + +def catch_exceptions(view_func): + @wraps(view_func) + def _wrapped_view(request, *args, **kwargs): + try: + return view_func(request, *args, **kwargs) + except Exception as e: + # Optionally return JSON or plain HttpResponse + if request.headers.get('Accept') == 'application/json': + return JsonResponse( + {'error': 'Internal server error. Please try again later.'}, + status=500 + ) + else: + return render(request, 'errors/error.html', get_base_context(request)) + return _wrapped_view + + +def editable(request, user): + url = request.build_absolute_uri(request.path) + if PackageManager.is_package_url(url): + _package = PackageManager.get_package_lp(request.build_absolute_uri()) + return PackageManager.writable(user, _package) + elif GroupManager.is_group_url(url): + _group = GroupManager.get_group_lp(request.build_absolute_uri()) + return GroupManager.writable(user, _group) + elif UserManager.is_user_url(url): + _user = UserManager.get_user_lp(request.build_absolute_uri()) + return UserManager.writable(user, _user) + elif url in [s.SERVER_URL, f"{s.SERVER_URL}/", f"{s.SERVER_URL}/package", f"{s.SERVER_URL}/user", + f"{s.SERVER_URL}/group", f"{s.SERVER_URL}/search"]: + return True + else: + print(f"Unknown url: {url}") + return False + def get_base_context(request) -> Dict[str, Any]: current_user = _anonymous_or_real(request) + can_edit = editable(request, current_user) + ctx = { 'title': 'enviPath', 'meta': { 'version': '0.0.1', 'server_url': s.SERVER_URL, 'user': current_user, + 'can_edit': can_edit, 'readable_packages': PackageManager.get_all_readable_packages(current_user, include_reviewed=True), 'writeable_packages': PackageManager.get_all_writeable_packages(current_user), 'available_groups': GroupManager.get_groups(current_user), @@ -65,8 +108,9 @@ def breadcrumbs(first_level_object=None, second_level_namespace=None, second_lev return bread +# @catch_exceptions + def index(request): - current_user = _anonymous_or_real(request) context = get_base_context(request) context['title'] = 'enviPath - Home' context['meta']['current_package'] = context['meta']['user'].default_package @@ -77,6 +121,16 @@ def index(request): return render(request, 'index/index.html', context) +# def login(request): +# current_user = _anonymous_or_real(request) +# if request.method == 'GET': +# context = get_base_context(request) +# context['title'] = 'enviPath' +# return render(request, 'login.html', context) +# else: +# return HttpResponseBadRequest() +# +# @login_required(login_url='/login') def packages(request): current_user = _anonymous_or_real(request) @@ -86,9 +140,10 @@ def packages(request): context['object_type'] = 'package' context['meta']['current_package'] = context['meta']['user'].default_package + context['meta']['can_edit'] = True - reviewed_package_qs = Package.objects.filter(reviewed=True) - unreviewed_package_qs = PackageManager.get_all_readable_packages(current_user) + reviewed_package_qs = Package.objects.filter(reviewed=True).order_by('created') + unreviewed_package_qs = PackageManager.get_all_readable_packages(current_user).order_by('name') context['reviewed_objects'] = reviewed_package_qs context['unreviewed_objects'] = unreviewed_package_qs @@ -103,7 +158,6 @@ def packages(request): else: package_name = request.POST.get('package-name') package_description = request.POST.get('package-description', s.DEFAULT_VALUES['description']) - # group = GroupManager.get_group_by_url(request.user, request.POST.get('package-group')) created_package = PackageManager.create_package(current_user, package_name, package_description) @@ -342,7 +396,23 @@ def models(request): def search(request): + current_user = _anonymous_or_real(request) + if request.method == 'GET': + package_urls = request.GET.getlist('packages') + searchterm = request.GET.get('search') + mode = request.GET.get('mode') + + # add HTTP_ACCEPT check to differentiate between index and ajax call + if 'application/json' in request.META.get('HTTP_ACCEPT') and all([searchterm, mode]): + if package_urls: + packages = [PackageManager.get_package_by_url(current_user, p) for p in package_urls] + else: + packages = PackageManager.get_reviewed_packages() + + search_result = SearchManager.search(packages, searchterm, mode) + return JsonResponse(search_result, safe=False) + context = get_base_context(request) context['title'] = 'enviPath - Search' @@ -352,13 +422,21 @@ def search(request): {'Home': s.SERVER_URL}, {'Search': s.SERVER_URL + '/search'}, ] - # TODO perm - reviewed_package_qs = Package.objects.filter(reviewed=True) - unreviewed_package_qs = Package.objects.filter(reviewed=False) + + reviewed_package_qs = PackageManager.get_reviewed_packages() + unreviewed_package_qs = PackageManager.get_all_readable_packages(current_user) context['reviewed_objects'] = reviewed_package_qs context['unreviewed_objects'] = unreviewed_package_qs + if all([searchterm, mode]): + if package_urls: + packages = [PackageManager.get_package_by_url(current_user, p) for p in package_urls] + else: + packages = PackageManager.get_reviewed_packages() + + context['search_result'] = SearchManager.search(packages, searchterm, mode) + return render(request, 'search.html', context) @@ -487,7 +565,7 @@ def package_model(request, package_uuid, model_uuid): return render(request, 'objects/model.html', context) - if request.method == 'POST': + elif request.method == 'POST': if hidden := request.POST.get('hidden', None): if hidden == 'delete-model': current_model.delete() @@ -496,21 +574,9 @@ def package_model(request, package_uuid, model_uuid): return HttpResponseBadRequest() else: return HttpResponseBadRequest() - # - # new_compound_name = request.POST.get('compound-name') - # new_compound_description = request.POST.get('compound-description') - # - # if new_compound_name: - # current_compound.name = new_compound_name - # - # if new_compound_description: - # current_compound.description = new_compound_description - # - # if any([new_compound_name, new_compound_description]): - # current_compound.save() - # return redirect(current_compound.url) - # else: - # return HttpResponseBadRequest() + + else: + return HttpResponseBadRequest() def package(request, package_uuid): @@ -803,8 +869,7 @@ def package_rules(request, package_uuid): elif request.method == 'POST': - for k, v in request.POST.items(): - print(k, v) + log_post_params(request) # Generic params rule_name = request.POST.get('rule-name') @@ -817,8 +882,8 @@ def package_rules(request, package_uuid): # Obtain parameters as required by rule type if rule_type == 'SimpleAmbitRule': params['smirks'] = request.POST.get('rule-smirks') - params['reactant_smarts'] = request.POST.get('rule-reactant-smarts') - params['product_smarts'] = request.POST.get('rule-product-smarts') + params['reactant_filter_smarts'] = request.POST.get('rule-reactant-smarts') + params['product_filter_smarts'] = request.POST.get('rule-product-smarts') elif rule_type == 'SimpleRDKitRule': params['reaction_smarts'] = request.POST.get('rule-reaction-smarts') elif rule_type == 'ParallelRule': @@ -828,7 +893,7 @@ def package_rules(request, package_uuid): else: return HttpResponseBadRequest() - r = Rule.create(current_package, rule_type, name=rule_name, description=rule_description, **params) + r = Rule.create(rule_type=rule_type, package=current_package, name=rule_name, description=rule_description, **params) return redirect(r.url) else: @@ -854,7 +919,7 @@ def package_rule(request, package_uuid, rule_uuid): else: # isinstance(current_rule, ParallelRule) or isinstance(current_rule, SequentialRule): return render(request, 'objects/composite_rule.html', context) - if request.method == 'POST': + elif request.method == 'POST': if hidden := request.POST.get('hidden', None): if hidden == 'delete-rule': current_rule.delete() @@ -862,8 +927,23 @@ def package_rule(request, package_uuid, rule_uuid): else: return HttpResponseBadRequest() - # TODO update! + rule_name = request.POST.get('rule-name', '').strip() + rule_description = request.POST.get('rule-description', '').strip() + if rule_name: + current_rule.name = rule_name + + if rule_description: + current_rule.description = rule_description + + if any([rule_name, rule_description]): + current_rule.save() + return redirect(current_rule.url) + else: + return HttpResponseBadRequest() + + else: + return HttpResponseBadRequest() # https://envipath.org/package//reaction def package_reactions(request, package_uuid): @@ -912,6 +992,8 @@ def package_reactions(request, package_uuid): return redirect(r.url) + else: + return HttpResponseBadRequest() # https://envipath.org/package//reaction/ def package_reaction(request, package_uuid, reaction_uuid): @@ -931,7 +1013,7 @@ def package_reaction(request, package_uuid, reaction_uuid): return render(request, 'objects/reaction.html', context) - if request.method == 'POST': + elif request.method == 'POST': if hidden := request.POST.get('hidden', None): if hidden == 'delete-reaction': current_reaction.delete() @@ -954,6 +1036,8 @@ def package_reaction(request, package_uuid, reaction_uuid): else: return HttpResponseBadRequest() + else: + return HttpResponseBadRequest() # https://envipath.org/package//pathway def package_pathways(request, package_uuid): @@ -989,7 +1073,7 @@ def package_pathways(request, package_uuid): return render(request, 'collections/objects_list.html', context) - if request.method == 'POST': + elif request.method == 'POST': log_post_params(request) @@ -1002,15 +1086,34 @@ def package_pathways(request, package_uuid): return HttpResponseBadRequest() stand_smiles = FormatConverter.standardize(smiles) - pw = Pathway.create(current_package, name, description, stand_smiles) - if pw_mode != 'build': + if pw_mode not in ['predict', 'build', 'incremental']: + return HttpResponseBadRequest() + + pw = Pathway.create(current_package, name, description, stand_smiles) + # set mode + pw.kv.update({'mode': pw_mode}) + pw.save() + + if pw_mode == 'predict' or pw_mode == 'incremental': + # unlimited pred (will be handled by setting) + limit = -1 + + # For incremental predict first level and return + if pw_mode == 'incremental': + limit = 1 + pred_setting = current_user.prediction_settings() + pw.setting = pred_setting + pw.save() + from .tasks import predict - predict.delay(pw.pk, pred_setting.pk) + predict.delay(pw.pk, pred_setting.pk, limit=limit) return redirect(pw.url) + else: + return HttpResponseBadRequest() # https://envipath.org/package//pathway/ def package_pathway(request, package_uuid, pathway_uuid): @@ -1043,7 +1146,7 @@ def package_pathway(request, package_uuid, pathway_uuid): return render(request, 'objects/pathway.html', context) # return render(request, 'pathway_playground2.html', context) - if request.method == 'POST': + elif request.method == 'POST': if hidden := request.POST.get('hidden', None): if hidden == 'delete-pathway': current_pathway.delete() @@ -1051,30 +1154,92 @@ def package_pathway(request, package_uuid, pathway_uuid): else: return HttpResponseBadRequest() + pathway_name = request.POST.get('pathway-name') + pathway_description = request.POST.get('pathway-description') -# -# -# -# def package_relative_reasonings(request, package_id): -# if request.method == 'GET': -# pass -# -# -# def package_relative_reasoning(request, package_id, relative_reasoning_id): -# current_user = _anonymous_or_real(request) -# -# if request.method == 'GET': -# pass -# elif request.method == 'POST': -# pass -# -# # -# # -# # # https://envipath.org/package//pathway//node -# # def package_pathway_nodes(request, package_id, pathway_id): -# # pass -# # -# # + if any([pathway_name, pathway_description]): + if pathway_name is not None and pathway_name.strip() != '': + pathway_name = pathway_name.strip() + + current_pathway.name = pathway_name + + if pathway_description is not None and pathway_description.strip() != '': + pathway_description = pathway_description.strip() + + current_pathway.description = pathway_description + + current_pathway.save() + return redirect(current_pathway.url) + + node_url = request.POST.get('node') + + if node_url: + n = current_pathway.get_node(node_url) + + from .tasks import predict + # Dont delay? + predict(current_pathway.pk, current_pathway.setting.pk, node_pk=n.pk) + return JsonResponse({'success': current_pathway.url}) + + return HttpResponseBadRequest() + + else: + return HttpResponseBadRequest() + +# https://envipath.org/package//pathway//node +def package_pathway_nodes(request, package_uuid, pathway_uuid): + current_user = _anonymous_or_real(request) + current_package = PackageManager.get_package_by_id(current_user, package_uuid) + current_pathway = Pathway.objects.get(package=current_package, uuid=pathway_uuid) + + if request.method == 'GET': + context = get_base_context(request) + context['title'] = f'enviPath - {current_package.name} - {current_pathway.name} - Nodes' + + context['meta']['current_package'] = current_package + context['object_type'] = 'node' + context['breadcrumbs'] = [ + {'Home': s.SERVER_URL}, + {'Package': s.SERVER_URL + '/package'}, + {current_package.name: current_package.url}, + {'Pathway': current_package.url + '/pathway'}, + {current_pathway.name: current_pathway.url}, + {'Node': current_pathway.url + '/node'}, + ] + + reviewed_node_qs = Node.objects.none() + unreviewed_node_qs = Node.objects.none() + + if current_package.reviewed: + reviewed_node_qs = Node.objects.filter(pathway=current_pathway).order_by('name') + else: + unreviewed_node_qs = Node.objects.filter(pathway=current_pathway).order_by('name') + + if request.GET.get('all'): + return JsonResponse({ + "objects": [ + {"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed} + for pw in (reviewed_node_qs if current_package.reviewed else unreviewed_node_qs) + ] + }) + + context['reviewed_objects'] = reviewed_node_qs + context['unreviewed_objects'] = unreviewed_node_qs + + return render(request, 'collections/objects_list.html', context) + + elif request.method == 'POST': + + node_name = request.POST.get('node-name') + node_description = request.POST.get('node-description') + node_smiles = request.POST.get('node-smiles') + + current_pathway.add_node(node_smiles, name=node_name, description=node_description) + + return redirect(current_pathway.url) + + else: + return HttpResponseBadRequest() # https://envipath.org/package//pathway//node/ @@ -1091,19 +1256,129 @@ def package_pathway_node(request, package_uuid, pathway_uuid, node_uuid): svg_data = current_node.as_svg return HttpResponse(svg_data, content_type="image/svg+xml") + context = get_base_context(request) + context['title'] = f'enviPath - {current_package.name} - {current_pathway.name}' + + context['meta']['current_package'] = current_package + context['object_type'] = 'pathway' + + context['breadcrumbs'] = [ + {'Home': s.SERVER_URL}, + {'Package': s.SERVER_URL + '/package'}, + {current_package.name: current_package.url}, + {'Pathway': current_package.url + '/pathway'}, + {current_pathway.name: current_pathway.url}, + {'Node': current_pathway.url + '/node'}, + {current_node.name: current_node.url}, + ] + + context['node'] = current_node + + return render(request, 'objects/node.html', context) + + elif request.method == 'POST': + if s.DEBUG: + for k, v in request.POST.items(): + print(k, v) + + if hidden := request.POST.get('hidden', None): + if hidden == 'delete-node': + current_node.delete() + return redirect(current_pathway.url) + + else: + return HttpResponseBadRequest() + + +# https://envipath.org/package//pathway//edge +def package_pathway_edges(request, package_uuid, pathway_uuid): + current_user = _anonymous_or_real(request) + current_package = PackageManager.get_package_by_id(current_user, package_uuid) + current_pathway = Pathway.objects.get(package=current_package, uuid=pathway_uuid) + + if request.method == 'GET': + context = get_base_context(request) + context['title'] = f'enviPath - {current_package.name} - {current_pathway.name} - Edges' + + context['meta']['current_package'] = current_package + context['object_type'] = 'edge' + context['breadcrumbs'] = [ + {'Home': s.SERVER_URL}, + {'Package': s.SERVER_URL + '/package'}, + {current_package.name: current_package.url}, + {'Pathway': current_package.url + '/pathway'}, + {current_pathway.name: current_pathway.url}, + {'Edge': current_pathway.url + '/edge'}, + ] + + reviewed_edge_qs = Edge.objects.none() + unreviewed_edge_qs = Edge.objects.none() + + if current_package.reviewed: + reviewed_edge_qs = Edge.objects.filter(pathway=current_pathway).order_by('name') + else: + unreviewed_edge_qs = Edge.objects.filter(pathway=current_pathway).order_by('name') + + if request.GET.get('all'): + return JsonResponse({ + "objects": [ + {"name": pw.name, "url": pw.url, "reviewed": current_package.reviewed} + for pw in (reviewed_edge_qs if current_package.reviewed else unreviewed_edge_qs) + ] + }) + + context['reviewed_objects'] = reviewed_edge_qs + context['unreviewed_objects'] = unreviewed_edge_qs + + return render(request, 'collections/objects_list.html', context) + + elif request.method == 'POST': + + edge_name = request.POST.get('edge-name') + edge_description = request.POST.get('edge-description') + edge_substrates = request.POST.getlist('edge-substrates') + edge_products = request.POST.getlist('edge-products') + + substrate_nodes = [current_pathway.get_node(url) for url in edge_substrates] + product_nodes = [current_pathway.get_node(url) for url in edge_products] + + # TODO in the future consider Rules here? + current_pathway.add_edge(substrate_nodes, product_nodes, name=edge_name, description=edge_description) + + return redirect(current_pathway.url) + + else: + return HttpResponseBadRequest() + + +# https://envipath.org/package//pathway//edge/ +def package_pathway_edge(request, package_uuid, pathway_uuid, edge_uuid): + current_user = _anonymous_or_real(request) + current_package = PackageManager.get_package_by_id(current_user, package_uuid) + current_pathway = Pathway.objects.get(package=current_package, uuid=pathway_uuid) + current_edge = Edge.objects.get(pathway=current_pathway, uuid=edge_uuid) + + if request.method == 'GET': + is_image_request = request.GET.get('image') + if is_image_request: + if is_image_request == 'svg': + svg_data = current_edge.as_svg + return HttpResponse(svg_data, content_type="image/svg+xml") + + elif request.method == 'POST': + if s.DEBUG: + for k, v in request.POST.items(): + print(k, v) + + if hidden := request.POST.get('hidden', None): + if hidden == 'delete-edge': + current_edge.delete() + return redirect(current_pathway.url) + + else: + return HttpResponseBadRequest() + -# # -# # -# # # https://envipath.org/package//pathway//edge -# # def package_pathway_edges(request, package_id, pathway_id): -# # pass -# # -# # -# # # https://envipath.org/package//pathway//edge/ -# # def package_pathway_edge(request, package_id, pathway_id, edge_id): -# # pass -# # -# # # https://envipath.org/package//scenario def package_scenarios(request, package_uuid): current_user = _anonymous_or_real(request) @@ -1158,11 +1433,6 @@ def package_scenario(request, package_uuid, scenario_uuid): return render(request, 'objects/scenario.html', context) - - -### END UNTESTED - - ############## # User/Group # ############## @@ -1201,7 +1471,7 @@ def users(request): return render(request, 'errors/user_account_inactive.html', status=403) email = temp_user.email - except get_user_model().DoesNotExists: + except get_user_model().DoesNotExist: return HttpResponseBadRequest() user = authenticate(username=email, password=password) @@ -1289,6 +1559,18 @@ def user(request, user_uuid): logout(request) return redirect(s.SERVER_URL) + default_package = request.POST.get('default-package') + default_group = request.POST.get('default-group') + default_prediction_setting = request.POST.get('default-prediction-setting') + + if any([default_package, default_group, default_prediction_setting]): + current_user.default_package = PackageManager.get_package_by_url(current_user, default_package) + current_user.default_group = GroupManager.get_group_by_url(current_user, default_group) + current_user.default_setting = SettingManager.get_setting_by_url(current_user, default_prediction_setting) + current_user.save() + + return redirect(current_user.url) + prediction_model_pk = request.POST.get('model') prediction_threshold = request.POST.get('threshold') prediction_max_nodes = request.POST.get('max_nodes') @@ -1509,3 +1791,9 @@ def layout(request): def depict(request): if smiles := request.GET.get('smiles'): return HttpResponse(IndigoUtils.mol_to_svg(smiles), content_type='image/svg+xml') + + elif smirks := request.GET.get('smirks'): + query_smirks = request.GET.get('is_query_smirks', False) == 'true' + return HttpResponse(IndigoUtils.smirks_to_svg(smirks, query_smirks), content_type='image/svg+xml') + else: + return HttpResponseBadRequest() diff --git a/static/images/enviPy-screenshot.png b/static/images/enviPy-screenshot.png new file mode 100644 index 00000000..09676aa5 Binary files /dev/null and b/static/images/enviPy-screenshot.png differ diff --git a/static/js/pps.js b/static/js/pps.js index b05a941c..43ff24dc 100644 --- a/static/js/pps.js +++ b/static/js/pps.js @@ -507,28 +507,6 @@ function makeAccordionPanel(accordionId, panelName, panelContent, collapsed, id) + ""; } -function makeSearchList(divToAppend, jsonob) { - - if(jsonob.status){ - $(divToAppend).append(''); - return; - } - - var content = makeAccordionHead("searchAccordion", "Results",""); - - for ( var type in jsonob){ - var obj = jsonob[type]; - var objs = ""; - for ( var x in obj) { - objs += "" - + obj[x].name + ""; - } - content += makeAccordionPanel("searchAccordion", type, objs, true); - } - $(divToAppend).append(content); - -} - function fillPRCurve(modelUri, onclick){ if (modelUri == '') { return; diff --git a/static/js/pw.js b/static/js/pw.js index ff9868d1..266e9917 100644 --- a/static/js/pw.js +++ b/static/js/pw.js @@ -1,5 +1,18 @@ console.log("loaded") + +function predictFromNode(url) { + $.post("", {node: url}) + .done(function (data) { + console.log("Success:", data); + window.location.href = data.success; + }) + .fail(function (xhr, status, error) { + console.error("Error:", xhr.status, xhr.responseText); + // show user-friendly message or log error + }); +} + // data = {{ pathway.d3_json | safe }}; // elem = 'vizdiv' function draw(pathway, elem) { @@ -48,7 +61,7 @@ function draw(pathway, elem) { const avgY = d3.mean(childNodes, d => d.y); n.fx = avgX; // keep level as is - n.fy = n.y; + n.fy = n.y; } } }); @@ -77,8 +90,100 @@ function draw(pathway, elem) { d3.select(t).select("circle").classed("highlighted", !d3.select(t).select("circle").classed("highlighted")); } + // Wait one second before showing popup + var popupWaitBeforeShow = 1000; + // Keep Popup at least for one second + var popushowAtLeast = 1000; - const tooltip = d3.select("#tooltip"); + function pop_show_e(element) { + var e = element; + setTimeout(function () { + if ($(e).is(':hover')) { // if element is still hovered + $(e).popover("show"); + + // workaround to set fixed positions + pop = $(e).attr("aria-describedby") + h = $('#' + pop).height(); + $('#' + pop).attr("style", `position: fixed; top: ${clientY - (h / 2.0)}px; left: ${clientX + 10}px; margin: 0px; max-width: 1000px; display: block;`) + setTimeout(function () { + var close = setInterval(function () { + if (!$(".popover:hover").length // mouse outside popover + && !$(e).is(':hover')) { // mouse outside element + $(e).popover('hide'); + clearInterval(close); + } + }, 100); + }, popushowAtLeast); + } + }, popupWaitBeforeShow); + } + + function pop_add(objects, title, contentFunction) { + objects.attr("id", "pop") + .attr("data-container", "body") + .attr("data-toggle", "popover") + .attr("data-placement", "right") + .attr("title", title); + + objects.each(function (d, i) { + options = {trigger: "manual", html: true, animation: false}; + this_ = this; + var p = $(this).popover(options).on("mouseenter", function () { + pop_show_e(this); + }); + p.on("show.bs.popover", function (e) { + // this is to dynamically ajdust the content and bounds of the popup + p.attr('data-content', contentFunction(d)); + p.data("bs.popover").setContent(); + p.data("bs.popover").tip().css({"max-width": "1000px"}); + }); + }); + } + + + function node_popup(n) { + popupContent = "" + n.name + "
"; + popupContent += "Depth " + n.depth + "
" + popupContent += "
" + if (n.scenarios.length > 0) { + popupContent += 'Half-lives and related scenarios:
' + for (var s of n.scenarios) { + popupContent += "" + s.name + "
"; + } + } + + var isLeaf = pathway.links.filter(obj => obj.source.id === n.id).length === 0; + if(pathway.isIncremental && isLeaf) { + popupContent += '
Predict from here
'; + } + + return popupContent; + } + + function edge_popup(e) { + popupContent = "" + e.name + "
"; + popupContent += "
" + if (e.reaction_probability) { + popupContent += 'Probability:
' + e.reaction_probability.toFixed(3) + '
'; + } + + + if (e.scenarios.length > 0) { + popupContent += 'Half-lives and related scenarios:
' + for (var s of e.scenarios) { + popupContent += "" + s.name + "
"; + } + } + + return popupContent; + } + + var clientX; + var clientY; + document.addEventListener('mousemove', function(event) { + clientX = event.clientX; + clientY =event.clientY; + }); const zoomable = d3.select("#zoomable"); @@ -102,10 +207,10 @@ function draw(pathway, elem) { orig_depth = n.depth // console.log(n.id, parents) - for(idx in parents) { + for (idx in parents) { p = nodes[parents[idx]] // console.log(p.depth) - if(p.depth >= n.depth) { + if (p.depth >= n.depth) { // keep the .5 steps for pseudo nodes n.depth = n.pseudo ? p.depth + 1 : Math.floor(p.depth + 1); // console.log("Adjusting", orig_depth, Math.floor(p.depth + 1)); @@ -128,13 +233,15 @@ function draw(pathway, elem) { .enter().append("line") // Check if target is pseudo and draw marker only if not pseudo .attr("class", d => d.target.pseudo ? "link_no_arrow" : "link") - .on("mouseover", (event, d) => { - tooltip.style("visibility", "visible") - .text(`Link: ${d.source.id} → ${d.target.id}`) - .style("top", `${event.pageY + 5}px`) - .style("left", `${event.pageX + 5}px`); - }) - .on("mouseout", () => tooltip.style("visibility", "hidden")); + // .on("mouseover", (event, d) => { + // tooltip.style("visibility", "visible") + // .text(`Link: ${d.source.id} → ${d.target.id}`) + // .style("top", `${event.pageY + 5}px`) + // .style("left", `${event.pageX + 5}px`); + // }) + // .on("mouseout", () => tooltip.style("visibility", "hidden")); + + pop_add(link, "Reaction", edge_popup); // Knoten zeichnen const node = zoomable.append("g") @@ -145,19 +252,19 @@ function draw(pathway, elem) { .on("start", dragstarted) .on("drag", dragged) .on("end", dragended)) - .on("click", function(event, d) { + .on("click", function (event, d) { d3.select(this).select("circle").classed("highlighted", !d3.select(this).select("circle").classed("highlighted")); }) - .on("mouseover", (event, d) => { - if (d.pseudo) { - return - } - tooltip.style("visibility", "visible") - .text(`Node: ${d.id} Depth: ${d.depth}`) - .style("top", `${event.pageY + 5}px`) - .style("left", `${event.pageX + 5}px`); - }) - .on("mouseout", () => tooltip.style("visibility", "hidden")); + // .on("mouseover", (event, d) => { + // if (d.pseudo) { + // return + // } + // tooltip.style("visibility", "visible") + // .text(`Node: ${d.id} Depth: ${d.depth}`) + // .style("top", `${event.pageY + 5}px`) + // .style("left", `${event.pageX + 5}px`); + // }) + // .on("mouseout", () => tooltip.style("visibility", "hidden")); // Kreise für die Knoten hinzufügen node.append("circle") @@ -172,4 +279,6 @@ function draw(pathway, elem) { .attr("y", -nodeRadius) .attr("width", nodeRadius * 2) .attr("height", nodeRadius * 2); + + pop_add(node, "Compound", node_popup); } diff --git a/templates/actions/collections/compound.html b/templates/actions/collections/compound.html index cd08bdcb..90551b07 100644 --- a/templates/actions/collections/compound.html +++ b/templates/actions/collections/compound.html @@ -1,4 +1,6 @@ -
  • - - New Compound -
  • \ No newline at end of file +{% if meta.can_edit %} +
  • + + New Compound +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/collections/compound_structure.html b/templates/actions/collections/compound_structure.html index db18a112..3e615cc1 100644 --- a/templates/actions/collections/compound_structure.html +++ b/templates/actions/collections/compound_structure.html @@ -1,4 +1,6 @@ -
  • - - New Compound Structure -
  • \ No newline at end of file +{% if meta.can_edit %} +
  • + + New Compound Structure +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/collections/edge.html b/templates/actions/collections/edge.html new file mode 100644 index 00000000..eb003dc1 --- /dev/null +++ b/templates/actions/collections/edge.html @@ -0,0 +1,6 @@ +{% if meta.can_edit %} +
  • + + New Edge +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/collections/group.html b/templates/actions/collections/group.html index 5c666979..dd9054cf 100644 --- a/templates/actions/collections/group.html +++ b/templates/actions/collections/group.html @@ -1,4 +1,4 @@
  • New Group -
  • \ No newline at end of file + diff --git a/templates/actions/collections/model.html b/templates/actions/collections/model.html index 89008b2b..a384080b 100644 --- a/templates/actions/collections/model.html +++ b/templates/actions/collections/model.html @@ -1,4 +1,6 @@ -
  • - - New Model -
  • +{% if meta.can_edit %} +
  • + + New Model +
  • +{% endif %} diff --git a/templates/actions/collections/node.html b/templates/actions/collections/node.html new file mode 100644 index 00000000..968423f3 --- /dev/null +++ b/templates/actions/collections/node.html @@ -0,0 +1,6 @@ +{% if meta.can_edit %} +
  • + + New Node +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/collections/pathway.html b/templates/actions/collections/pathway.html index 8268b0fd..bcfc043c 100644 --- a/templates/actions/collections/pathway.html +++ b/templates/actions/collections/pathway.html @@ -1,4 +1,6 @@ -
  • - - New Pathway -
  • \ No newline at end of file +{% if meta.can_edit %} +
  • + + New Pathway +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/collections/reaction.html b/templates/actions/collections/reaction.html index 144cc5e7..00d96f4f 100644 --- a/templates/actions/collections/reaction.html +++ b/templates/actions/collections/reaction.html @@ -1,4 +1,6 @@ -
  • - - New Reaction -
  • \ No newline at end of file +{% if meta.can_edit %} +
  • + + New Reaction +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/collections/rule.html b/templates/actions/collections/rule.html index 0fd48bd3..dbef29da 100644 --- a/templates/actions/collections/rule.html +++ b/templates/actions/collections/rule.html @@ -1,4 +1,6 @@ -
  • - - New Rule -
  • +{% if meta.can_edit %} +
  • + + New Rule +
  • +{% endif %} diff --git a/templates/actions/collections/scenario.html b/templates/actions/collections/scenario.html index 0055c8eb..f9ccc08e 100644 --- a/templates/actions/collections/scenario.html +++ b/templates/actions/collections/scenario.html @@ -1,4 +1,6 @@ -
  • - - New Scenario -
  • \ No newline at end of file +{% if meta.can_edit %} +
  • + + New Scenario +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/collections/setting.html b/templates/actions/collections/setting.html index b3438bac..db8b95dc 100644 --- a/templates/actions/collections/setting.html +++ b/templates/actions/collections/setting.html @@ -1,4 +1,6 @@ -
  • - - New Setting -
  • \ No newline at end of file +{% if meta.can_edit %} +
  • + + New Setting +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/compound.html b/templates/actions/objects/compound.html index ab69f18a..41bbcba2 100644 --- a/templates/actions/objects/compound.html +++ b/templates/actions/objects/compound.html @@ -1,12 +1,14 @@ -
  • - - Edit Compound -
  • -
  • - - Add Structure -
  • -
  • - - Delete Compound -
  • +{% if meta.can_edit %} +
  • + + Edit Compound +
  • +
  • + + Add Structure +
  • +
  • + + Delete Compound +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/compound_structure.html b/templates/actions/objects/compound_structure.html index e786638b..be8bd9a8 100644 --- a/templates/actions/objects/compound_structure.html +++ b/templates/actions/objects/compound_structure.html @@ -1,8 +1,10 @@ -
  • - - Edit Compound Structure -
  • -
  • - - Delete Compound Structure -
  • +{% if meta.can_edit %} +
  • + + Edit Compound Structure +
  • +
  • + + Delete Compound Structure +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/group.html b/templates/actions/objects/group.html index 1cd6f1dc..fb941c90 100644 --- a/templates/actions/objects/group.html +++ b/templates/actions/objects/group.html @@ -1,8 +1,10 @@ -
  • - - Delete Group -
  • -
  • - - Add/Remove Member -
  • \ No newline at end of file +{% if meta.can_edit %} +
  • + + Delete Group +
  • +
  • + + Add/Remove Member +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/model.html b/templates/actions/objects/model.html new file mode 100644 index 00000000..1047618c --- /dev/null +++ b/templates/actions/objects/model.html @@ -0,0 +1,6 @@ +{% if meta.can_edit %} +
  • + + Delete Model +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/node.html b/templates/actions/objects/node.html new file mode 100644 index 00000000..77352594 --- /dev/null +++ b/templates/actions/objects/node.html @@ -0,0 +1,10 @@ +{% if meta.can_edit %} +
  • + + Edit Node +
  • +
  • + + Delete Node +
  • +{% endif %} diff --git a/templates/actions/objects/package.html b/templates/actions/objects/package.html index d2e1dfa2..7ebaabb1 100644 --- a/templates/actions/objects/package.html +++ b/templates/actions/objects/package.html @@ -1,16 +1,18 @@ -
  • - - Edit Package -
  • -
  • - - Edit Permissions -
  • -
  • - - License -
  • -
  • - - Delete Package -
  • +{% if meta.can_edit %} +
  • + + Edit Package +
  • +
  • + + Edit Permissions +
  • +
  • + + License +
  • +
  • + + Delete Package +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/pathway.html b/templates/actions/objects/pathway.html index 007e337e..399adaa5 100644 --- a/templates/actions/objects/pathway.html +++ b/templates/actions/objects/pathway.html @@ -1,8 +1,32 @@ -
  • - - Edit Pathway -
  • -
  • - - Delete Pathway -
  • +{% if meta.can_edit %} +
  • + + Add Compound +
  • +
  • + + Add Reaction +
  • + +
  • + + Edit Pathway +
  • +{#
  • #} +{# #} +{# Calculate Compound Properties#} +{#
  • #} + +
  • + + Delete Compound +
  • +
  • + + Delete Reaction +
  • +
  • + + Delete Pathway +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/reaction.html b/templates/actions/objects/reaction.html index 617d0238..413ae1ba 100644 --- a/templates/actions/objects/reaction.html +++ b/templates/actions/objects/reaction.html @@ -1,8 +1,10 @@ -
  • - - Edit Reaction -
  • -
  • - - Delete Reaction -
  • +{% if meta.can_edit %} +
  • + + Edit Reaction +
  • +
  • + + Delete Reaction +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/rule.html b/templates/actions/objects/rule.html index 61345b0b..be1ec35c 100644 --- a/templates/actions/objects/rule.html +++ b/templates/actions/objects/rule.html @@ -1,4 +1,6 @@ -
  • - - Edit Rule -
  • +{% if meta.can_edit %} +
  • + + Edit Rule +
  • +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/scenario.html b/templates/actions/objects/scenario.html index e69de29b..c9ddab01 100644 --- a/templates/actions/objects/scenario.html +++ b/templates/actions/objects/scenario.html @@ -0,0 +1,2 @@ +{% if meta.can_edit %} +{% endif %} \ No newline at end of file diff --git a/templates/actions/objects/user.html b/templates/actions/objects/user.html index 0c4368d4..99d7f45a 100644 --- a/templates/actions/objects/user.html +++ b/templates/actions/objects/user.html @@ -1,20 +1,22 @@ -
  • - - Update -
  • -
  • - - Update Password -
  • -
  • - - New Prediction Setting -
  • -
  • - - Manage API Token -
  • -
  • - - Delete Account -
  • +{% if meta.can_edit %} +
  • + + Update +
  • +
  • + + Update Password +
  • +
  • + + New Prediction Setting +
  • +{#
  • #} +{# #} +{# Manage API Token#} +{#
  • #} +
  • + + Delete Account +
  • +{% endif %} \ No newline at end of file diff --git a/templates/collections/objects_list.html b/templates/collections/objects_list.html index 4c4ac16e..16d7e13d 100644 --- a/templates/collections/objects_list.html +++ b/templates/collections/objects_list.html @@ -35,12 +35,16 @@ {% include "modals/collections/new_reaction_modal.html" %} {% elif object_type == 'pathway' %} {# {% include "modals/collections/new_pathway_modal.html" %} #} +{% elif object_type == 'node' %} +{% include "modals/collections/new_node_modal.html" %} +{% elif object_type == 'edge' %} +{% include "modals/collections/new_edge_modal.html" %} {% elif object_type == 'scenario' %} {% include "modals/collections/new_scenario_modal.html" %} {% elif object_type == 'model' %} {% include "modals/collections/new_model_modal.html" %} {% elif object_type == 'setting' %} -{% include "modals/collections/new_setting_modal.html" %} +{#{% include "modals/collections/new_setting_modal.html" %}#} {% elif object_type == 'user' %}
    {% elif object_type == 'group' %} @@ -63,6 +67,10 @@ Reactions {% elif object_type == 'pathway' %} Pathways + {% elif object_type == 'node' %} + Nodes + {% elif object_type == 'edge' %} + Edges {% elif object_type == 'scenario' %} Scenarios {% elif object_type == 'model' %} @@ -75,34 +83,38 @@ Groups {% endif %} @@ -133,6 +145,14 @@

    A pathway displays the (predicted) biodegradation of a compound as graph. Learn more >>

    + {% elif object_type == 'node' %} +

    Nodes represent the (predicted) compounds in a graph. + Learn more + >>

    + {% elif object_type == 'edge' %} +

    Edges represent the links between Nodes in a graph + Learn more + >>

    {% elif object_type == 'scenario' %}

    A scenario contains meta-information that can be attached to other data (compounds, rules, ..). Learn more @@ -185,7 +205,7 @@ {% endfor %} {% else %} {% for obj in reviewed_objects|slice:":50" %} - {{ obj.name }} + {{ obj.name }}{# ({{ obj.package.name }}) #} ' + obj.name + ''); + } else { + $('#UnreviewedContent').append('' + obj.name + ''); + } + } + + $('#load-all-loading').empty(); + $('#load-remaining').hide(); + }).fail(function (resp) { + $('#load-all-loading').empty(); + $('#load-all-error').show(); + }); + }); + } + + $('#object-search').on('keyup', function () { + let query = $(this).val().toLowerCase(); + $('a.list-group-item').each(function () { + let text = $(this).text().toLowerCase(); + $(this).toggle(text.indexOf(query) !== -1); + }); + }); + + }); + {% endblock content %} diff --git a/templates/errors/error.html b/templates/errors/error.html new file mode 100644 index 00000000..1db057f2 --- /dev/null +++ b/templates/errors/error.html @@ -0,0 +1,14 @@ +{% extends "framework.html" %} +{% load static %} +{% block content %} + +

    + +{% endblock content %} diff --git a/templates/errors/user_account_inactive.html b/templates/errors/user_account_inactive.html index 89d451ac..3b656b77 100644 --- a/templates/errors/user_account_inactive.html +++ b/templates/errors/user_account_inactive.html @@ -2,9 +2,9 @@ {% load static %} {% block content %} -