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)
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 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)
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
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))