def testExportFeatureRelations(self): """ Test exporting a feature with relations """ #parent layer parent = QgsVectorLayer( "Point?field=fldtxt:string&field=fldint:integer&field=foreignkey:integer", "parent", "memory") pr = parent.dataProvider() pf1 = QgsFeature() pf1.setFields(parent.fields()) pf1.setAttributes(["test1", 67, 123]) pf2 = QgsFeature() pf2.setFields(parent.fields()) pf2.setAttributes(["test2", 68, 124]) assert pr.addFeatures([pf1, pf2]) #child layer child = QgsVectorLayer( "Point?field=x:string&field=y:integer&field=z:integer", "referencedlayer", "memory") pr = child.dataProvider() f1 = QgsFeature() f1.setFields(child.fields()) f1.setAttributes(["foo", 123, 321]) f2 = QgsFeature() f2.setFields(child.fields()) f2.setAttributes(["bar", 123, 654]) f3 = QgsFeature() f3.setFields(child.fields()) f3.setAttributes(["foobar", 124, 554]) assert pr.addFeatures([f1, f2, f3]) QgsProject.instance().addMapLayers([child, parent]) rel = QgsRelation() rel.setId('rel1') rel.setName('relation one') rel.setReferencingLayer(child.id()) rel.setReferencedLayer(parent.id()) rel.addFieldPair('y', 'foreignkey') QgsProject.instance().relationManager().addRelation(rel) exporter = QgsJsonExporter() exporter.setVectorLayer(parent) self.assertEqual(exporter.vectorLayer(), parent) exporter.setIncludeRelated(True) self.assertEqual(exporter.includeRelated(), True) expected = """{ "type":"Feature", "id":0, "geometry":null, "properties":{ "fldtxt":"test1", "fldint":67, "foreignkey":123, "relation one":[{"x":"foo", "y":123, "z":321}, {"x":"bar", "y":123, "z":654}] } }""" self.assertEqual(exporter.exportFeature(pf1), expected) expected = """{ "type":"Feature", "id":0, "geometry":null, "properties":{ "fldtxt":"test2", "fldint":68, "foreignkey":124, "relation one":[{"x":"foobar", "y":124, "z":554}] } }""" self.assertEqual(exporter.exportFeature(pf2), expected) # with field formatter setup = QgsEditorWidgetSetup('ValueMap', {"map": { "apples": 123, "bananas": 124 }}) child.setEditorWidgetSetup(1, setup) expected = """{ "type":"Feature", "id":0, "geometry":null, "properties":{ "fldtxt":"test1", "fldint":67, "foreignkey":123, "relation one":[{"x":"foo", "y":"apples", "z":321}, {"x":"bar", "y":"apples", "z":654}] } }""" self.assertEqual(exporter.exportFeature(pf1), expected) # test excluding related attributes exporter.setIncludeRelated(False) self.assertEqual(exporter.includeRelated(), False) expected = """{ "type":"Feature", "id":0, "geometry":null, "properties":{ "fldtxt":"test2", "fldint":68, "foreignkey":124 } }""" self.assertEqual(exporter.exportFeature(pf2), expected) # test without vector layer set exporter.setIncludeRelated(True) exporter.setVectorLayer(None) expected = """{ "type":"Feature", "id":0, "geometry":null, "properties":{ "fldtxt":"test2", "fldint":68, "foreignkey":124 } }""" self.assertEqual(exporter.exportFeature(pf2), expected)
def testExportFeatures(self): """ Test exporting feature collections """ fields = QgsFields() fields.append(QgsField("name", QVariant.String)) fields.append(QgsField("cost", QVariant.Double)) fields.append(QgsField("population", QVariant.Int)) feature = QgsFeature(fields, 5) feature.setGeometry(QgsGeometry(QgsPoint(5, 6))) feature.setAttributes(['Valsier Peninsula', 6.8, 198]) exporter = QgsJsonExporter() # single feature expected = """{ "type": "FeatureCollection", "features":[ { "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":{ "name":"Valsier Peninsula", "cost":6.8, "population":198 } } ]}""" self.assertEqual(exporter.exportFeatures([feature]), expected) # multiple features feature2 = QgsFeature(fields, 6) feature2.setGeometry(QgsGeometry(QgsPoint(7, 8))) feature2.setAttributes(['Henry Gale Island', 9.7, 38]) expected = """{ "type": "FeatureCollection", "features":[ { "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":{ "name":"Valsier Peninsula", "cost":6.8, "population":198 } }, { "type":"Feature", "id":6, "geometry": {"type": "Point", "coordinates": [7, 8]}, "properties":{ "name":"Henry Gale Island", "cost":9.7, "population":38 } } ]}""" self.assertEqual(exporter.exportFeatures([feature, feature2]), expected)
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 testJSONExporter(self): """ test converting features to GeoJSON """ fields = QgsFields() fields.append(QgsField("name", QVariant.String)) fields.append(QgsField("cost", QVariant.Double)) fields.append(QgsField("population", QVariant.Int)) feature = QgsFeature(fields, 5) feature.setGeometry(QgsGeometry(QgsPoint(5, 6))) feature.setAttributes(['Valsier Peninsula', 6.8, 198]) exporter = QgsJsonExporter() expected = """{ "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":{ "name":"Valsier Peninsula", "cost":6.8, "population":198 } }""" self.assertEqual(exporter.exportFeature(feature), expected) # test with linestring for bbox inclusion l = QgsLineString() l.setPoints([QgsPoint(5, 6), QgsPoint(15, 16)]) feature.setGeometry(QgsGeometry(QgsLineString(l))) expected = """{ "type":"Feature", "id":5, "bbox":[5, 6, 15, 16], "geometry": {"type": "LineString", "coordinates": [ [5, 6], [15, 16]]}, "properties":{ "name":"Valsier Peninsula", "cost":6.8, "population":198 } }""" self.assertEqual(exporter.exportFeature(feature), expected) # test that precision is respected feature.setGeometry(QgsGeometry(QgsPoint(5.444444444, 6.333333333))) exporter.setPrecision(3) self.assertEqual(exporter.precision(), 3) expected = """{ "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5.444, 6.333]}, "properties":{ "name":"Valsier Peninsula", "cost":6.8, "population":198 } }""" self.assertEqual(exporter.exportFeature(feature), expected) feature.setGeometry(QgsGeometry(QgsPoint(5, 6))) exporter.setPrecision(17) # test that attribute subset is respected exporter.setAttributes([0, 2]) self.assertEqual(exporter.attributes(), [0, 2]) expected = """{ "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":{ "name":"Valsier Peninsula", "population":198 } }""" self.assertEqual(exporter.exportFeature(feature), expected) exporter.setAttributes([1]) self.assertEqual(exporter.attributes(), [1]) expected = """{ "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":{ "cost":6.8 } }""" self.assertEqual(exporter.exportFeature(feature), expected) exporter.setAttributes([]) # text excluding attributes exporter.setExcludedAttributes([1]) self.assertEqual(exporter.excludedAttributes(), [1]) expected = """{ "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":{ "name":"Valsier Peninsula", "population":198 } }""" self.assertEqual(exporter.exportFeature(feature), expected) exporter.setExcludedAttributes([1, 2]) self.assertEqual(exporter.excludedAttributes(), [1, 2]) expected = """{ "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":{ "name":"Valsier Peninsula" } }""" self.assertEqual(exporter.exportFeature(feature), expected) exporter.setExcludedAttributes([0, 1, 2]) self.assertEqual(exporter.excludedAttributes(), [0, 1, 2]) expected = """{ "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":null }""" self.assertEqual(exporter.exportFeature(feature), expected) # test that excluded attributes take precedence over included exporter.setAttributes([1, 2]) exporter.setExcludedAttributes([0, 1]) expected = """{ "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":{ "population":198 } }""" self.assertEqual(exporter.exportFeature(feature), expected) exporter.setAttributes([]) exporter.setExcludedAttributes([]) # test excluding geometry exporter.setIncludeGeometry(False) self.assertEqual(exporter.includeGeometry(), False) feature.setGeometry(QgsGeometry(QgsLineString(l))) expected = """{ "type":"Feature", "id":5, "geometry":null, "properties":{ "name":"Valsier Peninsula", "cost":6.8, "population":198 } }""" self.assertEqual(exporter.exportFeature(feature), expected) exporter.setIncludeGeometry(True) feature.setGeometry(QgsGeometry(QgsPoint(5, 6))) # test excluding attributes exporter.setIncludeAttributes(False) self.assertEqual(exporter.includeAttributes(), False) expected = """{ "type":"Feature", "id":5, "geometry": {"type": "Point", "coordinates": [5, 6]}, "properties":null }""" self.assertEqual(exporter.exportFeature(feature), expected) exporter.setIncludeGeometry(False) expected = """{ "type":"Feature", "id":5, "geometry":null, "properties":null }""" self.assertEqual(exporter.exportFeature(feature), expected) exporter.setIncludeAttributes(True) # test overriding ID expected = """{ "type":"Feature", "id":29, "geometry":null, "properties":{ "name":"Valsier Peninsula", "cost":6.8, "population":198 } }""" self.assertEqual(exporter.exportFeature(feature, id=29), expected) # test injecting extra attributes expected = """{ "type":"Feature", "id":5, "geometry":null, "properties":{ "name":"Valsier Peninsula", "cost":6.8, "population":198, "extra":"val1", "extra2":2 } }""" self.assertEqual( exporter.exportFeature(feature, extraProperties={ "extra": "val1", "extra2": 2 }), expected) exporter.setIncludeAttributes(False) expected = """{ "type":"Feature", "id":5, "geometry":null, "properties":{ "extra":"val1", "extra2":{"nested_map":5, "nested_map2":"val"}, "extra3":[1,2,3] } }""" expected2 = """{ "type":"Feature", "id":5, "geometry":null, "properties":{ "extra":"val1", "extra2":{"nested_map":5,"nested_map2":"val"}, "extra3":[1,2,3] } }""" exp_f = exporter.exportFeature(feature, extraProperties={ "extra": "val1", "extra2": { "nested_map": 5, "nested_map2": "val" }, "extra3": [1, 2, 3] }) self.assertTrue(exp_f == expected or exp_f == expected2) exporter.setIncludeGeometry(True)
def test_expression_eval(self): self.assertEqual(expression_eval('1'), 1) self.assertEqual(expression_eval('1=2'), False) layer = Layer.objects.get(title='world') # Errors with self.assertRaises(ExpressionProjectError) as ex: expression_eval('1', project_id='9999', qgs_layer_id='9999') with self.assertRaises(ExpressionLayerError) as ex: expression_eval('1', project_id=layer.project_id, qgs_layer_id='9999') with self.assertRaises(ExpressionParseError) as ex: expression_eval('dsa hdshk == t') with self.assertRaises(ExpressionEvalError) as ex: expression_eval('not_valid=2') with self.assertRaises(ExpressionForbiddenError) as ex: expression_eval('@qgis_version') with self.assertRaises(ExpressionForbiddenError) as ex: expression_eval('env(\'USER\')') # Test form data feature = next(layer.qgis_layer.getFeatures()) exp = QgsJsonExporter(layer.qgis_layer) form_data = exp.exportFeature(feature) self.assertEqual( expression_eval('current_value(\'APPROX\')', project_id=layer.project_id, qgs_layer_id=layer.qgs_layer_id, form_data=json.loads(form_data)), 9705000) self.assertEqual( expression_eval('"APPROX"', project_id=layer.project_id, qgs_layer_id=layer.qgs_layer_id, form_data=json.loads(form_data)), 9705000) self.assertEqual( expression_eval('APPROX', project_id=layer.project_id, qgs_layer_id=layer.qgs_layer_id, form_data=json.loads(form_data)), 9705000) self.assertEqual( expression_eval('APPROX = 9705000', project_id=layer.project_id, qgs_layer_id=layer.qgs_layer_id, form_data=json.loads(form_data)), True) self.assertEqual( expression_eval('APPROX = 99999', project_id=layer.project_id, qgs_layer_id=layer.qgs_layer_id, form_data=json.loads(form_data)), False)
def response_data_mode(self, request, export_features=False): """ Query layer and return data :param request: DjangoREST API request object :param formatter: Boolean, default False, True for to use QgsJsonExport.exportFeatures method :return: response dict data """ # Create the QGIS feature request, it will be passed through filters # and to the final QGIS API get features call. qgis_feature_request = QgsFeatureRequest() # Prepare arguments for the get feature call kwargs = {} # Apply filter backends, store original subset string original_subset_string = self.metadata_layer.qgis_layer.subsetString() if hasattr(self, 'filter_backends'): for backend in self.filter_backends: backend().apply_filter(request, self.metadata_layer.qgis_layer, qgis_feature_request, self) # Paging cannot be a backend filter if 'page' in request.query_params: kwargs['page'] = request.query_params.get('page') kwargs['page_size'] = request.query_params.get('page_size', 10) self.features = get_qgis_features( self.metadata_layer.qgis_layer, qgis_feature_request, **kwargs) ex = QgsJsonExporter(self.metadata_layer.qgis_layer) # If 'unique' request params is set, # api return a list of unique # field name sent with 'unique' param. # -------------------------------------- # IDEA: for big data it'll be iterate over features to get unique # c++ iteration is fast. Instead memory layer fith to much features can be a problem. if 'unique' in request.query_params: vl = QgsVectorLayer(QgsWkbTypes.displayString(self.metadata_layer.qgis_layer.wkbType()), "temporary_vector", "memory") pr = vl.dataProvider() # add fields pr.addAttributes(self.metadata_layer.qgis_layer.fields()) vl.updateFields() # tell the vector layer to fetch changes from the provider res = pr.addFeatures(self.features) uniques = vl.uniqueValues( self.metadata_layer.qgis_layer.fields().indexOf(request.query_params.get('unique')) ) values = [] for u in uniques: try: if u: values.append(json.loads(QgsJsonUtils.encodeValue(u))) except Exception as e: logger.error(f'Response vector widget unique: {e}') continue self.results.update({ 'data': values, 'count': len(values) }) del(vl) else: # patch for return GeoJson feature with CRS different from WGS84 # TODO: use .setTransformGeometries( false ) with QGIS >= 3.12 ex.setSourceCrs(QgsCoordinateReferenceSystem('EPSG:4326')) # check for formatter query url param and check if != 0 if 'formatter' in request.query_params: formatter = request.query_params.get('formatter') if formatter.isnumeric() and int(formatter) == 0: export_features = False else: export_features = True if export_features: feature_collection = json.loads(ex.exportFeatures(self.features)) else: # to exclude QgsFormater used into QgsJsonjExporter is necessary build by hand single json feature ex.setIncludeAttributes(False) feature_collection = { 'type': 'FeatureCollection', 'features': [] } for feature in self.features: fnames = [f.name() for f in feature.fields()] feature_collection['features'].append( json.loads(ex.exportFeature(feature, dict(zip(fnames, feature.attributes())))) ) # FIXME: QGIS api reprojecting? # Reproject if necessary # if self.reproject: # self.reproject_featurecollection(feature_collection) # Change media self.change_media(feature_collection) self.results.update(APIVectorLayerStructure(**{ 'data': feature_collection, 'count': count_qgis_features(self.metadata_layer.qgis_layer, qgis_feature_request, **kwargs), 'geometryType': self.metadata_layer.geometry_type, }).as_dict()) # FIXME: add extra fields data by signals and receivers # FIXME: featurecollection = post_serialize_maplayer.send(layer_serializer, layer=self.layer_name) # FIXME: Not sure how to map this to the new QGIS API # Restore the original subset string self.metadata_layer.qgis_layer.setSubsetString(original_subset_string)
def response_data_mode(self, request, export_features=False): """ Query layer and return data :param request: DjangoREST API request object :param formatter: Boolean, default False, True for to use QgsJsonExport.exportFeatures method :return: response dict data """ # Create the QGIS feature request, it will be passed through filters # and to the final QGIS API get features call. qgis_feature_request = QgsFeatureRequest() # Prepare arguments for the get feature call kwargs = {} # Apply filter backends, store original subset string original_subset_string = self.metadata_layer.qgis_layer.subsetString() if hasattr(self, 'filter_backends'): try: for backend in self.filter_backends: backend().apply_filter(request, self.metadata_layer, qgis_feature_request, self) except Exception as e: raise APIException(e) # Paging cannot be a backend filter if 'page' in request.query_params: kwargs['page'] = request.query_params.get('page') kwargs['page_size'] = request.query_params.get('page_size', 10) # Make sure we have all attrs we need to build the server FID provider = self.metadata_layer.qgis_layer.dataProvider() if qgis_feature_request.flags() & QgsFeatureRequest.SubsetOfAttributes: attrs = qgis_feature_request.subsetOfAttributes() for attr_idx in provider.pkAttributeIndexes(): if attr_idx not in attrs: attrs.append(attr_idx) qgis_feature_request.setSubsetOfAttributes(attrs) self.features = get_qgis_features(self.metadata_layer.qgis_layer, qgis_feature_request, **kwargs) # Reproject feature if layer CRS != Project CRS if self.reproject: for f in self.features: self.reproject_feature(f) ex = QgsJsonExporter(self.metadata_layer.qgis_layer) # If 'unique' request params is set, # api return a list of unique # field name sent with 'unique' param. # -------------------------------------- # IDEA: for big data it'll be iterate over features to get unique # c++ iteration is fast. Instead memory layer with too many features can be a problem. if 'unique' in request.query_params: vl = QgsVectorLayer( QgsWkbTypes.displayString( self.metadata_layer.qgis_layer.wkbType()), "temporary_vector", "memory") pr = vl.dataProvider() # add fields pr.addAttributes(self.metadata_layer.qgis_layer.fields()) vl.updateFields( ) # tell the vector layer to fetch changes from the provider res = pr.addFeatures(self.features) uniques = vl.uniqueValues( self.metadata_layer.qgis_layer.fields().indexOf( request.query_params.get('unique'))) values = [] for u in uniques: try: if u: values.append(json.loads(QgsJsonUtils.encodeValue(u))) except Exception as e: logger.error(f'Response vector widget unique: {e}') continue # sort values values.sort() self.results.update({'data': values, 'count': len(values)}) del (vl) else: ex.setTransformGeometries(False) # check for formatter query url param and check if != 0 if 'formatter' in request.query_params: formatter = request.query_params.get('formatter') if formatter.isnumeric() and int(formatter) == 0: export_features = False else: export_features = True if export_features: feature_collection = json.loads( ex.exportFeatures(self.features)) else: # to exclude QgsFormater used into QgsJsonExporter is necessary build by hand single json feature ex.setIncludeAttributes(False) feature_collection = { 'type': 'FeatureCollection', 'features': [] } for feature in self.features: fnames = [] date_fields = [] for f in feature.fields(): fnames.append(f.name()) if f.typeName() in ('date', 'datetime', 'time'): date_fields.append(f) jsonfeature = json.loads( ex.exportFeature( feature, dict(zip(fnames, feature.attributes())))) # Update date and datetime fields value if widget is active if len(date_fields) > 0: for f in date_fields: field_idx = self.metadata_layer.qgis_layer.fields( ).indexFromName(f.name()) options = self.metadata_layer.qgis_layer.editorWidgetSetup( field_idx).config() if 'field_iso_format' in options and not options[ 'field_iso_format']: try: jsonfeature['properties'][f.name()] = feature.attribute(f.name())\ .toString(options['field_format']) except: pass feature_collection['features'].append(jsonfeature) # Change media self.change_media(feature_collection) # Patch feature IDs with server featureIDs fids_map = {} for f in self.features: fids_map[f.id()] = server_fid(f, provider) for i in range(len(feature_collection['features'])): f = feature_collection['features'][i] f['id'] = fids_map[f['id']] self.results.update( APIVectorLayerStructure( **{ 'data': feature_collection, 'count': count_qgis_features(self.metadata_layer.qgis_layer, qgis_feature_request, **kwargs), 'geometryType': self.metadata_layer.geometry_type, }).as_dict()) # FIXME: add extra fields data by signals and receivers # FIXME: featurecollection = post_serialize_maplayer.send(layer_serializer, layer=self.layer_name) # FIXME: Not sure how to map this to the new QGIS API # Restore the original subset string self.metadata_layer.qgis_layer.setSubsetString(original_subset_string)
def virtualFields(self, params: Dict[str, str], response: QgsServerResponse, project: QgsProject) -> None: """ Get virtual fields for features In parameters: LAYER=wms-layer-name VIRTUALS={"key1": "first expression", "key2": "second expression"} // optionals FILTER=An expression to filter layer FIELDS=list of requested field separated by comma WITH_GEOMETRY=False """ layername = params.get('LAYER', '') if not layername: raise ExpressionServiceError( "Bad request error", "Invalid 'VirtualFields' REQUEST: LAYER parameter is mandatory", 400) # get layer layer = findVectorLayer(layername, project) # layer not found if not layer: raise ExpressionServiceError( "Bad request error", "Invalid LAYER parameter for 'VirtualFields': {} provided". format(layername), 400) # get virtuals virtuals = params.get('VIRTUALS', '') if not virtuals: raise ExpressionServiceError( "Bad request error", "Invalid 'VirtualFields' REQUEST: VIRTUALS parameter is mandatory", 400) # try to load virtuals dict vir_json = None try: vir_json = json.loads(virtuals) except Exception: QgsMessageLog.logMessage( "JSON loads virtuals '{}' exception:\n{}".format( virtuals, traceback.format_exc()), "lizmap", Qgis.Critical) raise ExpressionServiceError( "Bad request error", "Invalid 'VirtualFields' REQUEST: VIRTUALS '{}' are not well formed" .format(virtuals), 400) if not isinstance(vir_json, dict): raise ExpressionServiceError( "Bad request error", "Invalid 'VirtualFields' REQUEST: VIRTUALS '{}' are not well formed" .format(virtuals), 400) # create expression context exp_context = QgsExpressionContext() exp_context.appendScope(QgsExpressionContextUtils.globalScope()) exp_context.appendScope( QgsExpressionContextUtils.projectScope(project)) exp_context.appendScope(QgsExpressionContextUtils.layerScope(layer)) # create distance area context da = QgsDistanceArea() da.setSourceCrs(layer.crs(), project.transformContext()) da.setEllipsoid(project.ellipsoid()) # parse virtuals exp_map = {} exp_parser_errors = [] for k, e in vir_json.items(): exp = QgsExpression(e) exp.setGeomCalculator(da) exp.setDistanceUnits(project.distanceUnits()) exp.setAreaUnits(project.areaUnits()) if exp.hasParserError(): exp_parser_errors.append('Error "{}": {}'.format( e, exp.parserErrorString())) continue if not exp.isValid(): exp_parser_errors.append('Expression not valid "{}"'.format(e)) continue exp.prepare(exp_context) exp_map[k] = exp # expression parser errors found if exp_parser_errors: raise ExpressionServiceError( "Bad request error", "Invalid VIRTUALS for 'VirtualFields':\n{}".format( '\n'.join(exp_parser_errors)), 400) req = QgsFeatureRequest() # get filter req_filter = params.get('FILTER', '') if req_filter: req_exp = QgsExpression(req_filter) req_exp.setGeomCalculator(da) req_exp.setDistanceUnits(project.distanceUnits()) req_exp.setAreaUnits(project.areaUnits()) if req_exp.hasParserError(): raise ExpressionServiceError( "Bad request error", "Invalid FILTER for 'VirtualFields' Error \"{}\": {}". format(req_filter, req_exp.parserErrorString()), 400) if not req_exp.isValid(): raise ExpressionServiceError( "Bad request error", "Invalid FILTER for 'VirtualFields' Expression not valid \"{}\"" .format(req_filter), 400) req_exp.prepare(exp_context) req = QgsFeatureRequest(req_exp, exp_context) # With geometry withGeom = params.get('WITH_GEOMETRY', '').lower() in ['true', '1', 't'] if not withGeom: req.setFlags(QgsFeatureRequest.NoGeometry) # Fields pkAttributes = layer.primaryKeyAttributes() attributeList = [i for i in pkAttributes] fields = layer.fields() r_fields = [ f.strip() for f in params.get('FIELDS', '').split(',') if f ] for f in r_fields: attributeList.append(fields.indexOf(f)) # response response.setStatusCode(200) response.setHeader("Content-Type", "application/json") response.write('{ "type": "FeatureCollection","features":[') response.flush() jsonExporter = QgsJsonExporter(layer) if attributeList: jsonExporter.setAttributes(attributeList) separator = '' for feat in layer.getFeatures(req): fid = layername + '.' + getServerFid(feat, pkAttributes) extra = {} # Update context exp_context.setFeature(feat) exp_context.setFields(feat.fields()) # Evaluate expressions for virtual fields errors = {} for k, exp in exp_map.items(): value = exp.evaluate(exp_context) if exp.hasEvalError(): extra[k] = None errors[k] = exp.evalErrorString() else: extra[k] = json.loads(QgsJsonUtils.encodeValue(value)) errors[k] = exp.expression() response.write(separator + jsonExporter.exportFeature(feat, extra, fid)) response.flush() separator = ',\n' response.write(']}') return
def getFeatureWithFormScope(self, params: Dict[str, str], response: QgsServerResponse, project: QgsProject) -> None: """ Get filtered features with a form scope In parameters: LAYER=wms-layer-name FILTER=An expression to filter layer FORM_FEATURE={"type": "Feature", "geometry": {}, "properties": {}} // optionals FIELDS=list of requested field separated by comma WITH_GEOMETRY=False """ layername = params.get('LAYER', '') if not layername: raise ExpressionServiceError( "Bad request error", "Invalid 'GetFeatureWithFormScope' REQUEST: LAYER parameter is mandatory", 400) # get layer layer = findVectorLayer(layername, project) # layer not found if not layer: raise ExpressionServiceError( "Bad request error", "Invalid LAYER parameter for 'VirtualField': {} provided". format(layername), 400) # get filter exp_filter = params.get('FILTER', '') if not exp_filter: raise ExpressionServiceError( "Bad request error", "Invalid 'GetFeatureWithFormScope' REQUEST: FILTER parameter is mandatory", 400) # get form feature form_feature = params.get('FORM_FEATURE', '') if not form_feature: raise ExpressionServiceError( "Bad request error", "Invalid 'GetFeatureWithFormScope' REQUEST: FORM_FEATURE parameter is mandatory", 400) # Check features geojson = {} try: geojson = json.loads(form_feature) except Exception: QgsMessageLog.logMessage( "JSON loads form feature '{}' exception:\n{}".format( form_feature, traceback.format_exc()), "lizmap", Qgis.Critical) raise ExpressionServiceError( "Bad request error", "Invalid 'GetFeatureWithFormScope' REQUEST: FORM_FEATURE '{}' are not well formed" .format(form_feature), 400) if not geojson or not isinstance(geojson, dict): raise ExpressionServiceError( "Bad request error", "Invalid 'GetFeatureWithFormScope' REQUEST: FORM_FEATURE '{}' are not well formed" .format(form_feature), 400) if ('type' not in geojson) or geojson['type'] != 'Feature': raise ExpressionServiceError( "Bad request error", "Invalid 'GetFeatureWithFormScope' REQUEST: FORM_FEATURE '{}' are not well formed: type not defined or not Feature." .format(form_feature), 400) # try to load form feature # read fields form_feature_fields = QgsJsonUtils.stringToFields( form_feature, QTextCodec.codecForName("UTF-8")) # read features form_feature_list = QgsJsonUtils.stringToFeatureList( form_feature, form_feature_fields, QTextCodec.codecForName("UTF-8")) # features not well formed if not form_feature_list: raise ExpressionServiceError( "Bad request error", "Invalid FORM_FEATURE for 'GetFeatureWithFormScope': not GeoJSON feature provided\n{}" .format(form_feature), 400) if len(form_feature_list) != 1: raise ExpressionServiceError( "Bad request error", "Invalid FORM_FEATURE for 'GetFeatureWithFormScope': not GeoJSON feature provided\n{}" .format(form_feature), 400) # Get the form feature form_feat = form_feature_list[0] # create expression context exp_context = QgsExpressionContext() exp_context.appendScope(QgsExpressionContextUtils.globalScope()) exp_context.appendScope( QgsExpressionContextUtils.projectScope(project)) exp_context.appendScope(QgsExpressionContextUtils.layerScope(layer)) exp_context.appendScope(QgsExpressionContextUtils.formScope(form_feat)) # create distance area context da = QgsDistanceArea() da.setSourceCrs(layer.crs(), project.transformContext()) da.setEllipsoid(project.ellipsoid()) # Get filter expression exp_f = QgsExpression(exp_filter) exp_f.setGeomCalculator(da) exp_f.setDistanceUnits(project.distanceUnits()) exp_f.setAreaUnits(project.areaUnits()) if exp_f.hasParserError(): raise ExpressionServiceError( "Bad request error", "Invalid FILTER for 'GetFeatureWithFormScope': Error \"{}\": {}" .format(exp_filter, exp_f.parserErrorString()), 400) if not exp_f.isValid(): raise ExpressionServiceError( "Bad request error", "Invalid FILTER for 'GetFeatureWithFormScope': Expression not valid \"{}\"" .format(exp_filter), 400) exp_f.prepare(exp_context) req = QgsFeatureRequest(exp_f, exp_context) # With geometry withGeom = params.get('WITH_GEOMETRY', '').lower() in ['true', '1', 't'] if not withGeom: req.setFlags(QgsFeatureRequest.NoGeometry) # Fields pkAttributes = layer.primaryKeyAttributes() attributeList = [i for i in pkAttributes] fields = layer.fields() r_fields = [ f.strip() for f in params.get('FIELDS', '').split(',') if f ] for f in r_fields: attributeList.append(fields.indexOf(f)) # response response.setStatusCode(200) response.setHeader("Content-Type", "application/json") response.write('{ "type": "FeatureCollection","features":[') response.flush() jsonExporter = QgsJsonExporter(layer) if attributeList: jsonExporter.setAttributes(attributeList) separator = '' for feat in layer.getFeatures(req): fid = layername + '.' + getServerFid(feat, pkAttributes) response.write(separator + jsonExporter.exportFeature(feat, {}, fid)) response.flush() separator = ',\n' response.write(']}') return