Skip to content

Commit

Permalink
[processing][FEATURE] Store models inside QGIS project files
Browse files Browse the repository at this point in the history
Allows processing models to be stored inside QGIS project files,
so that opening the project makes that model available.

Some models are so intrinsically linked to the logic inside
a particular project that they have no meaning (or are totally
broken) outside of that project (e.g. models which rely
on the presence of particular map layers, relations, etc)

This change allows these models to be stored inside that project,
avoid cluttering up the "global" model provider with models
which make no sense, and making it easier to distribute a single
project with these models included.

Models are stored inside projects by clicking the new "embed
in project" button in the modeler dialog toolbar. Models can be
removed from a project from the model's right click menu in the
toolbox.
  • Loading branch information
nyalldawson committed Aug 13, 2018
1 parent 3a08300 commit 63fd4ba
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 7 deletions.
4 changes: 3 additions & 1 deletion python/plugins/processing/core/Processing.py
Expand Up @@ -64,6 +64,7 @@

# should be loaded last - ensures that all dependent algorithms are available when loading models
from processing.modeler.ModelerAlgorithmProvider import ModelerAlgorithmProvider # NOQA
from processing.modeler.ProjectProvider import ProjectProvider # NOQA


class Processing(object):
Expand Down Expand Up @@ -92,7 +93,8 @@ def initialize():
GdalAlgorithmProvider,
SagaAlgorithmProvider,
ScriptAlgorithmProvider,
ModelerAlgorithmProvider
ModelerAlgorithmProvider,
ProjectProvider
]:
p = c()
if QgsApplication.processingRegistry().addProvider(p):
Expand Down
28 changes: 24 additions & 4 deletions python/plugins/processing/modeler/DeleteModelAction.py
Expand Up @@ -26,10 +26,13 @@
__revision__ = '$Format:%H$'

import os
from qgis.core import QgsApplication, QgsProcessingModelAlgorithm
from qgis.core import (QgsApplication,
QgsProcessingModelAlgorithm,
QgsProject)
from qgis.PyQt.QtWidgets import QMessageBox
from qgis.PyQt.QtCore import QCoreApplication
from processing.gui.ContextAction import ContextAction
from processing.modeler.ProjectProvider import PROJECT_PROVIDER_ID


class DeleteModelAction(ContextAction):
Expand All @@ -41,12 +44,29 @@ def isEnabled(self):
return isinstance(self.itemData, QgsProcessingModelAlgorithm)

def execute(self):
model = self.itemData
if model is None:
return # shouldn't happen, but let's be safe

project_provider = model.provider().id() == PROJECT_PROVIDER_ID

if project_provider:
msg = self.tr('Are you sure you want to delete this model from the current project?', 'DeleteModelAction')
else:
msg = self.tr('Are you sure you want to delete this model?', 'DeleteModelAction')

reply = QMessageBox.question(
None,
self.tr('Delete Model', 'DeleteModelAction'),
self.tr('Are you sure you want to delete this model?', 'DeleteModelAction'),
msg,
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No)

if reply == QMessageBox.Yes:
os.remove(self.itemData.sourceFilePath())
QgsApplication.processingRegistry().providerById('model').refreshAlgorithms()
if project_provider:
provider = QgsApplication.processingRegistry().providerById(PROJECT_PROVIDER_ID)
provider.remove_model(model)
QgsProject.instance().setDirty(True)
else:
os.remove(model.sourceFilePath())
QgsApplication.processingRegistry().providerById('model').refreshAlgorithms()
37 changes: 35 additions & 2 deletions python/plugins/processing/modeler/ModelerDialog.py
Expand Up @@ -69,6 +69,7 @@
from qgis.core import (Qgis,
QgsApplication,
QgsProcessingAlgorithm,
QgsProject,
QgsSettings,
QgsMessageLog,
QgsProcessingUtils,
Expand All @@ -88,6 +89,7 @@
from processing.modeler.ModelerParametersDialog import ModelerParametersDialog
from processing.modeler.ModelerUtils import ModelerUtils
from processing.modeler.ModelerScene import ModelerScene
from processing.modeler.ProjectProvider import PROJECT_PROVIDER_ID
from qgis.utils import iface


Expand Down Expand Up @@ -245,13 +247,16 @@ def __init__(self, model=None):
except:
pass

self.mToolbar.setIconSize(iface.iconSize())
if iface is not None:
self.mToolbar.setIconSize(iface.iconSize())
self.mActionOpen.setIcon(
QgsApplication.getThemeIcon('/mActionFileOpen.svg'))
self.mActionSave.setIcon(
QgsApplication.getThemeIcon('/mActionFileSave.svg'))
self.mActionSaveAs.setIcon(
QgsApplication.getThemeIcon('/mActionFileSaveAs.svg'))
self.mActionSaveInProject.setIcon(
QgsApplication.getThemeIcon('/mActionFileSaveAs.svg'))
self.mActionZoomActual.setIcon(
QgsApplication.getThemeIcon('/mActionZoomActual.svg'))
self.mActionZoomIn.setIcon(
Expand Down Expand Up @@ -414,6 +419,7 @@ def _mimeDataInput(items):
self.mActionOpen.triggered.connect(self.openModel)
self.mActionSave.triggered.connect(self.save)
self.mActionSaveAs.triggered.connect(self.saveAs)
self.mActionSaveInProject.triggered.connect(self.saveInProject)
self.mActionZoomIn.triggered.connect(self.zoomIn)
self.mActionZoomOut.triggered.connect(self.zoomOut)
self.mActionZoomActual.triggered.connect(self.zoomActual)
Expand Down Expand Up @@ -486,6 +492,23 @@ def save(self):
def saveAs(self):
self.saveModel(True)

def saveInProject(self):
if not self.can_save():
return

self.model.setName(str(self.textName.text()))
self.model.setGroup(str(self.textGroup.text()))
self.model.setSourceFilePath(None)

project_provider = QgsApplication.processingRegistry().providerById(PROJECT_PROVIDER_ID)
project_provider.add_model(self.model)

self.update_model.emit()
self.bar.pushMessage("", self.tr("Model was saved inside current project"), level=Qgis.Success, duration=5)

self.hasChanged = False
QgsProject.instance().setDirty(True)

def zoomIn(self):
self.view.setTransformationAnchor(QGraphicsView.NoAnchor)
point = self.view.mapToScene(QPoint(self.view.viewport().width() / 2, self.view.viewport().height() / 2))
Expand Down Expand Up @@ -620,11 +643,21 @@ def exportAsPython(self):

self.bar.pushMessage("", self.tr("Model was correctly exported as python script"), level=Qgis.Success, duration=5)

def saveModel(self, saveAs):
def can_save(self):
"""
Tests whether a model can be saved, or if it is not yet valid
:return: bool
"""
if str(self.textName.text()).strip() == '':
self.bar.pushWarning(
"", self.tr('Please a enter model name before saving')
)
return False

return True

def saveModel(self, saveAs):
if not self.can_save():
return
self.model.setName(str(self.textName.text()))
self.model.setGroup(str(self.textGroup.text()))
Expand Down
167 changes: 167 additions & 0 deletions python/plugins/processing/modeler/ProjectProvider.py
@@ -0,0 +1,167 @@
# -*- coding: utf-8 -*-

"""
***************************************************************************
ProjectProvider.py
------------------------
Date : July 2018
Copyright : (C) 2018 by Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* 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 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************
"""

__author__ = 'Nyall Dawson'
__date__ = 'July 2018'
__copyright__ = '(C) 2018, Nyall Dawson'

# This will get replaced with a git SHA1 when you do a git archive

__revision__ = '$Format:%H$'

from qgis.core import (Qgis,
QgsApplication,
QgsProcessingProvider,
QgsMessageLog,
QgsProcessingModelAlgorithm,
QgsProject,
QgsXmlUtils)

PROJECT_PROVIDER_ID = 'project'


class ProjectProvider(QgsProcessingProvider):

def __init__(self, project=None):
super().__init__()
if project is None:
self.project = QgsProject.instance()
else:
self.project = project

self.model_definitions = [] # list of maps defining models
self.is_loading = False

# must reload models if providers list is changed - previously unavailable algorithms
# which models depend on may now be available
QgsApplication.processingRegistry().providerAdded.connect(self.on_provider_added)

self.project.readProject.connect(self.read_project)
self.project.writeProject.connect(self.write_project)
self.project.cleared.connect(self.clear)

def on_provider_added(self, _):
self.refreshAlgorithms()

def load(self):
self.refreshAlgorithms()
return True

def clear(self):
"""
Remove all algorithms from the provider
"""
self.model_definitions = []
self.refreshAlgorithms()

def add_model(self, model):
"""
Adds a model to the provider
:type model: QgsProcessingModelAlgorithm
:param model: model to add
"""
definition = model.toVariant()
self.model_definitions.append(definition)
self.refreshAlgorithms()

def remove_model(self, model):
"""
Removes a model from the project
:type model: QgsProcessingModelAlgorithm
:param model: model to remove
"""
if model is None:
return

filtered_model_definitions = []
for m in self.model_definitions:
algorithm = QgsProcessingModelAlgorithm()
if algorithm.loadVariant(m) and algorithm.name() == model.name():
# found matching model definition, skip it
continue
filtered_model_definitions.append(m)

self.model_definitions = filtered_model_definitions
self.refreshAlgorithms()

def read_project(self, doc):
"""
Reads the project model definitions from the project DOM document
:param doc: DOM document
"""
self.model_definitions = []
project_models_nodes = doc.elementsByTagName('projectModels')
if project_models_nodes:
project_models_node = project_models_nodes.at(0)
model_nodes = project_models_node.childNodes()
for n in range(model_nodes.count()):
model_element = model_nodes.at(n).toElement()
definition = QgsXmlUtils.readVariant(model_element)
self.model_definitions.append(definition)

self.refreshAlgorithms()

def write_project(self, doc):
"""
Writes out the project model definitions into the project DOM document
:param doc: DOM document
"""
qgis_nodes = doc.elementsByTagName('qgis')
if not qgis_nodes:
return

qgis_node = qgis_nodes.at(0)
project_models_node = doc.createElement('projectModels')

for a in self.algorithms():
definition = a.toVariant()
element = QgsXmlUtils.writeVariant(definition, doc)
project_models_node.appendChild(element)
qgis_node.appendChild(project_models_node)

def name(self):
return self.tr('Project models', 'ProjectProvider')

def id(self):
return PROJECT_PROVIDER_ID

def icon(self):
return QgsApplication.getThemeIcon("/mIconQgsProjectFile.svg")

def svgIconPath(self):
return QgsApplication.iconPath("mIconQgsProjectFile.svg")

def supportsNonFileBasedOutput(self):
return True

def loadAlgorithms(self):
if self.is_loading:
return
self.is_loading = True

for definition in self.model_definitions:
algorithm = QgsProcessingModelAlgorithm()
if algorithm.loadVariant(definition):
self.addAlgorithm(algorithm)
else:
QgsMessageLog.logMessage(
self.tr('Could not load model from project', 'ProjectProvider'),
self.tr('Processing'), Qgis.Critical)

self.is_loading = False
1 change: 1 addition & 0 deletions python/plugins/processing/tests/CMakeLists.txt
Expand Up @@ -8,6 +8,7 @@ IF(ENABLE_TESTS)
INCLUDE(UsePythonTest)
ADD_PYTHON_TEST(ProcessingGuiTest GuiTest.py)
ADD_PYTHON_TEST(ProcessingModelerTest ModelerTest.py)
ADD_PYTHON_TEST(ProcessingProjectProviderTest ProjectProvider.py)
ADD_PYTHON_TEST(ProcessingToolsTest ToolsTest.py)
ADD_PYTHON_TEST(ProcessingGenericAlgorithmsTest AlgorithmsTestBase.py)
ADD_PYTHON_TEST(ProcessingQgisAlgorithmsTest QgisAlgorithmsTest.py)
Expand Down

0 comments on commit 63fd4ba

Please sign in to comment.