diff --git a/eddy/core/diagram.py b/eddy/core/diagram.py index 83974ea7..da8c1282 100644 --- a/eddy/core/diagram.py +++ b/eddy/core/diagram.py @@ -96,6 +96,7 @@ class Diagram(QtWidgets.QGraphicsScene): Extension of QtWidgets.QGraphicsScene which implements a single Graphol diagram. Additionally to built-in signals, this class emits: + * sgnDragDropEvent: whenever data is dropped onto the Diagram. * sgnItemAdded: whenever an element is added to the Diagram. * sgnItemInsertionCompleted: whenever an item 'MANUAL' insertion process is completed. * sgnItemRemoved: whenever an element is removed from the Diagram. @@ -110,6 +111,7 @@ class Diagram(QtWidgets.QGraphicsScene): MaxFontSize = 40 SelectionRadius = 4 + sgnDragDropEvent = QtCore.pyqtSignal(QtWidgets.QGraphicsScene, QtWidgets.QGraphicsSceneDragDropEvent) sgnItemAdded = QtCore.pyqtSignal(QtWidgets.QGraphicsScene, QtWidgets.QGraphicsItem) sgnItemInsertionCompleted = QtCore.pyqtSignal(QtWidgets.QGraphicsItem, int) sgnItemRemoved = QtCore.pyqtSignal(QtWidgets.QGraphicsScene, QtWidgets.QGraphicsItem) @@ -235,15 +237,25 @@ def dropEvent(self, dropEvent: QtWidgets.QGraphicsSceneDragDropEvent) -> None: Executed when a dragged element is dropped on the diagram. """ super().dropEvent(dropEvent) - if dropEvent.mimeData().hasFormat('text/plain') and Item.valueOf(dropEvent.mimeData().text()): + self.sgnDragDropEvent.emit(self, dropEvent) + if ( + dropEvent.dropAction() == QtCore.Qt.DropAction.CopyAction + and dropEvent.mimeData().hasFormat('text/plain') + and Item.valueOf(dropEvent.mimeData().text()) + ): snapToGrid = self.session.action('toggle_grid').isChecked() node = self.factory.create(Item.valueOf(dropEvent.mimeData().text())) node.setPos(snap(dropEvent.scenePos(), Diagram.GridSize, snapToGrid)) data = dropEvent.mimeData().data(dropEvent.mimeData().text()) - if int(dropEvent.mimeData().text()) in {Item.ConceptNode, Item.AttributeNode, - Item.RoleNode, Item.IndividualNode, - Item.ValueDomainNode, Item.LiteralNode, - Item.FacetNode}: + if Item.valueOf(dropEvent.mimeData().text()) in { + Item.ConceptNode, + Item.AttributeNode, + Item.RoleNode, + Item.IndividualNode, + Item.ValueDomainNode, + Item.LiteralNode, + Item.FacetNode, + }: if not data: # For new nodes (e.g. drag and drop from palette) if isinstance(node, FacetNode): diff --git a/eddy/core/exporters/owl2.py b/eddy/core/exporters/owl2.py index 836dfe96..2542a898 100644 --- a/eddy/core/exporters/owl2.py +++ b/eddy/core/exporters/owl2.py @@ -89,10 +89,6 @@ ) from eddy.core.functions.signals import connect from eddy.core.jvm import getJavaVM -from eddy.core.metadata import ( - LiteralValue, - NamedEntity, -) from eddy.core.ndc import ( ADMS, NDCDataset, @@ -481,17 +477,6 @@ def onErrored(self, exception): self.reject() - @QtCore.pyqtSlot(str) - def onMetadataFetchErrored(self, message): - """ - Executed when a metadata fetch request fails. - :type message: str - """ - self.session.addNotification(textwrap.dedent(f""" - ERROR:\n - {message} - """)) - @QtCore.pyqtSlot(str) def onNDCMetadataMissing(self, uri): """ @@ -570,7 +555,6 @@ def run(self): connect(worker.sgnStarted, self.onStarted) connect(worker.sgnCompleted, self.onCompleted) connect(worker.sgnErrored, self.onErrored) - connect(worker.sgnMetadataFetchErrored, self.onMetadataFetchErrored) connect(worker.sgnNDCMetadataMissing, self.onNDCMetadataMissing) connect(worker.sgnProgress, self.onProgress) self.startThread('OWL2Export', worker) @@ -582,7 +566,6 @@ class OWLOntologyExporterWorker(AbstractWorker): """ sgnCompleted = QtCore.pyqtSignal() sgnErrored = QtCore.pyqtSignal(Exception) - sgnMetadataFetchErrored = QtCore.pyqtSignal(str) sgnNDCMetadataMissing = QtCore.pyqtSignal(str) sgnProgress = QtCore.pyqtSignal(int, int) sgnStarted = QtCore.pyqtSignal() @@ -638,7 +621,6 @@ def __init__(self, project, path=None, **kwargs): self._axioms = set() self._converted = dict() self._converted_meta_individuals = dict() - self.metadataProperty = self.project.getIRI('urn:x-graphol:origin') self.df = None self.man = None @@ -1405,43 +1387,8 @@ def createAnnotationAssertionAxioms(self, node): if OWLAxiom.Annotation in self.axiomsList: for annotation in node.iri.annotationAssertions: subject = self.IRI.create(str(annotation.subject)) - if annotation.assertionProperty == self.metadataProperty: - uri = annotation.value - # FIXME: this is a sync request!!! - result, response = self.nmanager.getSync(str(uri)) - if not result: - msg = f'Retrieval of {uri} failed: {response}' - LOGGER.warning(msg) - self.sgnMetadataFetchErrored.emit(msg) - continue - try: - for assertion in NamedEntity.from_dict(json.loads(response)).annotations: - if isinstance(assertion.object, LiteralValue): - from eddy.core.owl import Annotation - value = Annotation( - self.project.getIRI(str(assertion.predicate.iri)), - assertion.object.value, - type=assertion.object.datatype, - language=assertion.object.language, - parent=self.project, - ) - elif isinstance(assertion.object, NamedEntity): - value = Annotation( - self.project.getIRI(str(assertion.predicate.iri)), - self.project.getIRI(str(assertion.object.iri)), - parent=self.project, - ) - else: - LOGGER.warning(f'Skipping annotation with bnode object {assertion}') - value = self.getOWLApiAnnotation(value) - self.addAxiom(self.df.getOWLAnnotationAssertionAxiom(subject, value)) - except Exception as e: - msg = f'Failed to parse metadata for {uri}' - LOGGER.warning(f'{msg}: {response}') - self.sgnMetadataFetchErrored.emit(f'{msg}: See log for details.') - else: - value = self.getOWLApiAnnotation(annotation) - self.addAxiom(self.df.getOWLAnnotationAssertionAxiom(subject, value)) + value = self.getOWLApiAnnotation(annotation) + self.addAxiom(self.df.getOWLAnnotationAssertionAxiom(subject, value)) def createClassAssertionAxiom(self, edge): """ diff --git a/eddy/core/metadata.py b/eddy/core/metadata.py deleted file mode 100644 index d8059221..00000000 --- a/eddy/core/metadata.py +++ /dev/null @@ -1,435 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# # -# Eddy: a graphical editor for the specification of Graphol ontologies # -# Copyright (C) 2015 Daniele Pantaleone # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ##################### ##################### # -# # -# Graphol is developed by members of the DASI-lab group of the # -# Dipartimento di Ingegneria Informatica, Automatica e Gestionale # -# A.Ruberti at Sapienza University of Rome: http://www.dis.uniroma1.it # -# # -# - Domenico Lembo # -# - Valerio Santarelli # -# - Domenico Fabio Savo # -# - Daniele Pantaleone # -# - Marco Console # -# # -########################################################################## - -from __future__ import annotations - -from abc import ( - ABCMeta, - abstractmethod, -) -from enum import unique -from typing import ( - List, - Optional, - cast, -) - -from PyQt5 import ( - QtCore, - QtNetwork, -) -from rdflib import ( - BNode, - Graph, - IdentifiedNode, - Literal, - URIRef, -) - -from eddy.core.datatypes.common import IntEnum_ - -# Graph instance used to keep track of namespaces -K_GRAPH = Graph(bind_namespaces='none') - - -class RepositoryMonitor(QtCore.QObject): - """ - This class can be used to listen for changes in the saved repository list. - """ - sgnUpdated = QtCore.pyqtSignal() - _instance = None - - def __new__(cls): - """ - Implements the singleton pattern. - """ - if cls._instance is None: - cls._instance = super().__new__(cls) - return cls._instance - - -K_REPO_MONITOR = RepositoryMonitor() - - -@unique -class MetadataRequest(IntEnum_): - """ - This class defines the attributes used in metadata REST requests. - """ - RepositoryAttribute = QtNetwork.QNetworkRequest.Attribute(7001) - - -class Repository: - """ - A repository of metadata as a RESTful API endpoint. - """ - - def __init__(self, name: str, uri: URIRef | str): - """Initialize the repository instance.""" - self._name = name - self._uri = uri - # TODO: Authentication mechanims will be added later - - @property - def name(self): - """Returns the repository name.""" - return self._name - - @property - def uri(self): - """Returns the repository uri.""" - return self._uri - - ############################################# - # INTERFACE - ################################# - - @classmethod - def load(cls) -> List[Repository]: - """Load the repositories list from user preferences.""" - repos = [] # type: List[Repository] - settings = QtCore.QSettings() - for index in range(settings.beginReadArray('metadata/repositories')): - settings.setArrayIndex(index) - repos.append(Repository( - name=settings.value('name'), - uri=settings.value('uri'), - )) - return repos - - @classmethod - def save(cls, repositories: List[Repository]) -> None: - """Save the repository in the user preferences.""" - settings = QtCore.QSettings() - settings.beginWriteArray('metadata/repositories') - for index, repo in enumerate(repositories): - settings.setArrayIndex(index) - settings.setValue('name', repo.name) - settings.setValue('uri', repo.uri) - settings.endArray() - K_REPO_MONITOR.sgnUpdated.emit() - - -class Node(metaclass=ABCMeta): - """ - Base class for any API object. - """ - - @classmethod - @abstractmethod - def from_dict(cls, data: dict, **kwargs: dict) -> Node: - """Creates a new node from the given dict.""" - pass - - @abstractmethod - def to_dict(self, deep: bool = False) -> dict: - """Serializes the object to a dict.""" - pass - - @abstractmethod - def n3(self) -> str: - """Returns the N-TRIPLES (n3) representation of the object.""" - pass - - -class Entity(Node, metaclass=ABCMeta): - """ - Base class for any named or unnamed entity. - """ - - def __init__(self, id_: str) -> None: - """Initialize the entity.""" - super().__init__() - self._id = id_ - self._types = [] - self._annotations = [] - - @property - def id(self) -> str: - """Return the identifier for this entity.""" - return self._id - - @property - @abstractmethod - def name(self) -> IdentifiedNode: - """Return the IRI or BNode for this entity.""" - pass - - @property - def types(self) -> List[NamedEntity]: - """Return the list of types of this entity.""" - return self._types - - @property - def annotations(self) -> List[Annotation]: - """Return the list of annotations of this entity.""" - return self._annotations - - def __eq__(self, other: Entity) -> bool: - return self.__class__ == other.__class__ and self.id == other.id - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.id})" - - -class NamedEntity(Entity): - """ - Represents entities that are uniquely identified with an IRI. - """ - - def __init__(self, id_: str, iri: URIRef | str) -> None: - """Initialize the named entity.""" - super().__init__(id_) - self._iri = iri if isinstance(iri, URIRef) else URIRef(iri) - - @property - def iri(self) -> URIRef: - """Return the iri associated with this named entity.""" - return self._iri - - @property - def name(self) -> IdentifiedNode: - return self.iri - - @classmethod - def from_dict(cls, data: dict, **kwargs) -> Node: - ent = NamedEntity(data["id"], data["iri"]) # noqa - if "types" in data: - for t in data["types"]: - ent.types.append(cast(NamedEntity, NamedEntity.from_dict(t))) - if "annotations" in data: - for a in data["annotations"]: - ant = cast(Annotation, Annotation.from_dict(a, subject=ent.to_dict())) - ent.annotations.append(ant) - return ent - - def to_dict(self, deep: bool = False) -> dict: - res = { - "id": self.id, - "iri": self.name, - } - if deep: - res |= { - "types": [t.to_dict() for t in self.types], - "annotations": [a.to_dict() for a in self.annotations], - } - return res - - def n3(self) -> str: - return self.iri.n3(K_GRAPH.namespace_manager) - - def __eq__(self, other: Entity) -> bool: - return super().__eq__(other) and self.iri == cast(NamedEntity, other).iri - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.id}, \"{self.iri}\")" - - -class AnonymousEntity(Entity): - """ - Represents entities not identified by an IRI (i.e. blank nodes in RDF). - """ - - def __init__(self, id_: str, bnode: BNode | str) -> None: - """Initialize the anonymous entity.""" - super().__init__(id_) - self._bnode = bnode if isinstance(bnode, BNode) else BNode(bnode) - - @property - def bnode(self) -> BNode: - """Returns the blank node associated with this entity.""" - return self._bnode - - @property - def name(self) -> IdentifiedNode: - return self.bnode - - @classmethod - def from_dict(cls, data: dict, **kwargs) -> Node: - ent = NamedEntity(data["id"], data["bnode"]) # noqa - if "types" in data: - for t in data["types"]: - ent.types.append(cast(NamedEntity, NamedEntity.from_dict(t))) - if "annotations" in data: - for a in data["annotations"]: - ent.annotations.append( - cast(Annotation, Annotation.from_dict(a, subject=ent.to_dict()))) - return ent - - def to_dict(self, deep: bool = False) -> dict: - res = { - "id": self.id, - "bnode": self.name, - } - if deep: - res |= { - "types": [t.to_dict() for t in self.types], - "annotations": [a.to_dict() for a in self.annotations], - } - return res - - def n3(self) -> str: - return self.bnode.n3(K_GRAPH.namespace_manager) - - def __eq__(self, other: Entity) -> bool: - return super().__eq__(other) and self.bnode == cast(AnonymousEntity, other).bnode - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.id}, \"{self.bnode}\")" - - -class LiteralValue(Node): - """ - Represent a literal value associated with an annotation assertion. - """ - - def __init__(self, value: str, language: Optional[str] = None, - datatype: Optional[URIRef | str] = None) -> None: - """Initialize the literal.""" - super().__init__() - self._value = value - self._language = language - self._datatype = datatype - # Check that literal is well-formed - if self._language is not None and self._datatype is not None: - raise ValueError("Literals with a datatype cannot have a language tag.") - - @property - def value(self) -> str: - """Return the value of this literal.""" - return self._value - - @property - def language(self) -> Optional[str]: - """Return the language tag of this literal, or `None` if there is no tag.""" - return self._language - - @property - def datatype(self) -> Optional[URIRef | str]: - """Return the datatype of this literal, or `None` if there is no datatype.""" - return self._datatype - - @classmethod - def from_dict(cls, data: dict, **kwargs) -> Node: - return cls(data["value"], data["language"], data["datatype"]) # noqa - - def to_dict(self, deep: bool = False) -> dict: - return { - "value": self.value, - "language": self.language, - "datatype": self.datatype - } - - def n3(self) -> str: - return Literal(self.value, self.language, self.datatype).n3(K_GRAPH.namespace_manager) - - def __eq__(self, other: Entity) -> bool: - return ( - super().__eq__(other) - and self.value == cast(LiteralValue, other).value - and self.language == cast(LiteralValue, other).language - and self.datatype == cast(LiteralValue, other).datatype - ) - - def __repr__(self) -> str: - if self.language: - return f"{self.__class__.__name__}(\"{self.value}\"@{self.language})" - else: - return f"{self.__class__.__name__}(\"{self.value}\"^^{self.datatype})" - - -class Annotation(Node): - """ - Represent an annotation assertion. - """ - - def __init__(self, subject: Entity, predicate: NamedEntity, object: Node) -> None: - """Initialize the annotation.""" - self._subject = subject - self._predicate = predicate - self._object = object - - @property - def subject(self) -> Entity: - """Return the annotation subject.""" - return self._subject - - @property - def predicate(self) -> NamedEntity: - """Return the annotation predicate.""" - return self._predicate - - @property - def object(self) -> Node: - """Return the annotation object.""" - return self._object - - @classmethod - def from_dict(cls, data: dict, **kwargs) -> Node: - s = kwargs["subject"] - p = data["property"] - v = data["value"] - - sub = NamedEntity.from_dict(s) if "iri" in s else AnonymousEntity.from_dict(s) - prop = NamedEntity.from_dict(p) - - if "id" in v: - obj = NamedEntity.from_dict(v) if "iri" in v else AnonymousEntity.from_dict(v) - else: - obj = LiteralValue.from_dict(v) - - return Annotation(cast(Entity, sub), cast(NamedEntity, prop), obj) - - def to_dict(self, deep: bool = False) -> dict: - return { - "property": self.predicate.to_dict(), - "value": self.object.to_dict() - } - - def n3(self) -> str: - sub = self.subject.n3() - pred = self.predicate.n3() - obj = self.object.n3() - return f'{sub} {pred} {obj}' - - def __eq__(self, other: Annotation) -> bool: - return ( - self.__class__ == other.__class__ - and self.subject == other.subject - and self.predicate == other.predicate - and self.object == other.object - ) - - def __repr__(self) -> str: - return f"{self.__class__.__name__}({self.subject}, {self.predicate}, {self.object})" diff --git a/eddy/plugins/metadata-importer/metadata_importer/__init__.py b/eddy/plugins/metadata-importer/metadata_importer/__init__.py deleted file mode 100644 index 4328c9b3..00000000 --- a/eddy/plugins/metadata-importer/metadata_importer/__init__.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# # -# Eddy: a graphical editor for the specification of Graphol ontologies # -# Copyright (C) 2015 Daniele Pantaleone # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ##################### ##################### # -# # -# Graphol is developed by members of the DASI-lab group of the # -# Dipartimento di Ingegneria Informatica, Automatica e Gestionale # -# A.Ruberti at Sapienza University of Rome: http://www.dis.uniroma1.it # -# # -# - Domenico Lembo # -# - Valerio Santarelli # -# - Domenico Fabio Savo # -# - Daniele Pantaleone # -# - Marco Console # -# # -########################################################################## - -from PyQt5 import ( - QtCore, - QtGui, -) - -from eddy.core.functions.signals import connect, disconnect -from eddy.core.metadata import K_REPO_MONITOR -from eddy.core.output import getLogger -from eddy.core.plugin import AbstractPlugin -from eddy.ui.dock import DockWidget -from .widgets import MetadataImporterWidget # noqa - -LOGGER = getLogger() - - -class MetadataImporterPlugin(AbstractPlugin): - """ - Search and import ontology metadata from an external service. - """ - sgnProjectChanged = QtCore.pyqtSignal(str) - sgnUpdateState = QtCore.pyqtSignal() - - def __init__(self, spec, session): - """ - Initialises a new instance of the metadata importer plugin. - :type spec: PluginSpec - :type session: Session - """ - super().__init__(spec, session) - - ############################################# - # EVENTS - ################################# - - def eventFilter(self, source: QtCore.QObject, event: QtCore.QEvent) -> bool: - """ - Filters events if this object has been installed as an event filter for the watched object. - """ - if event.type() == QtCore.QEvent.Resize: - widget = source.widget() - widget.redraw() - return super().eventFilter(source, event) - - ############################################# - # SLOTS - ################################# - - @QtCore.pyqtSlot() - def onSessionReady(self): - """ - Executed whenever the main session completes the startup sequence. - """ - widget = self.widget('metadata_importer') # type: MetadataImporterWidget - # CONNECT TO PROJECT SPECIFIC SIGNALS - self.debug('Connecting to project: %s', self.project.name) - connect(self.project.sgnPrefixAdded, widget.onPrefixChanged) - connect(self.project.sgnPrefixModified, widget.onPrefixChanged) - connect(self.project.sgnPrefixRemoved, widget.onPrefixChanged) - connect(K_REPO_MONITOR.sgnUpdated, widget.doUpdateState) - widget.onPrefixChanged() - widget.doUpdateState() - - @QtCore.pyqtSlot() - def doUpdateState(self): - """ - Executed when the plugin session updates its state. - """ - pass - - ############################################# - # HOOKS - ################################# - - def dispose(self): - """ - Executed whenever the plugin is going to be destroyed. - """ - # DISCONNECT FROM CURRENT PROJECT - widget = self.widget('metadata_importer') # type: MetadataImporterWidget - self.debug('Disconnecting from project: %s', self.project.name) - disconnect(self.project.sgnPrefixAdded, widget.onPrefixChanged) - disconnect(self.project.sgnPrefixModified, widget.onPrefixChanged) - disconnect(self.project.sgnPrefixRemoved, widget.onPrefixChanged) - disconnect(K_REPO_MONITOR.sgnUpdated, widget.doUpdateState) - - # DISCONNECT FROM ACTIVE SESSION - self.debug('Disconnecting from active session') - disconnect(self.session.sgnReady, self.onSessionReady) - disconnect(self.session.sgnUpdateState, self.doUpdateState) - - def start(self): - """ - Perform initialization tasks for the plugin. - """ - # INITIALIZE THE WIDGET - self.debug('Starting Metadata Importer plugin') - widget = MetadataImporterWidget(self) - widget.setObjectName('metadata_importer') - self.addWidget(widget) - - # CREATE DOCKING AREA WIDGET - self.debug('Creating docking area widget') - widget = DockWidget('Metadata Importer', QtGui.QIcon(':icons/18/ic_explore_black'), self.session) - widget.installEventFilter(self) - widget.setAllowedAreas( - QtCore.Qt.LeftDockWidgetArea - | QtCore.Qt.RightDockWidgetArea - | QtCore.Qt.BottomDockWidgetArea - ) - widget.setObjectName('metadata_importer_dock') - widget.setWidget(self.widget('metadata_importer')) - self.addWidget(widget) - - # CREATE SHORTCUTS - action = widget.toggleViewAction() - action.setParent(self.session) - action.setShortcut(QtGui.QKeySequence('Alt+8')) - - # CREATE ENTRY IN VIEW MENU - self.debug('Creating docking area widget toggle in "view" menu') - menu = self.session.menu('view') - menu.addAction(self.widget('metadata_importer_dock').toggleViewAction()) - - # INSTALL DOCKING AREA WIDGET - self.debug('Installing docking area widget') - self.session.addDockWidget( - QtCore.Qt.LeftDockWidgetArea, - self.widget('metadata_importer_dock'), - ) - - # CONFIGURE SIGNAL/SLOTS - connect(self.session.sgnReady, self.onSessionReady) - connect(self.session.sgnUpdateState, self.doUpdateState) diff --git a/eddy/plugins/metadata-importer/metadata_importer/widgets.py b/eddy/plugins/metadata-importer/metadata_importer/widgets.py deleted file mode 100644 index 67842f85..00000000 --- a/eddy/plugins/metadata-importer/metadata_importer/widgets.py +++ /dev/null @@ -1,962 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# # -# Eddy: a graphical editor for the specification of Graphol ontologies # -# Copyright (C) 2015 Daniele Pantaleone # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ##################### ##################### # -# # -# Graphol is developed by members of the DASI-lab group of the # -# Dipartimento di Ingegneria Informatica, Automatica e Gestionale # -# A.Ruberti at Sapienza University of Rome: http://www.dis.uniroma1.it # -# # -# - Domenico Lembo # -# - Valerio Santarelli # -# - Domenico Fabio Savo # -# - Daniele Pantaleone # -# - Marco Console # -# # -########################################################################## - -from abc import ( - ABCMeta, - abstractmethod, -) -import json -import textwrap -from typing import ( - Any, - cast, -) - -from PyQt5 import ( - QtCore, - QtGui, - QtNetwork, - QtWidgets, -) -from rdflib import Graph -from rdflib.namespace import NamespaceManager - -from eddy.core.commands.iri import CommandIRIAddAnnotationAssertion -from eddy.core.datatypes.graphol import Item -from eddy.core.functions.misc import first -from eddy.core.functions.signals import connect -from eddy.core.metadata import ( - Entity, - K_GRAPH, - LiteralValue, - MetadataRequest, - NamedEntity, - Repository, -) -from eddy.core.output import getLogger -from eddy.ui.fields import ( - ComboBox, - IntegerField, - StringField, - TextField, -) - -LOGGER = getLogger() - - -class MetadataImporterWidget(QtWidgets.QWidget): - """ - This class implements the metadata importer used to search external metadata sources. - """ - sgnItemActivated = QtCore.pyqtSignal(QtGui.QStandardItem) - sgnItemClicked = QtCore.pyqtSignal(QtGui.QStandardItem) - sgnItemDoubleClicked = QtCore.pyqtSignal(QtGui.QStandardItem) - sgnItemRightClicked = QtCore.pyqtSignal(QtGui.QStandardItem) - - def __init__(self, plugin): - """ - Initialize the metadata importer widget. - :type plugin: Session - """ - super().__init__(plugin.session) - - self.plugin = plugin - self.settings = QtCore.QSettings() - self.iconAttribute = QtGui.QIcon(':/icons/18/ic_treeview_attribute') - self.iconConcept = QtGui.QIcon(':/icons/18/ic_treeview_concept') - self.iconInstance = QtGui.QIcon(':/icons/18/ic_treeview_instance') - self.iconRole = QtGui.QIcon(':/icons/18/ic_treeview_role') - self.iconValue = QtGui.QIcon(':/icons/18/ic_treeview_value') - - self.search = StringField(self) - self.search.setAcceptDrops(False) - self.search.setClearButtonEnabled(True) - self.search.setPlaceholderText('Search...') - self.search.setFixedHeight(30) - self.combobox = QtWidgets.QComboBox(self) - self.combobox.addItems(map(lambda r: r.name, Repository.load())) - self.combobox.setCurrentIndex(self.settings.value('metadata/index', 0, int)) - self.model = QtGui.QStandardItemModel(self) - self.proxy = MetadataImporterFilterProxyModel(self) - self.proxy.setDynamicSortFilter(False) - self.proxy.setFilterCaseSensitivity(QtCore.Qt.CaseInsensitive) - self.proxy.setSortCaseSensitivity(QtCore.Qt.CaseSensitive) - self.proxy.setSourceModel(self.model) - self.entityview = MetadataImporterView(self) - self.entityview.setModel(self.proxy) - self.details = MetadataInfoWidget(self) - - self.searchLayout = QtWidgets.QHBoxLayout() - self.searchLayout.setContentsMargins(0, 0, 0, 0) - self.searchLayout.addWidget(self.search) - self.searchLayout.addWidget(self.combobox) - self.mainLayout = QtWidgets.QVBoxLayout(self) - self.mainLayout.setContentsMargins(0, 0, 0, 0) - self.mainLayout.addLayout(self.searchLayout) - self.mainLayout.addWidget(self.entityview) - self.mainLayout.addWidget(self.details) - self.setTabOrder(self.search, self.combobox) - self.setTabOrder(self.combobox, self.entityview) - self.setTabOrder(self.entityview, self.details) - - self.setContentsMargins(0, 0, 0, 0) - self.setMinimumWidth(216) - self.setStyleSheet(""" - QLineEdit, - QLineEdit:editable, - QLineEdit:hover, - QLineEdit:pressed, - QLineEdit:focus { - border: none; - border-radius: 0; - background: #FFFFFF; - color: #000000; - padding: 4px 4px 4px 4px; - } - """) - - connect(self.entityview.activated, self.onItemActivated) - connect(self.entityview.doubleClicked, self.onItemDoubleClicked) - connect(self.entityview.pressed, self.onItemPressed) - connect(self.search.textChanged, self.doFilterItem) - connect(self.search.returnPressed, self.onReturnPressed) - connect(self.combobox.currentIndexChanged, self.onRepositoryChanged) - # connect(self.sgnItemActivated, self.session.doFocusItem) - # connect(self.sgnItemDoubleClicked, self.session.doFocusItem) - # connect(self.sgnItemRightClicked, self.session.doFocusItem) - - ############################################# - # PROPERTIES - ################################# - - @property - def project(self): - """ - Returns the reference to the active project. - :rtype: Session - """ - return self.session.project - - @property - def session(self): - """ - Returns the reference to the active session. - :rtype: Session - """ - return self.plugin.parent() - - ############################################# - # EVENTS - ################################# - - def paintEvent(self, paintEvent): - """ - This is needed for the widget to pick the stylesheet. - :type paintEvent: QPaintEvent - """ - option = QtWidgets.QStyleOption() - option.initFrom(self) - painter = QtGui.QPainter(self) - style = self.style() - style.drawPrimitive(QtWidgets.QStyle.PE_Widget, option, painter, self) - - ############################################# - # SLOTS - ################################# - - @QtCore.pyqtSlot(str) - def doFilterItem(self, key): - """ - Executed when the search box is filled with data. - :type key: str - """ - self.proxy.setFilterFixedString(key) - self.proxy.sort(QtCore.Qt.AscendingOrder) - - @QtCore.pyqtSlot() - def doUpdateState(self): - """ - Executed to refresh the metadata widget. - """ - repos = Repository.load() - if len(repos) > 0: - index = self.combobox.currentIndex() - # Prevent signals while refreshing the combobox - status = self.combobox.blockSignals(True) - self.combobox.clear() - self.combobox.addItems(map(lambda r: r.name, repos)) - self.combobox.blockSignals(status) - # Force repetition of current selection to trigger refresh - self.combobox.setCurrentIndex(index) - self.combobox.currentIndexChanged.emit(index) - else: - self.model.clear() - self.details.repository = None - self.details.entity = None - self.details.stack() - - @QtCore.pyqtSlot(QtCore.QModelIndex) - def onItemActivated(self, index): - """ - Executed when an item in the list view is activated (e.g. by pressing Return or Enter key). - :type index: QModelIndex - """ - # noinspection PyArgumentList - if QtWidgets.QApplication.mouseButtons() == QtCore.Qt.NoButton: - item = self.model.itemFromIndex(self.proxy.mapToSource(index)) - if item: - self.details.entity = item.data() - self.sgnItemActivated.emit(item) - # KEEP FOCUS ON THE TREE VIEW UNLESS SHIFT IS PRESSED - if QtWidgets.QApplication.queryKeyboardModifiers() & QtCore.Qt.SHIFT: - return - self.entityview.setFocus() - self.details.stack() - - @QtCore.pyqtSlot(QtCore.QModelIndex) - def onItemDoubleClicked(self, index): - """ - Executed when an item in the list view is double-clicked. - :type index: QModelIndex - """ - # noinspection PyArgumentList - if QtWidgets.QApplication.mouseButtons() & QtCore.Qt.LeftButton: - item = self.model.itemFromIndex(self.proxy.mapToSource(index)) - if item: - self.details.entity = item.data() - self.details.repository = item.data().repository - self.sgnItemDoubleClicked.emit(item) - self.details.stack() - - @QtCore.pyqtSlot(QtCore.QModelIndex) - def onItemPressed(self, index): - """ - Executed when an item in the treeview is clicked. - :type index: QModelIndex - """ - # noinspection PyArgumentList - if QtWidgets.QApplication.mouseButtons() & QtCore.Qt.LeftButton: - item = self.model.itemFromIndex(self.proxy.mapToSource(index)) - if item: - self.details.entity = item.data() - #self.details.repository = item.data().repository - self.sgnItemDoubleClicked.emit(item) - self.details.stack() - - @QtCore.pyqtSlot() - @QtCore.pyqtSlot(str) - @QtCore.pyqtSlot(str, str) - def onPrefixChanged(self, _name: str = None, _ns: str = None): - """ - Executed when a project prefix is changed to update the medatata namespace manager. - """ - # There is currently no support for unbinding a namespace in rdflib, - # so we have to resort to recreating it from scratch. - # See: https://github.com/RDFLib/rdflib/issues/1932 - K_GRAPH.namespace_manager = NamespaceManager(Graph(), bind_namespaces='none') - for prefix, ns in self.project.prefixDictItems(): - K_GRAPH.bind(prefix, ns, override=True) - self.redraw() - - @QtCore.pyqtSlot() - def onReturnPressed(self): - """ - Executed when the Return or Enter key is pressed in the search field. - """ - self.focusNextChild() - - @QtCore.pyqtSlot(int) - def onRepositoryChanged(self, index): - """ - Executed when the selected repository in the combobox changes. - """ - name = self.combobox.itemText(index) - repo = first(Repository.load(), filter_on_item=lambda i: i.name == name) - self.model.clear() - if repo: - self.settings.setValue('metadata/index', self.combobox.currentIndex()) - url = QtCore.QUrl(repo.uri) - url.setPath(f'{url.path()}/classes') - request = QtNetwork.QNetworkRequest(url) - request.setAttribute(MetadataRequest.RepositoryAttribute, repo) - reply = self.session.nmanager.get(request) - connect(reply.finished, self.onRequestCompleted) - else: - repo = None - self.details.repository = repo - self.details.entity = None - self.details.stack() - - @QtCore.pyqtSlot() - def onRequestCompleted(self): - """ - Executed when a metadata request has completed to update the widget. - """ - reply = self.sender() - try: - reply.deleteLater() - if reply.isFinished() and reply.error() == QtNetwork.QNetworkReply.NoError: - data = json.loads(str(reply.readAll(), encoding='utf-8')) - entities = [NamedEntity.from_dict(d) for d in data if "iri" in d] - for e in entities: - e.repository = reply.request().attribute(MetadataRequest.RepositoryAttribute) - try: - itemText = K_GRAPH.namespace_manager.curie(e.iri, generate=False) - except KeyError: - itemText = e.iri - item = QtGui.QStandardItem(self.iconConcept, f"{itemText}") - item.setData(e) - self.model.appendRow(item) - self.session.statusBar().showMessage('Metadata fetch completed') - elif reply.isFinished() and reply.error() != QtNetwork.QNetworkReply.NoError: - msg = f'Failed to retrieve metadata: {reply.errorString()}' - LOGGER.warning(msg) - self.session.statusBar().showMessage(msg) - except Exception as e: - LOGGER.error(f'Failed to retrieve metadata: {e}') - - ############################################# - # INTERFACE - ################################# - - def redraw(self) -> None: - """ - Redraw the content of the widget. - """ - for index in range(self.model.rowCount()): - item = self.model.item(index, 0) - if isinstance(item.data(), NamedEntity): - try: - itemText = K_GRAPH.namespace_manager.curie(item.data().iri, generate=False) - except KeyError: - itemText = item.data().iri - item.setText(itemText) - self.entityview.update() - self.details.redraw() - - def sizeHint(self): - """ - Returns the recommended size for this widget. - :rtype: QtCore.QSize - """ - return QtCore.QSize(216, 266) - - -class MetadataImporterView(QtWidgets.QListView): - """ - This class implements the metadata importer list view. - """ - def __init__(self, parent): - """ - Initialize the metadata importer list view. - :type parent: MetadataImporterWidget - """ - super().__init__(parent) - self.startPos = None - self.setContextMenuPolicy(QtCore.Qt.PreventContextMenu) - self.setEditTriggers(QtWidgets.QTreeView.NoEditTriggers) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setHorizontalScrollMode(QtWidgets.QTreeView.ScrollPerPixel) - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.setSelectionMode(QtWidgets.QListView.SelectionMode.SingleSelection) - self.setWordWrap(True) - # self.setItemDelegate(MetadataImporterItemDelegate(self)) - - ############################################# - # PROPERTIES - ################################# - - @property - def session(self): - """ - Returns the reference to the Session holding the MetadataImporter widget. - :rtype: Session - """ - return self.widget.session - - @property - def widget(self): - """ - Returns the reference to the MetadataImporter widget. - :rtype: MetadataImporterWidget - """ - return self.parent() - - ############################################# - # EVENTS - ################################# - - def mousePressEvent(self, mouseEvent): - """ - Executed when the mouse is pressed on the treeview. - :type mouseEvent: QMouseEvent - """ - self.clearSelection() - if mouseEvent.buttons() & QtCore.Qt.LeftButton: - self.startPos = mouseEvent.pos() - super().mousePressEvent(mouseEvent) - - def mouseMoveEvent(self, mouseEvent): - """ - Executed when the mouse if moved while a button is being pressed. - :type mouseEvent: QMouseEvent - """ - if mouseEvent.buttons() & QtCore.Qt.LeftButton: - distance = (mouseEvent.pos() - self.startPos).manhattanLength() - if distance >= QtWidgets.QApplication.startDragDistance(): - index = first(self.selectedIndexes()) - if index: - model = self.model().sourceModel() - index = self.model().mapToSource(index) - item = model.itemFromIndex(index) - data = item.data() - if data: - if isinstance(data, Entity): - mimeData = QtCore.QMimeData() - mimeData.setText(str(Item.ConceptNode.value)) - buf = QtCore.QByteArray() - buf.append(data.name) - mimeData.setData(str(Item.ConceptNode.value), buf) - drag = QtGui.QDrag(self) - drag.setMimeData(mimeData) - drag.exec_(QtCore.Qt.CopyAction) - - # Add assertion indicating source - from eddy.core.owl import IRI, AnnotationAssertion - subj = self.session.project.getIRI(str(data.name)) # type: IRI - pred = self.session.project.getIRI('urn:x-graphol:origin') - loc = QtCore.QUrl(data.repository.uri) - loc.setPath(f'{loc.path()}/entities/{data.id}'.replace('//', '/')) - obj = IRI(loc.toString()) - ast = AnnotationAssertion(subj, pred, obj) - cmd = CommandIRIAddAnnotationAssertion(self.session.project, subj, ast) - self.session.undostack.push(cmd) - - super().mouseMoveEvent(mouseEvent) - - def paintEvent(self, event: QtGui.QPaintEvent): - """ - Overrides paintEvent to display a placeholder text. - """ - super().paintEvent(event) - if self.model().rowCount() == 0: - painter = QtGui.QPainter(self.viewport()) - painter.save() - painter.setPen(self.palette().placeholderText().color()) - fm = self.fontMetrics() - bgMsg = 'No Metadata Available' - elided_text = fm.elidedText(bgMsg, QtCore.Qt.ElideRight, self.viewport().width()) - painter.drawText(self.viewport().rect(), QtCore.Qt.AlignCenter, elided_text) - painter.restore() - - ############################################# - # INTERFACE - ################################# - - def sizeHintForColumn(self, column): - """ - Returns the size hint for the given column. - This will make the column of the treeview as wide as the widget that contains the view. - :type column: int - :rtype: int - """ - return max(super().sizeHintForColumn(column), self.viewport().width()) - - -class MetadataImporterFilterProxyModel(QtCore.QSortFilterProxyModel): - """ - Extends QSortFilterProxyModel adding filtering functionalities for the metadata importer - """ - def __init__(self, parent=None): - super().__init__(parent) - - ############################################# - # INTERFACE - ################################# - - def filterAcceptsRow(self, sourceRow: int, sourceParent: QtCore.QModelIndex) -> bool: - """ - Overrides filterAcceptsRow to include extra filtering conditions - :type sourceRow: int - :type sourceParent: QModelIndex - :rtype: bool - """ - return sourceParent.isValid() or super().filterAcceptsRow(sourceRow, sourceParent) - - -class MetadataInfoWidget(QtWidgets.QScrollArea): - """ - This class implements the metadata detail widget. - """ - def __init__(self, parent: QtWidgets.QWidget) -> None: - """ - Initialize the metadata info box. - """ - super().__init__(parent) - - self.repository = None - self.entity = None - self.stacked = QtWidgets.QStackedWidget(self) - self.stacked.setContentsMargins(0, 0, 0, 0) - self.infoEmpty = EmptyInfo(self.stacked) - self.infoRepository = RepositoryInfo(self.stacked) - self.infoEntity = EntityInfo(self.stacked) - self.stacked.addWidget(self.infoEmpty) - self.stacked.addWidget(self.infoRepository) - self.stacked.addWidget(self.infoEntity) - self.setContentsMargins(0, 0, 0, 0) - self.setMinimumSize(QtCore.QSize(216, 120)) - self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded) - self.setWidget(self.stacked) - self.setWidgetResizable(True) - - self.setStyleSheet(""" - MetadataInfoWidget { - background: #FFFFFF; - } - MetadataInfoWidget Header { - background: #5A5050; - padding-left: 4px; - color: #FFFFFF; - } - MetadataInfoWidget Key { - background: #BBDEFB; - border-top: none; - border-right: none; - border-bottom: 1px solid #BBDEFB; - border-left: none; - padding: 0 0 0 4px; - } - MetadataInfoWidget Button, - MetadataInfoWidget Button:focus, - MetadataInfoWidget Button:hover, - MetadataInfoWidget Button:hover:focus, - MetadataInfoWidget Button:pressed, - MetadataInfoWidget Button:pressed:focus, - MetadataInfoWidget Text, - MetadataInfoWidget Integer, - MetadataInfoWidget String, - MetadataInfoWidget Select, - MetadataInfoWidget Parent { - background: #E3F2FD; - border-top: none; - border-right: none; - border-bottom: 1px solid #BBDEFB !important; - border-left: 1px solid #BBDEFB !important; - padding: 0 0 0 4px; - text-align:left; - } - MetadataInfoWidget Button::menu-indicator { - image: none; - } - MetadataInfoWidget Select:!editable, - MetadataInfoWidget Select::drop-down:editable { - background: #FFFFFF; - } - MetadataInfoWidget Select:!editable:on, - MetadataInfoWidget Select::drop-down:editable:on { - background: #FFFFFF; - } - MetadataInfoWidget QCheckBox { - background: #FFFFFF; - spacing: 0; - margin-left: 4px; - margin-top: 2px; - } - MetadataInfoWidget QCheckBox::indicator:disabled { - background-color: #BABABA; - } - """) - - scrollbar = self.verticalScrollBar() - scrollbar.installEventFilter(self) - - ############################################# - # EVENTS - ################################# - - def eventFilter(self, source: QtCore.QObject, event: QtCore.QEvent) -> bool: - """ - Filter incoming events. - """ - if source is self.verticalScrollBar(): - if event.type() in {QtCore.QEvent.Show, QtCore.QEvent.Hide}: - self.redraw() - return super().eventFilter(source, event) - - ############################################# - # INTERFACE - ################################# - - def redraw(self) -> None: - """ - Redraw the content of the widget. - """ - width = self.width() - scrollbar = self.verticalScrollBar() - if scrollbar.isVisible(): - width -= scrollbar.width() - widget = self.stacked.currentWidget() - widget.setFixedWidth(width) - sizeHint = widget.sizeHint() - height = sizeHint.height() - self.stacked.setFixedWidth(width) - # self.stacked.setFixedHeight(clamp(height, 0)) - - def stack(self) -> None: - """ - Set the current stacked widget. - """ - if self.entity: - show = self.infoEntity - show.updateData(self.repository, self.entity) - elif self.repository: - show = self.infoRepository - show.updateData(self.repository) - else: - show = self.infoEmpty - - prev = self.stacked.currentWidget() - self.stacked.setCurrentWidget(show) - self.redraw() - if prev is not show: - scrollbar = self.verticalScrollBar() - scrollbar.setValue(0) - - -############################################# -# COMPONENTS -################################# - - -class Header(QtWidgets.QLabel): - """ - This class implements the header of properties section. - """ - def __init__(self, *args: Any) -> None: - """ - Initialize the header. - """ - super().__init__(*args) - self.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignVCenter) - self.setFixedHeight(24) - - -class Key(QtWidgets.QLabel): - """ - This class implements the key of an info field. - """ - def __init__(self, *args: Any) -> None: - """ - Initialize the key. - """ - super().__init__(*args) - self.setFixedSize(88, 20) - - -class Button(QtWidgets.QPushButton): - """ - This class implements the button to which associate a QtWidgets.QMenu instance of an info field. - """ - def __init__(self, *args: Any) -> None: - """ - Initialize the button. - """ - super().__init__(*args) - - -class Integer(IntegerField): - """ - This class implements the integer value of an info field. - """ - def __init__(self, *args: Any) -> None: - """ - Initialize the field. - """ - super().__init__(*args) - self.setFixedHeight(20) - - -class String(StringField): - """ - This class implements the string value of an info field. - """ - def __init__(self, *args: Any) -> None: - """ - Initialize the field. - """ - super().__init__(*args) - self.setFixedHeight(20) - self.setReadOnly(True) - - -class Text(TextField): - """ - This class implements the string value of an info field. - """ - def __init__(self, *args: Any) -> None: - """ - Initialize the field. - """ - super().__init__(*args) - self.setFixedHeight(20 * (self.document().lineCount() + 1)) - self.setReadOnly(True) - - -class Select(ComboBox): - """ - This class implements the selection box of an info field. - """ - def __init__(self, *args: Any) -> None: - """ - Initialize the field. - """ - super().__init__(*args) - self.setSizePolicy(QtWidgets.QSizePolicy.Ignored, QtWidgets.QSizePolicy.Ignored) - self.setFocusPolicy(QtCore.Qt.StrongFocus) - self.setScrollEnabled(False) - - -class Parent(QtWidgets.QWidget): - """ - This class implements the parent placeholder to be used - to store checkbox and radio button value fields. - """ - def __init__(self, *args: Any) -> None: - """ - Initialize the field. - """ - super().__init__(*args) - self.setFixedHeight(20) - - def paintEvent(self, paintEvent: QtGui.QPaintEvent) -> None: - """ - This is needed for the widget to pick the stylesheet. - """ - option = QtWidgets.QStyleOption() - option.initFrom(self) - painter = QtGui.QPainter(self) - style = self.style() - style.drawPrimitive(QtWidgets.QStyle.PE_Widget, option, painter, self) - - -############################################# -# INFO WIDGETS -################################# - -class AbstractInfo(QtWidgets.QWidget): - """ - This class implements the base information box. - """ - __metaclass__ = ABCMeta - - def __init__(self, parent: QtWidgets.QWidget = None) -> None: - """ - Initialize the base information box. - """ - super().__init__(parent) - self.setContentsMargins(0, 0, 0, 0) - - ############################################# - # INTERFACE - ################################# - - @abstractmethod - def updateData(self, **kwargs: Any) -> None: - """ - Fetch new information and fill the widget with data. - """ - pass - - -class RepositoryInfo(AbstractInfo): - """ - This class implements the repository information box. - """ - def __init__(self, parent: QtWidgets.QWidget = None) -> None: - """ - Initialize the repository information box. - """ - super().__init__(parent) - - self.nameKey = Key('Name', self) - self.nameField = String(self) - self.nameField.setReadOnly(True) - - self.uriKey = Key('Location', self) - self.uriField = String(self) - self.uriField.setReadOnly(True) - - self.versionKey = Key('Version', self) - self.versionField = String(self) - self.versionField.setReadOnly(True) - - self.infoHeader = Header('Repository Info', self) - self.infoLayout = QtWidgets.QFormLayout() - self.infoLayout.setSpacing(0) - self.infoLayout.addRow(self.nameKey, self.nameField) - self.infoLayout.addRow(self.uriKey, self.uriField) - self.infoLayout.addRow(self.versionKey, self.versionField) - - self.mainLayout = QtWidgets.QVBoxLayout(self) - self.mainLayout.setAlignment(QtCore.Qt.AlignTop) - self.mainLayout.setContentsMargins(0, 0, 0, 0) - self.mainLayout.setSpacing(0) - self.mainLayout.insertWidget(0, self.infoHeader) - self.mainLayout.insertLayout(1, self.infoLayout) - - ############################################# - # INTERFACE - ################################# - - def updateData(self, repository: Repository) -> None: - """ - Fetch new information and fill the widget with data. - """ - self.nameField.setValue(repository.name) - self.uriField.setValue(repository.uri) - self.versionField.setValue('1.0') - - -class EntityInfo(AbstractInfo): - """ - This class implements the information box for entities. - """ - def __init__(self, parent: QtWidgets.QWidget = None) -> None: - """ - Initialize the generic node information box. - """ - super().__init__(parent) - - self.entity = None - self.repoKey = Key('Repository', self) - self.repoField = String(self) - self.repoField.setReadOnly(True) - - self.idKey = Key('Entity ID', self) - self.idField = String(self) - self.idField.setReadOnly(True) - - self.iriKey = Key('Entity IRI', self) - self.iriField = String(self) - self.iriField.setReadOnly(True) - - self.nodePropHeader = Header('Entity properties', self) - self.nodePropLayout = QtWidgets.QFormLayout() - self.nodePropLayout.setSpacing(0) - self.nodePropLayout.addRow(self.repoKey, self.repoField) - self.nodePropLayout.addRow(self.idKey, self.idField) - self.nodePropLayout.addRow(self.iriKey, self.iriField) - - self.typesHeader = Header('Entity Types', self) - self.typesLayout = QtWidgets.QFormLayout() - self.typesLayout.setSpacing(0) - - self.metadataHeader = Header('Entity Annotations', self) - self.metadataLayout = QtWidgets.QFormLayout() - self.metadataLayout.setSpacing(0) - - self.mainLayout = QtWidgets.QVBoxLayout(self) - self.mainLayout.setAlignment(QtCore.Qt.AlignTop) - self.mainLayout.setContentsMargins(0, 0, 0, 0) - self.mainLayout.setSpacing(0) - self.mainLayout.addWidget(self.nodePropHeader) - self.mainLayout.addLayout(self.nodePropLayout) - self.mainLayout.addWidget(self.typesHeader) - self.mainLayout.addLayout(self.typesLayout) - self.mainLayout.addWidget(self.metadataHeader) - self.mainLayout.addLayout(self.metadataLayout) - - ############################################# - # INTERFACE - ################################# - - def updateData(self, repository: Repository, entity: Entity) -> None: - """ - Fetch new information and fill the widget with data. - """ - self.repoField.setValue(repository.uri) - self.idField.setValue(entity.id) - self.iriField.setValue(entity.name) - - # ENTITY TYPES - while self.typesLayout.rowCount() > 0: - self.typesLayout.removeRow(0) - for t in entity.types: - self.typesLayout.addRow(Key('Type', self), String(t.name, self)) - - # ENTITY ANNOTATIONS - while self.metadataLayout.rowCount() > 0: - self.metadataLayout.removeRow(0) - for a in entity.annotations: - self.metadataLayout.addRow(Key('Property', self), String(a.predicate.n3(), self)) - if isinstance(a.object, LiteralValue): - literal = cast(LiteralValue, a.object) - value, lang, dtype = literal.value, literal.language, literal.datatype - self.metadataLayout.addRow(Key('Value', self), Text(value, self)) - if lang: - self.metadataLayout.addRow(Key('lang', self), String(lang, self)) - if dtype: - self.metadataLayout.addRow(Key('dtype', self), String(dtype.n3(), self)) - else: - self.metadataLayout.addRow(Key('Entity', self), String(a.object.n3(), self)) - self.metadataLayout.addItem(QtWidgets.QSpacerItem(10, 2)) - - -class EmptyInfo(QtWidgets.QTextEdit): - """ - This class implements the information box when there is no metadata repository. - """ - - ############################################# - # INTERFACE - ################################# - - def paintEvent(self, event: QtGui.QPaintEvent): - """ - Overrides paintEvent to display a placeholder text. - """ - super().paintEvent(event) - painter = QtGui.QPainter(self.viewport()) - painter.save() - painter.setPen(self.palette().placeholderText().color()) - fm = self.fontMetrics() - bgMsg = textwrap.dedent(""" - To start using the 'Metadata Importer', add a repository location - in the 'Ontology Manager -> Metadata Repositories' tab. - """) - elided_text = fm.elidedText(bgMsg, QtCore.Qt.ElideRight, self.viewport().width()) - painter.drawText(self.viewport().rect(), QtCore.Qt.AlignCenter, elided_text) - painter.restore() diff --git a/eddy/plugins/metadata-importer/plugin.spec b/eddy/plugins/metadata-importer/plugin.spec deleted file mode 100644 index 5889ebc6..00000000 --- a/eddy/plugins/metadata-importer/plugin.spec +++ /dev/null @@ -1,40 +0,0 @@ -# -*- coding: utf-8 -*- - -########################################################################## -# # -# Eddy: a graphical editor for the specification of Graphol ontologies # -# Copyright (C) 2015 Daniele Pantaleone # -# # -# This program is free software: you can redistribute it and/or modify # -# it under the terms of the GNU General Public License as published by # -# the Free Software Foundation, either version 3 of the License, or # -# (at your option) any later version. # -# # -# This program is distributed in the hope that it will be useful, # -# but WITHOUT ANY WARRANTY; without even the implied warranty of # -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # -# GNU General Public License for more details. # -# # -# You should have received a copy of the GNU General Public License # -# along with this program. If not, see . # -# # -# ##################### ##################### # -# # -# Graphol is developed by members of the DASI-lab group of the # -# Dipartimento di Ingegneria Informatica, Automatica e Gestionale # -# A.Ruberti at Sapienza University of Rome: http://www.dis.uniroma1.it # -# # -# - Domenico Lembo # -# - Valerio Santarelli # -# - Domenico Fabio Savo # -# - Daniele Pantaleone # -# - Marco Console # -# # -########################################################################## - -[plugin] -author: OBDA Systems -contact: info@obdasystems.com -id: metadata_importer -name: Metadata Importer -version: 0.2.0 diff --git a/eddy/ui/iri.py b/eddy/ui/iri.py index d8340a34..1b14945e 100644 --- a/eddy/ui/iri.py +++ b/eddy/ui/iri.py @@ -58,7 +58,7 @@ ) from eddy.core.common import HasWidgetSystem from eddy.core.diagram import Diagram -from eddy.core.functions.signals import connect +from eddy.core.functions.signals import connect, disconnect from eddy.core.items.nodes.attribute import AttributeNode from eddy.core.items.nodes.common.base import AbstractNode from eddy.core.items.nodes.facet import FacetNode @@ -643,13 +643,14 @@ def accept(self): if self.iri: if not str(self.iri) == inputIriString: if len(self.project.iriOccurrences(iri=self.iri)) == 1: - existIRI = self.project.existIRI(inputIriString) - if existIRI: - newIRI = self.project.getIRI(inputIriString, - addLabelFromSimpleName=True, - addLabelFromUserInput=True, - userInput=userExplicitInput, - labelLang=labelLang) + if self.project.existIRI(inputIriString): + newIRI = self.project.getIRI( + inputIriString, + addLabelFromSimpleName=True, + addLabelFromUserInput=True, + userInput=userExplicitInput, + labelLang=labelLang, + ) if newIRI is not self.iri: oldIRI = self.iri self.iri = newIRI @@ -797,6 +798,15 @@ def __init__(self, iri, session, focusOnAnnotations=False): connect(addBtn.clicked, self.addAnnotation) connect(delBtn.clicked, self.removeAnnotation) connect(editBtn.clicked, self.editAnnotation) + ############################## + # Disable edit - add - delete if entity has an external origin annotation + if any([str(a.assertionProperty) == 'urn:x-graphol:origin' + for a in self.iri.annotationAssertions]): + addBtn.setDisabled(True) + delBtn.setDisabled(True) + editBtn.setDisabled(True) + disconnect(table.cellDoubleClicked, self.editAnnotation) + ############################## self.addWidget(addBtn) self.addWidget(delBtn) self.addWidget(editBtn) diff --git a/eddy/ui/ontology.py b/eddy/ui/ontology.py index 12807831..831915e3 100644 --- a/eddy/ui/ontology.py +++ b/eddy/ui/ontology.py @@ -32,8 +32,6 @@ # # ########################################################################## -import textwrap - from PyQt5 import ( QtCore, QtGui, @@ -79,7 +77,6 @@ from eddy.core.functions.misc import first from eddy.core.functions.path import expandPath from eddy.core.functions.signals import connect -from eddy.core.metadata import Repository from eddy.core.ndc import ( ADMS, NDCDataset, @@ -1152,67 +1149,6 @@ def __init__(self, session): self.setDistributionSuggestions() self.setProjectSuggestions() - ############################################# - # METADATA REPOSITORIES - ################################# - - table = QtWidgets.QTableWidget(0, 2, self, objectName='repository_table_widget') - table.setHorizontalHeaderLabels(['Name', 'Endpoint']) - table.horizontalHeader().setStretchLastSection(True) - table.horizontalHeader().setSectionsClickable(False) - table.horizontalHeader().setMinimumSectionSize(100) - table.horizontalHeader().setSectionsClickable(False) - table.verticalHeader().setVisible(False) - table.verticalHeader().setSectionsClickable(False) - table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - self.addWidget(table) - - delBtn = QtWidgets.QPushButton('Remove', objectName='repository_del_button') - delBtn.setEnabled(False) - connect(delBtn.clicked, self.removeRepository) - self.addWidget(delBtn) - - boxlayout = QtWidgets.QHBoxLayout() - boxlayout.setAlignment(QtCore.Qt.AlignCenter) - boxlayout.addWidget(delBtn) - formlayout = QtWidgets.QFormLayout() - formlayout.addRow(self.widget('repository_table_widget')) - formlayout.addRow(boxlayout) - - groupbox = QtWidgets.QGroupBox('Repositories', self) - groupbox.setObjectName('repository_list_groupbox') - groupbox.setLayout(formlayout) - self.addWidget(groupbox) - - nameField = StringField(self, objectName='repository_name_field') - uriField = StringField(self, objectName='repository_uri_field') - addBtn = QtWidgets.QPushButton('Add', objectName='repository_add_button') - connect(addBtn.clicked, self.addRepository) - self.addWidget(nameField) - self.addWidget(uriField) - self.addWidget(addBtn) - - boxlayout = QtWidgets.QHBoxLayout() - boxlayout.setAlignment(QtCore.Qt.AlignCenter) - boxlayout.addWidget(addBtn) - formlayout = QtWidgets.QFormLayout() - formlayout.addRow(QtWidgets.QLabel('Name'), nameField) - formlayout.addRow(QtWidgets.QLabel('URI'), uriField) - formlayout.addRow(boxlayout) - - groupbox = QtWidgets.QGroupBox('Add Repository', self) - groupbox.setObjectName('repository_add_groupbox') - groupbox.setLayout(formlayout) - self.addWidget(groupbox) - - layout = QtWidgets.QVBoxLayout() - layout.addWidget(self.widget('repository_list_groupbox')) - layout.addWidget(self.widget('repository_add_groupbox')) - widget = QtWidgets.QWidget() - widget.setObjectName('repositories_widget') - widget.setLayout(layout) - self.addWidget(widget) - ############################################# # CONFIRMATION BOX ################################# @@ -1235,7 +1171,6 @@ def __init__(self, session): widget.addTab(self.widget('annotations_widget'), 'Annotations') widget.addTab(self.widget('iri_widget'), 'Global IRIs') widget.addTab(self.widget('assertions_widget'), 'Annotation Assertions') - widget.addTab(self.widget('repositories_widget'), 'Metadata Repositories') widget.addTab(self.widget('NDCmetadata_widget'), 'NDC Metadata') self.addWidget(widget) @@ -1439,25 +1374,6 @@ def redraw(self): preField = self.widget('pre_input_field') postField = self.widget('post_input_field') - ############################################# - # METADATA REPOSITORIES - ################################# - - widget = self.widget('repository_table_widget') # type: QtWidgets.QTableWidget - widget.clearContents() - repos = Repository.load() - widget.setRowCount(len(repos)) - for index, repo in enumerate(repos): - nameItem = QtWidgets.QTableWidgetItem(repo.name) - nameItem.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) - uriItem = QtWidgets.QTableWidgetItem(repo.uri) - uriItem.setFlags(QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable) - widget.setItem(index, 0, nameItem) - widget.setItem(index, 1, uriItem) - widget.resizeColumnsToContents() - widget.sortItems(0) - self.widget('repository_del_button').setEnabled(len(repos) > 0) - ############################################# # GENERAL TAB ################################# @@ -2709,80 +2625,3 @@ def doAddMetadata(self): self.session.undostack.endMacro() # self.redraw() self.session.addNotification('Metadata added to the current project!') - - ############################################# - # METADATA REPOSITORIES - ################################# - - @QtCore.pyqtSlot() - def addRepository(self): - """Shows a dialog to insert a add a new repository.""" - nameField = self.widget('repository_name_field') # type: StringField - uriField = self.widget('repository_uri_field') # type: StringField - - # Validate user input - if len(nameField.text()) == 0: - msgBox = QtWidgets.QMessageBox( # noqa - QtWidgets.QMessageBox.Warning, - 'Invalid Repository Name', - 'Please specify a repository name.', - informativeText=textwrap.dedent(""" - The repository name can be any string that is used to easily - reference the repository. - """), - parent=self, - ) - msgBox.open() - elif not QtCore.QUrl(uriField.text()).isValid(): - msgBox = QtWidgets.QMessageBox( # noqa - QtWidgets.QMessageBox.Warning, - 'Invalid Repository URI', - 'Please specify a valid repository URI.', - informativeText=textwrap.dedent(""" - The repository URI is the base path at which the repository API is accessible, - and must include protocol, domain and port (if any). - - e.g.: - https://example.com:5000/ - https://example.com/myrepo/ - """), - parent=self, - ) - msgBox.open() - else: - # Add new repository - repos = Repository.load() - if any(map(lambda r: r.name == nameField.text(), repos)): - msgBox = QtWidgets.QMessageBox( # noqa - QtWidgets.QMessageBox.Warning, - 'Duplicate Repository Error', - f'A repository named {nameField.text()} already exists.', - informativeText=textwrap.dedent(""" - Repository names must be unique to avoid abiguity in the user interface. - """), - parent=self, - ) - msgBox.open() - else: - repos.append(Repository(name=nameField.text(), uri=uriField.text())) - Repository.save(repos) - self.redraw() - - @QtCore.pyqtSlot() - def removeRepository(self): - """Remove selected repositories.""" - # Delete selected repositories - widget = self.widget('repository_table_widget') # type: QtWidgets.QTableWidget - selections = widget.selectedRanges() - for sel in selections: - for row in range(sel.bottomRow(), sel.topRow() + 1): - widget.removeRow(row) - # Save the current repositories list - repos = [] - for row in range(widget.rowCount()): - repos.append(Repository( - name=widget.item(row, 0).text(), - uri=widget.item(row, 1).text(), - )) - Repository.save(repos) - self.redraw()