Beispiel #1
0
 def new_context(self):
     feature = QgsFeature()
     fields = QgsFields()
     fields.append(QgsField("testfield", QVariant.Int))
     feature.setFields(fields, True)
     feature["testfield"] = 20
     context = QgsExpressionContextUtils.createFeatureBasedContext(feature, fields)
     return context
 def test_MatchesTrueForFields(self):
     feature = QgsFeature()
     fields = QgsFields()
     fields.append(QgsField("testfield", QVariant.Int))
     feature.setFields(fields, True)
     feature["testfield"] = 20
     style = QgsConditionalStyle('"testfield" = @value')
     context = QgsExpressionContextUtils.createFeatureBasedContext(feature, fields)
     assert style.matches(20, context)
Beispiel #3
0
 def new_context(self):
     feature = QgsFeature()
     fields = QgsFields()
     fields.append(QgsField("testfield", QVariant.Int))
     feature.setFields(fields, True)
     feature["testfield"] = 20
     context = QgsExpressionContextUtils.createFeatureBasedContext(
         feature, fields)
     return context
Beispiel #4
0
 def testExpression(self, row):
     self._errors[row] = None
     field = self._mapping[row]
     expression = QgsExpression(field['expression'])
     if expression.hasParserError():
         self._errors[row] = expression.parserErrorString()
         return
     if self._layer is None:
         return
     context = QgsExpressionContextUtils.createFeatureBasedContext(QgsFeature(), self._layer.fields())
     for feature in self._layer.getFeatures():
         context.setFeature(feature)
         expression.evaluate(context)
         if expression.hasEvalError():
             self._errors[row] = expression.evalErrorString()
             return
         break
Beispiel #5
0
 def testExpression(self, row):
     self._errors[row] = None
     field = self._mapping[row]
     expression = QgsExpression(field['expression'])
     if expression.hasParserError():
         self._errors[row] = expression.parserErrorString()
         return
     if self._layer is None:
         return
     context = QgsExpressionContextUtils.createFeatureBasedContext(QgsFeature(), self._layer.fields())
     for feature in self._layer.getFeatures():
         context.setFeature(feature)
         expression.evaluate(context)
         if expression.hasEvalError():
             self._errors[row] = expression.evalErrorString()
             return
         break
 def test_MatchesReturnsTrueForComplexMatch(self):
     style = QgsConditionalStyle("@value > 10 and @value = 20")
     context = QgsExpressionContextUtils.createFeatureBasedContext(QgsFeature(), QgsFields())
     assert style.matches(20, context)
Beispiel #7
0
 def test_MatchesReturnsTrueForComplexMatch(self):
     style = QgsConditionalStyle("@value > 10 and @value = 20")
     context = QgsExpressionContextUtils.createFeatureBasedContext(QgsFeature(), QgsFields())
     assert style.matches(20, context)
Beispiel #8
0
def feature_validator(feature, layer):
    """Validate a QGIS feature by checking QGIS fields constraints

    The logic here is to:
    - if geometry is not None check if geometry type matches the layer type
    - loop through the fields and check for constraints:
        - NOT NULL, skip the next check if this fails
        - UNIQUE (only if not NULL)
        - EXPRESSION (QgsExpression configured in the form), always evaluated,
          even in case of NULLs

    Note: only hard constraints are checked!

    :param feature: QGIS feature
    :type feature: QgsFeature
    :param layer: QGIS layer
    :type layer: QgsVectorLayer
    :return: a dictionary of errors for each field + geometry
    :rtype: dict
    """

    errors = dict()
    geometry = feature.geometry()

    data_provider = layer.dataProvider()

    def _has_default_value(field_index, field):
        return (
            # Provider level
            data_provider.defaultValueClause(field_index)
            or data_provider.defaultValue(field_index)
            or field.defaultValueDefinition().isValid())

    # Check geometry type
    if not geometry.isNull() and geometry.wkbType() != layer.wkbType():
        if not (geometry.wkbType() == QgsWkbTypes.Point25D
                and layer.wkbType() == QgsWkbTypes.PointZ
                or geometry.wkbType() == QgsWkbTypes.Polygon25D
                and layer.wkbType() == QgsWkbTypes.PolygonZ
                or geometry.wkbType() == QgsWkbTypes.LineString25D
                and layer.wkbType() == QgsWkbTypes.LineStringZ
                or geometry.wkbType() == QgsWkbTypes.MultiPoint25D
                and layer.wkbType() == QgsWkbTypes.MultiPointZ
                or geometry.wkbType() == QgsWkbTypes.MultiPolygon25D
                and layer.wkbType() == QgsWkbTypes.MultiPolygonZ
                or geometry.wkbType() == QgsWkbTypes.MultiLineString25D
                and layer.wkbType() == QgsWkbTypes.MultiLineStringZ):

            errors['geometry'] = _(
                'Feature geometry type %s does not match layer type: %s') % (
                    QgsWkbTypes.displayString(geometry.wkbType()),
                    QgsWkbTypes.displayString(layer.wkbType()))

    def _set_error(field_name, error):
        if not field_name in errors:
            errors[field_name] = []
        errors[field_name].append(error)

    # Check fields "hard" constraints
    for field_index in range(layer.fields().count()):

        field = layer.fields().field(field_index)

        # check if fields is a join field:
        if layer.fields().fieldOrigin(field_index) == QgsFields.OriginJoin:
            continue

        # Check not null first, if it fails skip other tests (unique and expression)
        value = feature.attribute(field.name())
        # If there is a default value we assume it's not NULL (we cannot really know at this point
        # what will be the result of the default value clause evaluation, it might even be provider-side
        if (value is None or value
                == QVariant()) and not _has_default_value(field_index, field):
            not_null = (field.constraints().constraintOrigin(
                QgsFieldConstraints.ConstraintNotNull) !=
                        QgsFieldConstraints.ConstraintOriginNotSet
                        and field.constraints().constraintStrength(
                            QgsFieldConstraints.ConstraintNotNull)
                        == QgsFieldConstraints.ConstraintStrengthHard)
            if not_null:
                _set_error(field.name(), _('Field value must be NOT NULL'))
                continue

        value = feature.attribute(field_index)

        # Skip if NULL, not sure if we want to continue in this case but it seems pointless
        # to check for unique or type compatibility on NULLs
        if value is not None and value != QVariant():

            if not QVariant(value).convert(field.type()):
                _set_error(
                    field.name(),
                    _('Field value \'%s\' cannot be converted to %s') %
                    (value, QVariant.typeToName(field.type())))

            unique = (field.constraints().constraintOrigin(
                QgsFieldConstraints.ConstraintUnique) !=
                      QgsFieldConstraints.ConstraintOriginNotSet
                      and field.constraints().constraintStrength(
                          QgsFieldConstraints.ConstraintUnique)
                      == QgsFieldConstraints.ConstraintStrengthHard)

            if unique:
                # Search for features, excluding self if it's an update
                request = QgsFeatureRequest()
                request.setNoAttributes()
                request.setFlags(QgsFeatureRequest.NoGeometry)
                request.setLimit(2)
                if field.isNumeric():
                    request.setFilterExpression(
                        '"%s" = %s' %
                        (field.name().replace('"', '\\"'), value))
                elif field.type() == QVariant.String:
                    request.setFilterExpression(
                        '"%s" = \'%s\'' % (field.name().replace(
                            '"', '\\"'), value.replace("'", "\\'")))
                elif field.type() == QVariant.Date:
                    request.setFilterExpression(
                        'to_date("%s") = \'%s\'' % (field.name().replace(
                            '"', '\\"'), value.toString(Qt.ISODate)))
                elif field.type() == QVariant.DateTime:
                    request.setFilterExpression(
                        'to_datetime("{field_name}") = \'{date_time_string}\' OR to_datetime("{field_name}") = \'{date_time_string}.000\''
                        .format(field_name=field.name().replace('"', '\\"'),
                                date_time_string=value.toString(Qt.ISODate)))
                elif field.type(
                ) == QVariant.Bool:  # This does not make any sense, but still
                    request.setFilterExpression(
                        '"%s" = %s' % (field.name().replace(
                            '"', '\\"'), 'true' if value else 'false'))
                else:  # All the other formats: let's convert to string and hope for the best
                    request.setFilterExpression(
                        '"%s" = \'%s\'' %
                        (field.name().replace('"', '\\"'), value.toString()))

                # Exclude same feature by id
                found = [
                    f.id() for f in layer.getFeatures(request)
                    if f.id() != feature.id()
                ]
                if len(found) > 0:
                    _set_error(field.name(), _('Field value must be UNIQUE'))

        # Check for expressions, even in case of NULL because expressions may want to check for combined
        # conditions on multiple fields
        expression = (field.constraints().constraintOrigin(
            QgsFieldConstraints.ConstraintExpression) !=
                      QgsFieldConstraints.ConstraintOriginNotSet
                      and field.constraints().constraintStrength(
                          QgsFieldConstraints.ConstraintExpression)
                      == QgsFieldConstraints.ConstraintStrengthHard)
        if expression:
            constraints = field.constraints()
            expression = constraints.constraintExpression()
            description = constraints.constraintDescription()
            value = feature.attribute(field_index)
            exp = QgsExpression(expression)
            context = QgsExpressionContextUtils.createFeatureBasedContext(
                feature, layer.fields())
            context.appendScopes(
                QgsExpressionContextUtils.globalProjectLayerScopes(layer))
            if not bool(exp.evaluate(context)):
                if not description:
                    description = _('Expression check violation')
                _set_error(field.name(),
                           _("%s Expression: %s") % (description, expression))

    return errors
Beispiel #9
0
    def save_vector_data(self,
                         metadata_layer,
                         post_layer_data,
                         has_transactions,
                         post_save_signal=True,
                         **kwargs):
        """Save vector editing data

        :param metadata_layer: metadata of the layer being edited
        :type metadata_layer: MetadataVectorLayer
        :param post_layer_data: post data with 'add', 'delete' etc.
        :type post_layer_data: dict
        :param has_transactions: true if the layer support transactions
        :type has_transactions: bool
        :param post_save_signal: if this is a post_save_signal call, defaults to True
        :type post_save_signal: bool, optional
        """

        # Check atomic capabilities for validation
        # -----------------------------------------------
        #for mode_editing in (EDITING_POST_DATA_ADDED, EDITING_POST_DATA_UPDATED, EDITING_POST_DATA_DELETED):

        # try to get layer model object from metatada_layer
        layer = getattr(metadata_layer, 'layer', self.layer)

        if EDITING_POST_DATA_ADDED in post_layer_data and len(
                post_layer_data[EDITING_POST_DATA_ADDED]) > 0:
            if not self.request.user.has_perm('qdjango.add_feature', layer):
                raise ValidationError(
                    _('Sorry but your user doesn\'t has \'Add Feature\' capability'
                      ))

        if EDITING_POST_DATA_DELETED in post_layer_data and len(
                post_layer_data[EDITING_POST_DATA_DELETED]) > 0:
            if not self.request.user.has_perm('qdjango.delete_feature', layer):
                raise ValidationError(
                    _('Sorry but your user doesn\'t has \'Delete Feature\' capability'
                      ))

        if EDITING_POST_DATA_UPDATED in post_layer_data and len(
                post_layer_data[EDITING_POST_DATA_UPDATED]) > 0:
            if not self.request.user.has_perm('qdjango.change_feature', layer) and \
                    not self.request.user.has_perm('qdjango.change_attr_feature', layer):
                raise ValidationError(
                    _('Sorry but your user doesn\'t has \'Change or Change Attributes Features\' capability'
                      ))

        # get initial featurelocked
        metadata_layer.lock.getInitialFeatureLockedIds()

        # get lockids from client
        metadata_layer.lock.setLockeFeaturesFromClient(
            post_layer_data['lockids'])

        # data for response
        insert_ids = list()
        lock_ids = list()

        # FIXME: check this out
        # for add check if is a metadata_layer and referenced field is a pk
        is_referenced_field_is_pk = 'referenced_layer_insert_ids' in kwargs and kwargs['referenced_layer_insert_ids'] \
            and hasattr(metadata_layer, 'referenced_field_is_pk') \
            and metadata_layer.referenced_field_is_pk

        # Get the layer
        qgis_layer = metadata_layer.qgis_layer

        for mode_editing in (EDITING_POST_DATA_ADDED,
                             EDITING_POST_DATA_UPDATED):

            if mode_editing in post_layer_data:

                for geojson_feature in post_layer_data[mode_editing]:
                    data_extra_fields = {'feature': geojson_feature}

                    # Clear any old error
                    qgis_layer.dataProvider().clearErrors()

                    # add media data
                    self.add_media_property(geojson_feature, metadata_layer)

                    # for GEOSGeometry of Django 2.2 it must add crs to feature if is not set if a geo feature
                    if metadata_layer.geometry_type != QGIS_LAYER_TYPE_NO_GEOM:
                        if geojson_feature[
                                'geometry'] and 'crs' not in geojson_feature[
                                    'geometry']:
                            geojson_feature['geometry'][
                                'crs'] = "{}:{}".format(
                                    self.layer.project.group.srid.auth_name,
                                    self.layer.project.group.srid.auth_srid)

                    # reproject data if necessary
                    if kwargs[
                            'reproject'] and metadata_layer.geometry_type != QGIS_LAYER_TYPE_NO_GEOM:
                        self.reproject_feature(geojson_feature, to_layer=True)

                    # case relation data ADD, if father referenced field is pk
                    if is_referenced_field_is_pk:
                        for newid in kwargs['referenced_layer_insert_ids']:
                            if geojson_feature['properties'][
                                    metadata_layer.
                                    referencing_field] == newid['clientid']:
                                geojson_feature['properties'][
                                    metadata_layer.
                                    referencing_field] = newid['id']

                    if mode_editing == EDITING_POST_DATA_UPDATED:
                        # control feature locked
                        if not metadata_layer.lock.checkFeatureLocked(
                                geojson_feature['id']):
                            raise Exception(
                                self.no_more_lock_feature_msg.format(
                                    geojson_feature['id'],
                                    metadata_layer.client_var))

                    # Send for validation
                    # Note that this may raise a validation error
                    pre_save_maplayer.send(self,
                                           layer_metadata=metadata_layer,
                                           mode=mode_editing,
                                           data=data_extra_fields,
                                           user=self.request.user)

                    # Validate and save
                    try:

                        original_feature = None
                        feature = QgsFeature(qgis_layer.fields())
                        if mode_editing == EDITING_POST_DATA_UPDATED:

                            # add patch for shapefile type, geojson_feature['id'] id int() instead of str()
                            # path to fix into QGIS api
                            geojson_feature[
                                'id'] = get_layer_fids_from_server_fids(
                                    [str(geojson_feature['id'])],
                                    qgis_layer)[0]
                            feature.setId(geojson_feature['id'])

                            # Get feature from data provider before update
                            original_feature = qgis_layer.getFeature(
                                geojson_feature['id'])

                        # We use this feature for geometry parsing only:
                        imported_feature = QgsJsonUtils.stringToFeatureList(
                            json.dumps(geojson_feature),
                            qgis_layer.fields(),
                            None  # UTF8 codec
                        )[0]

                        feature.setGeometry(imported_feature.geometry())

                        # There is something wrong in QGIS 3.10 (fixed in later versions)
                        # so, better loop through the fields and set attributes individually
                        for name, value in geojson_feature['properties'].items(
                        ):
                            feature.setAttribute(name, value)

                        # Loop again for set expressions value:
                        # For update store expression result to use later into update condition
                        field_expresion_values = {}
                        for qgis_field in qgis_layer.fields():
                            if qgis_field.defaultValueDefinition().expression(
                            ):
                                exp = QgsExpression(
                                    qgis_field.defaultValueDefinition(
                                    ).expression())
                                if exp.rootNode().nodeType(
                                ) != QgsExpressionNode.ntLiteral and not exp.hasParserError(
                                ):
                                    context = QgsExpressionContextUtils.createFeatureBasedContext(
                                        feature, qgis_layer.fields())
                                    context.appendScopes(
                                        QgsExpressionContextUtils.
                                        globalProjectLayerScopes(qgis_layer))
                                    result = exp.evaluate(context)
                                    if not exp.hasEvalError():
                                        feature.setAttribute(
                                            qgis_field.name(), result)

                                        # Check update if expression default value has to run also on update e not
                                        # only on insert newone
                                        if qgis_field.defaultValueDefinition(
                                        ).applyOnUpdate():
                                            field_expresion_values[
                                                qgis_field.name()] = result

                            elif qgis_field.typeName() in ('date', 'datetime',
                                                           'time'):

                                if qgis_field.typeName() == 'date':
                                    qtype = QDate
                                elif qgis_field.typeName() == 'datetime':
                                    qtype = QDateTime
                                else:
                                    qtype = QTime

                                field_idx = qgis_layer.fields().indexFromName(
                                    qgis_field.name())
                                options = qgis_layer.editorWidgetSetup(
                                    field_idx).config()

                                if 'field_iso_format' in options and not options[
                                        'field_iso_format']:
                                    if geojson_feature['properties'][
                                            qgis_field.name()]:
                                        value = qtype.fromString(
                                            geojson_feature['properties'][
                                                qgis_field.name()],
                                            options['field_format'])
                                        feature.setAttribute(
                                            qgis_field.name(), value)

                        # Call validator!
                        errors = feature_validator(feature,
                                                   metadata_layer.qgis_layer)
                        if errors:
                            raise ValidationError(errors)

                        # Save the feature
                        if mode_editing == EDITING_POST_DATA_ADDED:
                            if has_transactions:
                                if not qgis_layer.addFeature(feature):
                                    raise Exception(
                                        _('Error adding feature: %s') %
                                        ', '.join(qgis_layer.dataProvider().
                                                  errors()))
                            else:
                                if not qgis_layer.dataProvider().addFeature(
                                        feature):
                                    raise Exception(
                                        _('Error adding feature: %s') %
                                        ', '.join(qgis_layer.dataProvider().
                                                  errors()))

                                # Patch for Spatialite provider on pk
                                if qgis_layer.dataProvider().name(
                                ) == 'spatialite':
                                    pks = qgis_layer.primaryKeyAttributes()
                                    if len(pks) > 1:
                                        raise Exception(
                                            _(f'Error adding feature on Spatialite provider: '
                                              f'layer {qgis_layer.id()} has more than one pk column'
                                              ))

                                    # update pk attribute:
                                    feature.setAttribute(
                                        pks[0],
                                        server_fid(feature,
                                                   qgis_layer.dataProvider()))

                        elif mode_editing == EDITING_POST_DATA_UPDATED:
                            attr_map = {}
                            for name, value in geojson_feature[
                                    'properties'].items():
                                if name in qgis_layer.dataProvider(
                                ).fieldNameMap():
                                    if name in field_expresion_values:
                                        value = field_expresion_values[name]
                                    attr_map[qgis_layer.dataProvider().
                                             fieldNameMap()[name]] = value

                            if has_transactions:
                                if not qgis_layer.changeAttributeValues(
                                        geojson_feature['id'], attr_map):
                                    raise Exception(
                                        _('Error changing attribute values: %s'
                                          ) %
                                        ', '.join(qgis_layer.dataProvider().
                                                  errors()))
                                # Check for errors because of https://github.com/qgis/QGIS/issues/36583
                                if qgis_layer.dataProvider().errors():
                                    raise Exception(', '.join(
                                        qgis_layer.dataProvider().errors()))
                                if not feature.geometry().isNull(
                                ) and not qgis_layer.changeGeometry(
                                        geojson_feature['id'],
                                        feature.geometry()):
                                    raise Exception(
                                        _('Error changing geometry: %s') %
                                        ', '.join(qgis_layer.dataProvider().
                                                  errors()))
                            else:
                                if not qgis_layer.dataProvider(
                                ).changeAttributeValues(
                                    {geojson_feature['id']: attr_map}):
                                    raise Exception(
                                        _('Error changing attribute values: %s'
                                          ) %
                                        ', '.join(qgis_layer.dataProvider().
                                                  errors()))
                                if not feature.geometry().isNull(
                                ) and not qgis_layer.dataProvider(
                                ).changeGeometryValues({
                                        geojson_feature['id']:
                                        feature.geometry()
                                }):
                                    raise Exception(
                                        _('Error changing geometry: %s') %
                                        ', '.join(qgis_layer.dataProvider().
                                                  errors()))

                        to_res = {}
                        to_res_lock = {}

                        if mode_editing == EDITING_POST_DATA_ADDED:

                            # to exclude QgsFormater used into QgsJsonjExporter is necessary build by hand single json feature
                            ex = QgsJsonExporter(qgis_layer)
                            ex.setIncludeAttributes(False)

                            fnames = [f.name() for f in feature.fields()]
                            jfeature = json.loads(
                                ex.exportFeature(
                                    feature,
                                    dict(zip(fnames, feature.attributes()))))

                            to_res.update({
                                'clientid':
                                geojson_feature['id'],
                                # This might be the internal QGIS feature id (< 0)
                                'id':
                                server_fid(
                                    feature,
                                    metadata_layer.qgis_layer.dataProvider()),
                                'properties':
                                jfeature['properties']
                            })

                            # lock news:
                            to_res_lock = metadata_layer.lock.modelLock2dict(
                                metadata_layer.lock.lockFeature(server_fid(
                                    feature,
                                    metadata_layer.qgis_layer.dataProvider()),
                                                                save=True))

                        if bool(to_res):
                            insert_ids.append(to_res)
                        if bool(to_res_lock):
                            lock_ids.append(to_res_lock)

                        # Send post vase signal
                        post_save_maplayer.send(
                            self,
                            layer_metadata=metadata_layer,
                            mode=mode_editing,
                            data=data_extra_fields,
                            user=self.request.user,
                            original_feature=original_feature,
                            to_res=to_res)

                    except ValidationError as ex:
                        raise ValidationError({
                            metadata_layer.client_var: {
                                mode_editing: {
                                    'id': geojson_feature['id'],
                                    'fields': ex.detail,
                                }
                            }
                        })

                    except Exception as ex:
                        raise ValidationError({
                            metadata_layer.client_var: {
                                mode_editing: {
                                    'id': geojson_feature['id'],
                                    'fields': str(ex),
                                }
                            }
                        })

        # erasing feature if to do
        if EDITING_POST_DATA_DELETED in post_layer_data:

            fids = post_layer_data[EDITING_POST_DATA_DELETED]

            # get feature fids from server fids from client.
            fids = get_layer_fids_from_server_fids([str(id) for id in fids],
                                                   qgis_layer)

            for feature_id in fids:

                # control feature locked
                if not metadata_layer.lock.checkFeatureLocked(str(feature_id)):
                    raise Exception(
                        self.no_more_lock_feature_msg.format(
                            feature_id, metadata_layer.client_var))

                # Get feature to delete
                ex = QgsJsonExporter(qgis_layer)
                deleted_feature = ex.exportFeature(
                    qgis_layer.getFeature(feature_id))

                pre_delete_maplayer.send(self,
                                         layer_metatada=metadata_layer,
                                         data=deleted_feature,
                                         user=self.request.user)

                qgis_layer.dataProvider().clearErrors()

                if has_transactions:
                    if not qgis_layer.deleteFeatures(
                        [feature_id]) or qgis_layer.dataProvider().errors():
                        raise Exception(
                            _('Cannot delete feature: %s') %
                            ', '.join(qgis_layer.dataProvider().errors()))
                else:
                    if not qgis_layer.dataProvider().deleteFeatures(
                        [feature_id]) or qgis_layer.dataProvider().errors():
                        raise Exception(
                            _('Cannot delete feature: %s') %
                            ', '.join(qgis_layer.dataProvider().errors()))

        return insert_ids, lock_ids
 def test_MatchesReturnsTrueForSimpleMatch(self):
     style = QgsConditionalStyle("@value > 10")
     context = QgsExpressionContextUtils.createFeatureBasedContext(
         QgsFeature(), QgsFields())
     self.assertTrue(style.matches(20, context))