diff --git a/python/core/processing/qgsprocessingoutputs.sip b/python/core/processing/qgsprocessingoutputs.sip index 44a26a742433..2cb435af2296 100644 --- a/python/core/processing/qgsprocessingoutputs.sip +++ b/python/core/processing/qgsprocessingoutputs.sip @@ -40,6 +40,8 @@ class QgsProcessingOutputDefinition sipType = sipType_QgsProcessingOutputString; else if ( sipCpp->type() == QgsProcessingOutputFolder::typeName() ) sipType = sipType_QgsProcessingOutputFolder; + else + sipType = nullptr; %End public: diff --git a/python/core/processing/qgsprocessingparameters.sip b/python/core/processing/qgsprocessingparameters.sip index 29cec7314f85..e0bf143e8959 100644 --- a/python/core/processing/qgsprocessingparameters.sip +++ b/python/core/processing/qgsprocessingparameters.sip @@ -191,6 +191,8 @@ class QgsProcessingParameterDefinition sipType = sipType_QgsProcessingParameterFolderDestination; else if ( sipCpp->type() == QgsProcessingParameterBand::typeName() ) sipType = sipType_QgsProcessingParameterBand; + else + sipType = nullptr; %End public: diff --git a/python/plugins/processing/algs/qgis/FieldsMapper.py b/python/plugins/processing/algs/qgis/FieldsMapper.py index 64c3e4fcf6ec..7bba9b0a3f0b 100644 --- a/python/plugins/processing/algs/qgis/FieldsMapper.py +++ b/python/plugins/processing/algs/qgis/FieldsMapper.py @@ -25,79 +25,76 @@ __revision__ = '$Format:%H$' -from qgis.core import (QgsField, - QgsFields, - QgsExpression, - QgsDistanceArea, - QgsFeatureSink, - QgsProject, - QgsFeature, - QgsApplication, - QgsProcessingUtils) -from processing.algs.qgis.QgisAlgorithm import QgisAlgorithm -from processing.core.GeoAlgorithmExecutionException import GeoAlgorithmExecutionException -from processing.core.parameters import ParameterTable -from processing.core.parameters import Parameter -from processing.core.outputs import OutputVector - - -class FieldsMapper(QgisAlgorithm): +from qgis.core import ( + QgsApplication, + QgsDistanceArea, + QgsExpression, + QgsFeature, + QgsFeatureSink, + QgsField, + QgsFields, + QgsProcessingException, + QgsProcessingParameterDefinition, + QgsProcessingUtils, + QgsProject, +) + +from processing.algs.qgis.QgisAlgorithm import QgisFeatureBasedAlgorithm + + +class FieldsMapper(QgisFeatureBasedAlgorithm): INPUT_LAYER = 'INPUT_LAYER' FIELDS_MAPPING = 'FIELDS_MAPPING' OUTPUT_LAYER = 'OUTPUT_LAYER' - def __init__(self): - GeoAlgorithm.__init__(self) - self.mapping = None - def group(self): return self.tr('Vector table tools') - def __init__(self): - super().__init__() - - def initAlgorithm(self, config=None): - self.addParameter(ParameterTable(self.INPUT_LAYER, - self.tr('Input layer'), - False)) + def initParameters(self, config=None): - class ParameterFieldsMapping(Parameter): + class ParameterFieldsMapping(QgsProcessingParameterDefinition): - default_metadata = { - 'widget_wrapper': 'processing.algs.qgis.ui.FieldsMappingPanel.FieldsMappingWidgetWrapper' - } + def __init__(self, name, description, parentLayerParameterName='INPUT'): + super().__init__(name, description) + self._parentLayerParameter = parentLayerParameterName - def __init__(self, name='', description='', parent=None): - Parameter.__init__(self, name, description) - self.parent = parent - self.value = [] + def type(self): + return 'fields_mapping' - def getValueAsCommandLineParameter(self): - return '"' + str(self.value) + '"' - - def setValue(self, value): - if value is None: + def checkValueIsAcceptable(self, value, context): + if not isinstance(value, list): return False - if isinstance(value, list): - self.value = value - return True - if isinstance(value, str): - try: - self.value = eval(value) - return True - except Exception as e: - # fix_print_with_import - print(str(e)) # display error in console + for field_def in value: + if not isinstance(field_def, dict): + return False + if not field_def.get('name', False): + return False + if not field_def.get('type', False): + return False + if not field_def.get('expression', False): return False - return False + return True - self.addParameter(ParameterFieldsMapping(self.FIELDS_MAPPING, - self.tr('Fields mapping'), - self.INPUT_LAYER)) - self.addOutput(OutputVector(self.OUTPUT_LAYER, - self.tr('Refactored'), - base_input=self.INPUT_LAYER)) + def valueAsPythonString(self, value, context): + return str(value) + + def asScriptCode(self): + raise NotImplementedError() + + @classmethod + def fromScriptCode(cls, name, description, isOptional, definition): + raise NotImplementedError() + + def parentLayerParameter(self): + return self._parentLayerParameter + + fields_mapping = ParameterFieldsMapping(self.FIELDS_MAPPING, + description=self.tr('Fields mapping')) + fields_mapping.setMetadata({ + 'widget_wrapper': 'processing.algs.qgis.ui.FieldsMappingPanel.FieldsMappingWidgetWrapper' + }) + self.addParameter(fields_mapping) def name(self): return 'refactorfields' @@ -105,79 +102,64 @@ def name(self): def displayName(self): return self.tr('Refactor fields') - def processAlgorithm(self, parameters, context, feedback): - layer = self.getParameterValue(self.INPUT_LAYER) - mapping = self.getParameterValue(self.FIELDS_MAPPING) - output = self.getOutputFromName(self.OUTPUT_LAYER) + def outputName(self): + return self.tr('Refactored') - layer = QgsProcessingUtils.mapLayerFromString(layer, context) - fields = QgsFields() - expressions = [] + def parameterAsFieldsMapping(self, parameters, name, context): + return parameters[name] + + def prepareAlgorithm(self, parameters, context, feedback): + source = self.parameterAsSource(parameters, 'INPUT', context) + mapping = self.parameterAsFieldsMapping(parameters, self.FIELDS_MAPPING, context) + + self.fields = QgsFields() + self.expressions = [] da = QgsDistanceArea() - da.setSourceCrs(layer.crs()) + da.setSourceCrs(source.sourceCrs()) da.setEllipsoid(context.project().ellipsoid()) - exp_context = layer.createExpressionContext() - for field_def in mapping: - fields.append(QgsField(field_def['name'], - field_def['type'], - field_def['length'], - field_def['precision'])) - + self.fields.append(QgsField(name=field_def['name'], + type=field_def['type'], + typeName="", + len=field_def.get('length', 0), + prec=field_def.get('precision', 0))) expression = QgsExpression(field_def['expression']) expression.setGeomCalculator(da) expression.setDistanceUnits(context.project().distanceUnits()) expression.setAreaUnits(context.project().areaUnits()) - expression.prepare(exp_context) if expression.hasParserError(): - raise GeoAlgorithmExecutionException( + raise QgsProcessingException( self.tr(u'Parser error in expression "{}": {}') .format(str(expression.expression()), str(expression.parserErrorString()))) - expressions.append(expression) - - writer = output.getVectorWriter(fields, layer.wkbType(), layer.crs(), context) - - # Create output vector layer with new attributes - error_exp = None - inFeat = QgsFeature() - outFeat = QgsFeature() - features = QgsProcessingUtils.getFeatures(layer, context) - count = QgsProcessingUtils.featureCount(layer, context) - if count > 0: - total = 100.0 / count - for current, inFeat in enumerate(features): - rownum = current + 1 - - geometry = inFeat.geometry() - outFeat.setGeometry(geometry) - - attrs = [] - for i in range(0, len(mapping)): - field_def = mapping[i] - expression = expressions[i] - exp_context.setFeature(inFeat) - exp_context.lastScope().setVariable("row_number", rownum) - value = expression.evaluate(exp_context) - if expression.hasEvalError(): - error_exp = expression - break - - attrs.append(value) - outFeat.setAttributes(attrs) - - writer.addFeature(outFeat, QgsFeatureSink.FastInsert) - - feedback.setProgress(int(current * total)) - else: - feedback.setProgress(100) - - del writer - - if error_exp is not None: - raise GeoAlgorithmExecutionException( - self.tr(u'Evaluation error in expression "{}": {}') - .format(str(error_exp.expression()), - str(error_exp.parserErrorString()))) + self.expressions.append(expression) + return True + + def outputFields(self, inputFields): + return self.fields + + def processAlgorithm(self, parameters, context, feeback): + # create an expression context using thead safe processing context + self.expr_context = self.createExpressionContext(parameters, context) + for expression in self.expressions: + expression.prepare(self.expr_context) + self._row_number = 0 + return super().processAlgorithm(parameters, context, feeback) + + def processFeature(self, feature, feedback): + attributes = [] + for expression in self.expressions: + self.expr_context.setFeature(feature) + self.expr_context.lastScope().setVariable("row_number", self._row_number) + value = expression.evaluate(self.expr_context) + if expression.hasEvalError(): + raise QgsProcessingException( + self.tr(u'Evaluation error in expression "{}": {}') + .format(str(expression.expression()), + str(expression.parserErrorString()))) + attributes.append(value) + feature.setAttributes(attributes) + self._row_number += 1 + return feature diff --git a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py index fb062d617d41..6629e62fbe79 100644 --- a/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py +++ b/python/plugins/processing/algs/qgis/QGISAlgorithmProvider.py @@ -160,7 +160,7 @@ # from .SetRasterStyle import SetRasterStyle # from .SelectByAttributeSum import SelectByAttributeSum # from .HypsometricCurves import HypsometricCurves -# from .FieldsMapper import FieldsMapper +from .FieldsMapper import FieldsMapper # from .Datasources2Vrt import Datasources2Vrt # from .OrientedMinimumBoundingBox import OrientedMinimumBoundingBox # from .DefineProjection import DefineProjection @@ -231,6 +231,7 @@ def getAlgs(self): ExtentFromLayer(), ExtractNodes(), ExtractSpecificNodes(), + FieldsMapper(), FixedDistanceBuffer(), FixGeometry(), GeometryByExpression(), diff --git a/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py b/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py index 15bed4f37157..6ec441c3fc7d 100644 --- a/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py +++ b/python/plugins/processing/algs/qgis/ui/FieldsMappingPanel.py @@ -31,17 +31,38 @@ from collections import OrderedDict from qgis.PyQt import uic -from qgis.PyQt.QtGui import QBrush -from qgis.PyQt.QtWidgets import QComboBox, QHeaderView, QLineEdit, QSpacerItem, QMessageBox, QSpinBox, QStyledItemDelegate -from qgis.PyQt.QtCore import QItemSelectionModel, QAbstractTableModel, QModelIndex, QVariant, Qt, pyqtSlot -from qgis.core import (QgsExpression, - QgsProject, - QgsApplication, - QgsProcessingUtils) +from qgis.PyQt.QtCore import ( + QItemSelectionModel, + QAbstractTableModel, + QModelIndex, + QVariant, + Qt, + pyqtSlot, +) +from qgis.PyQt.QtGui import QBrush +from qgis.PyQt.QtWidgets import ( + QComboBox, + QHeaderView, + QLineEdit, + QSpacerItem, + QMessageBox, + QSpinBox, + QStyledItemDelegate, +) + +from qgis.core import ( + QgsApplication, + QgsExpression, + QgsProcessingFeatureSourceDefinition, + QgsProcessingUtils, + QgsProject, + QgsVectorLayer, +) from qgis.gui import QgsFieldExpressionWidget from processing.gui.wrappers import WidgetWrapper, DIALOG_STANDARD, DIALOG_MODELER +from processing.tools import dataobjects pluginPath = os.path.dirname(__file__) WIDGET, BASE = uic.loadUiType( @@ -51,26 +72,49 @@ class FieldsMappingModel(QAbstractTableModel): fieldTypes = OrderedDict([ - (QVariant.Int, "Integer"), + (QVariant.Date, "Date"), + (QVariant.DateTime, "DateTime"), (QVariant.Double, "Double"), - (QVariant.String, "String"), - (QVariant.DateTime, "Date"), - (QVariant.LongLong, "Double"), - (QVariant.Date, "Date")]) - - columns = [ - {'name': 'name', 'type': QVariant.String}, - {'name': 'type', 'type': QVariant.Type}, - {'name': 'length', 'type': QVariant.Int}, - {'name': 'precision', 'type': QVariant.Int}, - # {'name': 'comment', 'type': QVariant.String}, - {'name': 'expression', 'type': QgsExpression}] + (QVariant.Int, "Integer"), + (QVariant.LongLong, "Integer64"), + (QVariant.String, "String")]) def __init__(self, parent=None): super(FieldsMappingModel, self).__init__(parent) self._mapping = [] self._errors = [] self._layer = None + self.configure() + + def configure(self): + self.columns = [{ + 'name': 'expression', + 'type': QgsExpression, + 'header': self.tr("Source expression"), + 'persistentEditor': True + }, { + 'name': 'name', + 'type': QVariant.String, + 'header': self.tr("Field name") + }, { + 'name': 'type', + 'type': QVariant.Type, + 'header': self.tr("Type"), + 'persistentEditor': True + }, { + 'name': 'length', + 'type': QVariant.Int, + 'header': self.tr("Length") + }, { + 'name': 'precision', + 'type': QVariant.Int, + 'header': self.tr("Precision") + }] + + def columnIndex(self, column_name): + for index, column in enumerate(self.columns): + if column['name'] == column_name: + return index def mapping(self): return self._mapping @@ -78,36 +122,8 @@ def mapping(self): def setMapping(self, value): self.beginResetModel() self._mapping = value - self.testAllExpressions() self.endResetModel() - def testAllExpressions(self): - self._errors = [None for i in range(len(self._mapping))] - for row in range(len(self._mapping)): - self.testExpression(row) - - def testExpression(self, row): - self._errors[row] = None - field = self._mapping[row] - exp_context = self.contextGenerator().createExpressionContext() - - expression = QgsExpression(field['expression']) - expression.prepare(exp_context) - if expression.hasParserError(): - self._errors[row] = expression.parserErrorString() - return - - # test evaluation on the first feature - if self._layer is None: - return - for feature in self._layer.getFeatures(): - exp_context.setFeature(feature) - exp_context.lastScope().setVariable("row_number", 1) - expression.evaluate(exp_context) - if expression.hasEvalError(): - self._errors[row] = expression.evalErrorString() - break - def contextGenerator(self): if self._layer: return self._layer @@ -118,7 +134,6 @@ def layer(self): def setLayer(self, layer): self._layer = layer - self.testAllExpressions() def columnCount(self, parent=QModelIndex()): if parent.isValid(): @@ -133,70 +148,45 @@ def rowCount(self, parent=QModelIndex()): def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: - return self.columns[section]['name'].title() + return self.columns[section]['header'] if orientation == Qt.Vertical: return section def flags(self, index): - flags = (Qt.ItemIsSelectable | - Qt.ItemIsEditable | - Qt.ItemIsEnabled) - - return Qt.ItemFlags(flags) + return Qt.ItemFlags(Qt.ItemIsSelectable | + Qt.ItemIsEditable | + Qt.ItemIsEnabled) def data(self, index, role=Qt.DisplayRole): - column = index.column() + field = self._mapping[index.row()] + column_def = self.columns[index.column()] if role == Qt.DisplayRole: - field = self._mapping[index.row()] - column_def = self.columns[column] value = field[column_def['name']] - - fieldType = column_def['type'] - if fieldType == QVariant.Type: + if column_def['type'] == QVariant.Type: if value == QVariant.Invalid: return '' return self.fieldTypes[value] return value if role == Qt.EditRole: - field = self._mapping[index.row()] - column_def = self.columns[column] - value = field[column_def['name']] - return value + return field[column_def['name']] if role == Qt.TextAlignmentRole: - fieldType = self.columns[column]['type'] - if fieldType in [QVariant.Int]: + if column_def['type'] in [QVariant.Int]: hAlign = Qt.AlignRight else: hAlign = Qt.AlignLeft return hAlign + Qt.AlignVCenter - if role == Qt.ForegroundRole: - column_def = self.columns[column] - if column_def['name'] == 'expression': - brush = QBrush() - if self._errors[index.row()]: - brush.setColor(Qt.red) - else: - brush.setColor(Qt.black) - return brush - - if role == Qt.ToolTipRole: - column_def = self.columns[column] - if column_def['name'] == 'expression': - return self._errors[index.row()] - def setData(self, index, value, role=Qt.EditRole): + field = self._mapping[index.row()] + column_def = self.columns[index.column()] + if role == Qt.EditRole: - field = self._mapping[index.row()] - column = index.column() - column_def = self.columns[column] field[column_def['name']] = value - if column_def['name'] == 'expression': - self.testExpression(index.row()) self.dataChanged.emit(index, index) + return True def insertRows(self, row, count, index=QModelIndex()): @@ -205,8 +195,6 @@ def insertRows(self, row, count, index=QModelIndex()): for i in range(count): field = self.newField() self._mapping.insert(row + i, field) - self._errors.insert(row + i, None) - self.testExpression(row) self.endInsertRows() return True @@ -216,7 +204,6 @@ def removeRows(self, row, count, index=QModelIndex()): for i in range(row + count - 1, row + 1): self._mapping.pop(i) - self._errors.pop(i) self.endRemoveRows() return True @@ -243,79 +230,57 @@ def loadLayerFields(self, layer): dp = layer.dataProvider() for field in dp.fields(): self._mapping.append(self.newField(field)) - self.testAllExpressions() self.endResetModel() -class FieldDelegate(QStyledItemDelegate): - - def __init__(self, parent=None): - super(FieldDelegate, self).__init__(parent) +class FieldTypeDelegate(QStyledItemDelegate): def createEditor(self, parent, option, index): - column = index.column() + editor = QComboBox(parent) + for key, text in list(FieldsMappingModel.fieldTypes.items()): + editor.addItem(text, key) + return editor + + def setEditorData(self, editor, index): + if not editor: + return + value = index.model().data(index, Qt.EditRole) + editor.setCurrentIndex(editor.findData(value)) - fieldType = FieldsMappingModel.columns[column]['type'] - if fieldType == QVariant.Type: - editor = QComboBox(parent) - for key, text in list(FieldsMappingModel.fieldTypes.items()): - editor.addItem(text, key) + def setModelData(self, editor, model, index): + if not editor: + return + value = editor.currentData() + if value is None: + value = QVariant.Invalid + model.setData(index, value) - elif fieldType == QgsExpression: - editor = QgsFieldExpressionWidget(parent) - editor.setLayer(index.model().layer()) - editor.registerExpressionContextGenerator(index.model().contextGenerator()) - editor.fieldChanged.connect(self.on_expression_fieldChange) - else: - editor = QStyledItemDelegate.createEditor(self, parent, option, index) +class ExpressionDelegate(QStyledItemDelegate): + def createEditor(self, parent, option, index): + editor = QgsFieldExpressionWidget(parent) + editor.setLayer(index.model().layer()) + editor.registerExpressionContextGenerator(index.model().contextGenerator()) + editor.fieldChanged.connect(self.on_expression_fieldChange) editor.setAutoFillBackground(True) return editor def setEditorData(self, editor, index): if not editor: return - - column = index.column() value = index.model().data(index, Qt.EditRole) - - fieldType = FieldsMappingModel.columns[column]['type'] - if fieldType == QVariant.Type: - editor.setCurrentIndex(editor.findData(value)) - - elif fieldType == QgsExpression: - editor.setField(value) - - else: - QStyledItemDelegate.setEditorData(self, editor, index) + editor.setField(value) def setModelData(self, editor, model, index): if not editor: return - - column = index.column() - - fieldType = FieldsMappingModel.columns[column]['type'] - if fieldType == QVariant.Type: - value = editor.currentData() - if value is None: - value = QVariant.Invalid + (value, isExpression, isValid) = editor.currentField() + if isExpression is True: model.setData(index, value) - - elif fieldType == QgsExpression: - (value, isExpression, isValid) = editor.currentField() - if isExpression is True: - model.setData(index, value) - else: - model.setData(index, QgsExpression.quotedColumnRef(value)) - else: - QStyledItemDelegate.setModelData(self, editor, model, index) - - def updateEditorGeometry(self, editor, option, index): - editor.setGeometry(option.rect) + model.setData(index, QgsExpression.quotedColumnRef(value)) def on_expression_fieldChange(self, fieldName): self.commitData.emit(self.sender()) @@ -333,27 +298,40 @@ def __init__(self, parent=None): self.downButton.setIcon(QgsApplication.getThemeIcon('/mActionArrowDown.svg')) self.resetButton.setIcon(QgsApplication.getThemeIcon('/mIconClearText.svg')) - self.model = FieldsMappingModel() - self.fieldsView.setModel(self.model) + self.configure() + self.model.modelReset.connect(self.on_model_modelReset) self.model.rowsInserted.connect(self.on_model_rowsInserted) - self.fieldsView.setItemDelegate(FieldDelegate()) self.updateLayerCombo() + def configure(self): + self.model = FieldsMappingModel() + self.fieldsView.setModel(self.model) + + self.setDelegate('expression', ExpressionDelegate(self)) + self.setDelegate('type', FieldTypeDelegate(self)) + + def setDelegate(self, column_name, delegate): + self.fieldsView.setItemDelegateForColumn( + self.model.columnIndex(column_name), + delegate) + def setLayer(self, layer): self.model.setLayer(layer) + if layer is None: + return if self.model.rowCount() == 0: self.on_resetButton_clicked() - else: - dlg = QMessageBox(self) - dlg.setText("Do you want to reset the field mapping?") - dlg.setStandardButtons( - QMessageBox.StandardButtons(QMessageBox.Yes | - QMessageBox.No)) - dlg.setDefaultButton(QMessageBox.No) - if dlg.exec_() == QMessageBox.Yes: - self.on_resetButton_clicked() + return + dlg = QMessageBox(self) + dlg.setText("Do you want to reset the field mapping?") + dlg.setStandardButtons( + QMessageBox.StandardButtons(QMessageBox.Yes | + QMessageBox.No)) + dlg.setDefaultButton(QMessageBox.No) + if dlg.exec_() == QMessageBox.Yes: + self.on_resetButton_clicked() def value(self): return self.model.mapping() @@ -366,11 +344,13 @@ def on_addButton_clicked(self, checked=False): rowCount = self.model.rowCount() self.model.insertRows(rowCount, 1) index = self.model.index(rowCount, 0) - self.fieldsView.selectionModel().select(index, - QItemSelectionModel.SelectionFlags(QItemSelectionModel.Clear | - QItemSelectionModel.Select | - QItemSelectionModel.Current | - QItemSelectionModel.Rows)) + self.fieldsView.selectionModel().select( + index, + QItemSelectionModel.SelectionFlags( + QItemSelectionModel.Clear | + QItemSelectionModel.Select | + QItemSelectionModel.Current | + QItemSelectionModel.Rows)) self.fieldsView.scrollTo(index) self.fieldsView.scrollTo(index) @@ -404,11 +384,13 @@ def on_upButton_clicked(self, checked=False): self.model.removeRows(row + 1, 1) - sel.select(self.model.index(row - 1, 0), - QItemSelectionModel.SelectionFlags(QItemSelectionModel.Clear | - QItemSelectionModel.Select | - QItemSelectionModel.Current | - QItemSelectionModel.Rows)) + sel.select( + self.model.index(row - 1, 0), + QItemSelectionModel.SelectionFlags( + QItemSelectionModel.Clear | + QItemSelectionModel.Select | + QItemSelectionModel.Current | + QItemSelectionModel.Rows)) @pyqtSlot(bool, name='on_downButton_clicked') def on_downButton_clicked(self, checked=False): @@ -430,38 +412,36 @@ def on_downButton_clicked(self, checked=False): self.model.removeRows(row, 1) - sel.select(self.model.index(row + 1, 0), - QItemSelectionModel.SelectionFlags(QItemSelectionModel.Clear | - QItemSelectionModel.Select | - QItemSelectionModel.Current | - QItemSelectionModel.Rows)) + sel.select( + self.model.index(row + 1, 0), + QItemSelectionModel.SelectionFlags( + QItemSelectionModel.Clear | + QItemSelectionModel.Select | + QItemSelectionModel.Current | + QItemSelectionModel.Rows)) @pyqtSlot(bool, name='on_resetButton_clicked') def on_resetButton_clicked(self, checked=False): self.model.loadLayerFields(self.model.layer()) - self.openPersistentEditor( - self.model.index(0, 0), - self.model.index(self.model.rowCount() - 1, - self.model.columnCount() - 1)) - self.resizeColumns() def resizeColumns(self): header = self.fieldsView.horizontalHeader() header.resizeSections(QHeaderView.ResizeToContents) for section in range(header.count()): size = header.sectionSize(section) - fieldType = FieldsMappingModel.columns[section]['type'] + fieldType = self.model.columns[section]['type'] if fieldType == QgsExpression: header.resizeSection(section, size + 100) else: header.resizeSection(section, size + 20) - def openPersistentEditor(self, topLeft, bottomRight): - return - for row in range(topLeft.row(), bottomRight.row() + 1): - for column in range(topLeft.column(), bottomRight.column() + 1): - self.fieldsView.openPersistentEditor(self.model.index(row, column)) - editor = self.fieldsView.indexWidget(self.model.index(row, column)) + def openPersistentEditors(self, row): + for index, column in enumerate(self.model.columns): + if 'persistentEditor' in column.keys() and column['persistentEditor']: + self.fieldsView.openPersistentEditor(self.model.index(row, index)) + continue + + editor = self.fieldsView.indexWidget(self.model.index(row, index)) if isinstance(editor, QLineEdit): editor.deselect() if isinstance(editor, QSpinBox): @@ -469,10 +449,14 @@ def openPersistentEditor(self, topLeft, bottomRight): lineEdit.setAlignment(Qt.AlignRight or Qt.AlignVCenter) lineEdit.deselect() + def on_model_modelReset(self): + for row in range(0, self.model.rowCount()): + self.openPersistentEditors(row) + self.resizeColumns() + def on_model_rowsInserted(self, parent, start, end): - self.openPersistentEditor( - self.model.index(start, 0), - self.model.index(end, self.model.columnCount() - 1)) + for row in range(start, end + 1): + self.openPersistentEditors(row) def updateLayerCombo(self): layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance()) @@ -489,18 +473,19 @@ def on_loadLayerFieldsButton_clicked(self, checked=False): class FieldsMappingWidgetWrapper(WidgetWrapper): + def __init__(self, *args, **kwargs): + super(FieldsMappingWidgetWrapper, self).__init__(*args, **kwargs) + self._layer = None + def createWidget(self): return FieldsMappingPanel() def postInitialize(self, wrappers): for wrapper in wrappers: - if wrapper.param.name == self.param.parent: + if wrapper.param.name() == self.param.parentLayerParameter(): + self.setLayer(wrapper.value()) wrapper.widgetValueHasChanged.connect(self.parentLayerChanged) break - layers = QgsProcessingUtils.compatibleVectorLayers(QgsProject.instance()) - if len(layers) > 0: - # as first item in combobox is already selected - self.widget.setLayer(layers[0]) # remove exiting spacers to get FieldsMappingPanel fully expanded if self.dialogType in (DIALOG_STANDARD, DIALOG_MODELER): @@ -509,8 +494,21 @@ def postInitialize(self, wrappers): if isinstance(spacer, QSpacerItem): layout.removeItem(spacer) - def parentLayerChanged(self): - self.widget.setLayer(self.sender().value()) + def parentLayerChanged(self, layer=None): + self.setLayer(self.sender().value()) + + def setLayer(self, layer): + context = dataobjects.createContext() + if layer == self._layer: + return + if isinstance(layer, QgsProcessingFeatureSourceDefinition): + layer, ok = layer.source.valueAsString(context.expressionContext()) + if isinstance(layer, str): + layer = QgsProcessingUtils.mapLayerFromString(layer, context) + if not isinstance(layer, QgsVectorLayer): + layer = None + self._layer = layer + self.widget.setLayer(self._layer) def setValue(self, value): self.widget.setValue(value) diff --git a/python/plugins/processing/tests/testdata/expected/refactorfields.gfs b/python/plugins/processing/tests/testdata/expected/refactorfields.gfs new file mode 100644 index 000000000000..2c17f8b03750 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/refactorfields.gfs @@ -0,0 +1,32 @@ + + + refactorfields + refactorfields + + 6 + EPSG:4326 + + 4 + 0.00000 + 9.00000 + -1.00000 + 6.00000 + + + Bname + Bname + String + 19 + + + Bintval + Bintval + Integer + + + Bfloatval + Bfloatval + Real + + + diff --git a/python/plugins/processing/tests/testdata/expected/refactorfields.gml b/python/plugins/processing/tests/testdata/expected/refactorfields.gml new file mode 100644 index 000000000000..b3f88b16cc81 --- /dev/null +++ b/python/plugins/processing/tests/testdata/expected/refactorfields.gml @@ -0,0 +1,46 @@ + + + + + 0-1 + 96 + + + + + + 2,1 2,2 3,2 3,3 4,3 4,1 2,1 + 0 + multipolys.0 - Test + 2 + 0.246 + + + + + 7,-1 8,-1 8,3 7,3 7,-17,6 7,5 7,4 8,4 9,5 9,6 7,6 + 1 + + + + + 0,0 0,1 1,1 1,0 0,0 + 2 + multipolys.2 - Test + 3 + -0.246 + + + + + 3 + multipolys.3 - Test + 4 + 0 + + + diff --git a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml index 2361db61ca37..2a97476caf14 100644 --- a/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml +++ b/python/plugins/processing/tests/testdata/qgis_algorithm_tests.yaml @@ -2086,6 +2086,38 @@ tests: geometry: precision: 7 + - algorithm: qgis:refactorfields + name: refactor fields + params: + INPUT: + name: multipolys.gml + type: vector + FIELDS_MAPPING: + - expression: '@row_number' + name: 'row_number' + type: 2 + length: 0 + precision: 0 + - expression: > + "fid" || ' - ' || "Bname" + name: 'Bname' + type: 10 + length: 30 + precision: 0 + - expression: '"Bintval" + 1' + name: 'Bintval' + type: 2 + length: 0 + precision: 0 + - expression: '"Bfloatval" * 2' + name: 'Bfloatval' + type: 6 + length: 0 + precision: 0 + results: + OUTPUT: + name: expected/refactorfields.gml + type: vector - algorithm: native:reprojectlayer name: reproject vector layer diff --git a/src/core/processing/qgsprocessingoutputs.h b/src/core/processing/qgsprocessingoutputs.h index 5157d2c2ca8b..139ba90e6dbc 100644 --- a/src/core/processing/qgsprocessingoutputs.h +++ b/src/core/processing/qgsprocessingoutputs.h @@ -55,6 +55,8 @@ class CORE_EXPORT QgsProcessingOutputDefinition sipType = sipType_QgsProcessingOutputString; else if ( sipCpp->type() == QgsProcessingOutputFolder::typeName() ) sipType = sipType_QgsProcessingOutputFolder; + else + sipType = nullptr; SIP_END #endif diff --git a/src/core/processing/qgsprocessingparameters.h b/src/core/processing/qgsprocessingparameters.h index 663627b6dc7d..aec0eee7ddb9 100644 --- a/src/core/processing/qgsprocessingparameters.h +++ b/src/core/processing/qgsprocessingparameters.h @@ -233,6 +233,8 @@ class CORE_EXPORT QgsProcessingParameterDefinition sipType = sipType_QgsProcessingParameterFolderDestination; else if ( sipCpp->type() == QgsProcessingParameterBand::typeName() ) sipType = sipType_QgsProcessingParameterBand; + else + sipType = nullptr; SIP_END #endif