3

Shortcut to the core question:

How to write a custom function to get the name of a field in an Attribute Form?

I would like to use it as a variable in a Filter expression of a field with a Value Relation Widget.


I have to prepare a project for field observation of species.

The species will be recorded in individual layers for different species groups which have different needs for input form values.

The fields in the Attribute form for each layer will be set to get its values from a Value Relation.

All possible values for all layers and all fields are collected in a separate table codes.


Fields of table codes:

layer | field | value | key

example content of the fields:

layer: mammals, birds, amphibians, reptiles, butterflies ....

field: species_name, status, gender, observation_method, behavoir ...

value: Elephant,male,adult, female, unknown, Camera-trap, mating ...

key: 001,999,234 ...

This approach is so much more convenient compared to a Value Map or separate tables for each layer cause you can prepare and quickly edit it outside of QGIS without fumbling around with Attribute Form setting permanently to adjust things.


Now, as an example, the layer mammals:

The names of the fields are identical to the content in field of the table codes :

species_name | status | gender | observation_method | ...

In the Attribute Form , the field species_name is now set in Value Relation to connect to the table codes and then Key column to key and Value column to value.

In the Filter expression, i can now use this expression to filter the desired values for the field:

 CASE
 WHEN
 "layer"  =  'mammals'
 THEN
 "field" =  'species_name' 
 END

This perfectly filters all the rows in the table codes first by the content of the field layer and then by the field field.

Great!

Not so great: I have 14 layers and 9 different fields. Not all fields occour in each layer, and the same fields have different values for the same category. This is tedious work to set al the stuff and recheck it.


So, now I would like to create an expression or even a custom function (I think python is here the more powerful tool, but I am a complete noob in that), that automatically replaces the hard-coded layer name and the attribute field name with a variable to replace them:

 CASE
 WHEN
 "layer"  =  variable_for_the_currently_edited_layers_name_this_attribute_field_belongs_to
 THEN
 "field" =  variable_for_current_attribute_field_name
 END

Having such variables at hand, I could just copy/paste the filter expression as is into each field in each layer, and everything would set up by itself!

I am sure, there might be other ways to achieve this, but as the filter with CASE WHEN worked so nicely, I thought I would give it a try. To no surprise, I can't find anything to give me those variables.


After lots of searching and lots of QGIS-crashes, i found:

https://gis.stackexchange.com/a/255530

with this custom function to return the active layer name

from qgis.core import *
from qgis.gui import *
from qgis.utils import iface

@qgsfunction(args='auto', group='Custom')
def ActiveLayerName(feature, parent):
    layer = iface.activeLayer()
    if layer != None :
      return layer.name()

This was presumably the easy part and indeed gives me a drop-down with the correctly filtered species names for the species_name field of the active layer, when adding a new feature.

 CASE
 WHEN
 "layer"  =  ActiveLayerName()
 THEN
 "field" =  'species_name' 
 END

I still have to hard-code the field name, and when I set an other layer active, I can not do edits in the attribute table anymore.

So no real success, but kind of a start.

1 Answer 1

4
+100

You could itterate through all layers in a pyqgis script or use it in filter function editor and call it in filters

layer = iface.mapCanvas().currentLayer()

def foo(layer):
    fields = layer.fields()
    for field in fields:
        filter = 'CASE\n WHEN\n "layer"  =  \''+ layer.name() +'\' \n THEN\n "field" =  \''+ field.name() + '\' \n END'
        config = {'AllowMulti': False,
              'AllowNull': False,
              'FilterExpression': filter,
              'Key': 'key',
              'LayerName': 'code',
              'NofColumns': 1,
              'OrderByValue': False,
              'UseCompleter': False,
              'Value': 'value',
              'Description': '"layer"'}
        i = fields.indexFromName(field.name())
        widget_setup = QgsEditorWidgetSetup('ValueRelation',config)
        layer.setEditorWidgetSetup(i, widget_setup)

foo(layer)

Tested in pygis editor with acitved mammals layer

How to quickly get widget settings:

In the python console you can get the current widget settings of your layer / field with this code. You can use it as starting point for your function. Delete the entries you don't need and update the other entries. So you can predesign your widget and copy the code:

layer = iface.mapCanvas().currentLayer()
field_index = layer.fields().indexOf('species')
field = layer.fields()[field_index]
config = field.editorWidgetSetup().config()
print(config)

enter image description here

Itterate layers:

Group your layers in a group:

enter image description here

And now itterate over all group layers:

root = QgsProject.instance().layerTreeRoot()
species_groupLayer = root.findGroup('species')

def iterate_speciesLayers(group):
    for layer in group.findLayers():
        fields = layer.fields()
        for field in fields:
            filter = 'CASE\n WHEN\n "layer"  =  \''+ layer.name() +'\' \n THEN\n "field" =  \''+ field.name() + '\' \n END'
            config = {'AllowMulti': False,
                  'AllowNull': False,
                  'FilterExpression': filter,
                  'Key': 'key',
                  'LayerName': 'code',
                  'NofColumns': 1,
                  'OrderByValue': False,
                  'UseCompleter': False,
                  'Value': 'value',
                  'Description': '"layer"'}
            i = fields.indexFromName(field.name())
            widget_setup = QgsEditorWidgetSetup('ValueRelation',config)
            layer.setEditorWidgetSetup(i, widget_setup)

iterate_speciesLayers(species_groupLayer)

enter image description here

With this workflow you can skip:

Having such variables at hand, I could just copy/paste the filter expression as is into each field in each layer, and everything would set up by itself!

You don't have to copy things to each layer and field

enter image description here

4
  • Try it, and perhaps you could responde if it works for you. But I had some problems to understand your ~database / layer structure. I'm not sure if this db structure is the best design for your mixed m:n / 1:n relationship. But that was not the question.
    – Xeppit
    Jan 31 at 14:57
  • I am still busy delivering the data in a format, the client of client thinks they want to have it ... which is complete bogus and even more hard to understand. They divide all data in layers per species group and then have value maps for each layer and each field for the allowed values to be filled in. Which are also inconsistent between layers for no reason. For each species group, there are either species names missing in the value map, or stati are incomplete or methods are missing. The peak of incompetence is reached with the actually stored values, which are integers.
    – Bernd V.
    Feb 1 at 15:21
  • My approach was meant to be even able to deal with all this stuff, so I created a lookup table for all constraints in all layers and fields to simply be able to overlook what is there and what is missing. From that, the idea derived to use this table for a Value relation instead of all those single hard-coded value maps. So my lookup table holds the layer name = species group| field name| value to be stored | description to be displayed | plus other crap that I changed from the original version.
    – Bernd V.
    Feb 1 at 15:31
  • But for sure I am always open to different approaches. The ultimate goal would be to find a procedure, that also work within QFIELD ... but that is just a wet dream for now.
    – Bernd V.
    Feb 1 at 15:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.