[{"data":1,"prerenderedAt":2225},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions":3},{"id":4,"title":5,"body":6,"description":2214,"extension":2215,"meta":2216,"navigation":217,"path":2221,"seo":2222,"stem":2223,"__hash__":2224},"docs\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Findex.md","Attribute Forms & Layer Actions in PyQGIS",{"type":7,"value":8,"toc":2200},"minimark",[9,13,17,26,156,161,181,184,441,453,457,460,544,552,669,677,681,693,786,801,912,929,933,936,1043,1051,1055,1058,1156,1164,1214,1223,1227,1234,1309,1322,1326,1341,1444,1458,1462,1465,2021,2030,2034,2037,2052,2056,2094,2098,2104,2118,2124,2130,2147,2151,2196],[10,11,5],"h1",{"id":12},"attribute-forms-layer-actions-in-pyqgis",[14,15,16],"p",{},"Most plugin work extends QGIS with new tools. This guide covers the other half: configuring the tools QGIS already has so that the people entering data get it right. Every vector layer carries a form configuration — which widget each field uses, what a new feature is filled with, which rules must hold before it can be saved, how fields are grouped into tabs — and a list of actions users can run against a feature. All of it is plain layer configuration, all of it can be built from Python, and together it decides whether a survey layer fills with clean records or with free-text noise.",[14,18,19,20,25],{},"This guide sits inside ",[21,22,24],"a",{"href":23},"\u002Fqgis-plugin-development\u002F","QGIS Plugin Development"," because that is where configuration code usually ends up living: in a plugin or a deployment script that sets up layers consistently for a whole team. It is written for developers and GIS leads who own a data model and want QGIS Desktop, and mobile apps that read the same projects, to enforce it.",[14,27,28],{},[29,30,35,39,43,50,59,69,76,79,83,88,92,95,99,102,106,109,113,120,126,131,134,138,142,148,152],"svg",{"viewBox":31,"role":32,"ariaLabel":33,"xmlns":34},"0 0 760 330","img","The layers of a layer's form configuration, from input control to behaviour: editor widgets, defaults, constraints, layout, init code and actions, arranged from declarative and portable at the bottom to scripted and desktop-only at the top","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[36,37,38],"title",{},"Six things a layer can say about its data entry",[40,41,42],"desc",{},"A stack of six bands. At the bottom, editor widgets control what can be typed. Above them, default values fill fields automatically. Constraints check values before saving. The form layout groups fields into tabs and conditional sections. These four are declarative, stored with the layer and honoured by mobile apps. Above them, form init code adds Python behaviour, and layer actions add commands users run against a feature; these two involve executable code and mostly run only in QGIS Desktop.",[44,45],"rect",{"x":46,"y":46,"width":47,"height":48,"fill":49},"0","760","330","#f6f3ea",[51,52,58],"text",{"x":53,"y":54,"style":55,"fill":56,"textAnchor":57},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Prefer the lower bands — they need no code at run time",[44,60],{"x":61,"y":62,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"120","46","440","38","8","#fdf2e2","#b45309","stroke-width:2",[51,70,75],{"x":71,"y":72,"style":73,"fill":74,"textAnchor":57},"340","70","text-anchor:middle;font-size:11px;font-family:sans-serif","#2f3b35","layer actions — commands on a feature",[44,77],{"x":61,"y":78,"width":63,"height":64,"rx":65,"fill":66,"stroke":67,"style":68},"90",[51,80,82],{"x":71,"y":81,"style":73,"fill":74,"textAnchor":57},"114","form init code — Python behaviour",[44,84],{"x":61,"y":85,"width":63,"height":64,"rx":65,"fill":86,"stroke":87,"style":68},"140","#eef7f4","#0f766e",[51,89,91],{"x":71,"y":90,"style":73,"fill":74,"textAnchor":57},"164","layout — tabs, groups, conditional sections",[44,93],{"x":61,"y":94,"width":63,"height":64,"rx":65,"fill":86,"stroke":87,"style":68},"184",[51,96,98],{"x":71,"y":97,"style":73,"fill":74,"textAnchor":57},"208","constraints — rules checked before saving",[44,100],{"x":61,"y":101,"width":63,"height":64,"rx":65,"fill":86,"stroke":87,"style":68},"228",[51,103,105],{"x":71,"y":104,"style":73,"fill":74,"textAnchor":57},"252","default values — filled automatically",[44,107],{"x":61,"y":108,"width":63,"height":64,"rx":65,"fill":86,"stroke":87,"style":68},"272",[51,110,112],{"x":71,"y":111,"style":73,"fill":74,"textAnchor":57},"296","editor widgets — what can be typed",[44,114],{"x":115,"y":62,"width":116,"height":117,"rx":65,"fill":118,"stroke":67,"style":119},"580","156","82","#fffdf7","stroke-width:1.8",[51,121,125],{"x":122,"y":123,"style":124,"fill":67,"textAnchor":57},"658","80","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif","scripted",[51,127,130],{"x":122,"y":128,"style":129,"fill":74,"textAnchor":57},"100","text-anchor:middle;font-size:10px;font-family:sans-serif","mostly desktop only",[44,132],{"x":115,"y":85,"width":116,"height":133,"rx":65,"fill":118,"stroke":87,"style":119},"170",[51,135,137],{"x":122,"y":136,"style":124,"fill":87,"textAnchor":57},"214","declarative",[51,139,141],{"x":122,"y":140,"style":129,"fill":74,"textAnchor":57},"234","desktop and mobile",[51,143,147],{"x":144,"y":145,"style":129,"fill":146,"textAnchor":57},"60","180","#59645f","stored",[51,149,151],{"x":144,"y":150,"style":129,"fill":146,"textAnchor":57},"196","with the",[51,153,155],{"x":144,"y":154,"style":129,"fill":146,"textAnchor":57},"212","layer",[157,158,160],"h2",{"id":159},"why-form-configuration-is-part-of-the-data-model","Why form configuration is part of the data model",[14,162,163,164,168,169,172,173,176,177,180],{},"It is easy to think of forms as presentation — something to tidy up after the schema is designed. In practice the form is where most data quality is won or lost. A ",[165,166,167],"code",{},"condition"," field defined as an integer column accepts ",[165,170,171],{},"7"," just as readily as ",[165,174,175],{},"4","; a ",[165,178,179],{},"species"," text column accepts every spelling a tired surveyor can produce. The database type is the weakest possible rule. The widget, the constraint and the default are where the real rules of the data model live for everyone who edits through QGIS.",[14,182,183],{},"That has a practical consequence for how configuration is managed. Configuration set by hand in the Layer Properties dialog lives in one project file on one machine. Configuration built by a script is reviewable, repeatable, and can be applied to every layer that shares the schema — the north and south survey layers, this year's and last year's, the template a new project starts from. The same script can save the configuration into the data source as a default style, so whoever adds the layer from the GeoPackage or PostGIS table gets the form without being told to load anything.",[185,186,191],"pre",{"className":187,"code":188,"language":189,"meta":190,"style":190},"language-python shiki shiki-themes github-dark","from qgis.core import QgsProject, QgsMapLayer\n\nlayer = QgsProject.instance().mapLayersByName(\"inspections\")[0]\nfields = layer.fields()\n\nfor idx, field in enumerate(fields):\n    widget = layer.editorWidgetSetup(idx)\n    default = layer.defaultValueDefinition(idx)\n    constraints = field.constraints().constraints()\n    print(f\"{field.name():\u003C18} widget={widget.type() or '-':\u003C16} \"\n          f\"default={default.expression() or '-':\u003C22} constraints={int(constraints)}\")\n\nprint(\"actions:\", [a.name() for a in layer.actions().actions()])\nprint(\"layout:\", layer.editFormConfig().layout())\n","python","",[165,192,193,212,219,244,255,260,278,289,300,311,360,399,404,428],{"__ignoreMap":190},[194,195,198,202,206,209],"span",{"class":196,"line":197},"line",1,[194,199,201],{"class":200},"snl16","from",[194,203,205],{"class":204},"s95oV"," qgis.core ",[194,207,208],{"class":200},"import",[194,210,211],{"class":204}," QgsProject, QgsMapLayer\n",[194,213,215],{"class":196,"line":214},2,[194,216,218],{"emptyLinePlaceholder":217},true,"\n",[194,220,222,225,228,231,235,238,241],{"class":196,"line":221},3,[194,223,224],{"class":204},"layer ",[194,226,227],{"class":200},"=",[194,229,230],{"class":204}," QgsProject.instance().mapLayersByName(",[194,232,234],{"class":233},"sU2Wk","\"inspections\"",[194,236,237],{"class":204},")[",[194,239,46],{"class":240},"sDLfK",[194,242,243],{"class":204},"]\n",[194,245,247,250,252],{"class":196,"line":246},4,[194,248,249],{"class":204},"fields ",[194,251,227],{"class":200},[194,253,254],{"class":204}," layer.fields()\n",[194,256,258],{"class":196,"line":257},5,[194,259,218],{"emptyLinePlaceholder":217},[194,261,263,266,269,272,275],{"class":196,"line":262},6,[194,264,265],{"class":200},"for",[194,267,268],{"class":204}," idx, field ",[194,270,271],{"class":200},"in",[194,273,274],{"class":240}," enumerate",[194,276,277],{"class":204},"(fields):\n",[194,279,281,284,286],{"class":196,"line":280},7,[194,282,283],{"class":204},"    widget ",[194,285,227],{"class":200},[194,287,288],{"class":204}," layer.editorWidgetSetup(idx)\n",[194,290,292,295,297],{"class":196,"line":291},8,[194,293,294],{"class":204},"    default ",[194,296,227],{"class":200},[194,298,299],{"class":204}," layer.defaultValueDefinition(idx)\n",[194,301,303,306,308],{"class":196,"line":302},9,[194,304,305],{"class":204},"    constraints ",[194,307,227],{"class":200},[194,309,310],{"class":204}," field.constraints().constraints()\n",[194,312,314,317,320,323,326,329,332,335,338,341,343,346,349,352,355,357],{"class":196,"line":313},10,[194,315,316],{"class":240},"    print",[194,318,319],{"class":204},"(",[194,321,322],{"class":200},"f",[194,324,325],{"class":233},"\"",[194,327,328],{"class":240},"{",[194,330,331],{"class":204},"field.name()",[194,333,334],{"class":200},":\u003C18",[194,336,337],{"class":240},"}",[194,339,340],{"class":233}," widget=",[194,342,328],{"class":240},[194,344,345],{"class":204},"widget.type() ",[194,347,348],{"class":200},"or",[194,350,351],{"class":233}," '-'",[194,353,354],{"class":200},":\u003C16",[194,356,337],{"class":240},[194,358,359],{"class":233}," \"\n",[194,361,363,366,369,371,374,376,378,381,383,386,389,392,394,396],{"class":196,"line":362},11,[194,364,365],{"class":200},"          f",[194,367,368],{"class":233},"\"default=",[194,370,328],{"class":240},[194,372,373],{"class":204},"default.expression() ",[194,375,348],{"class":200},[194,377,351],{"class":233},[194,379,380],{"class":200},":\u003C22",[194,382,337],{"class":240},[194,384,385],{"class":233}," constraints=",[194,387,388],{"class":240},"{int",[194,390,391],{"class":204},"(constraints)",[194,393,337],{"class":240},[194,395,325],{"class":233},[194,397,398],{"class":204},")\n",[194,400,402],{"class":196,"line":401},12,[194,403,218],{"emptyLinePlaceholder":217},[194,405,407,410,412,415,418,420,423,425],{"class":196,"line":406},13,[194,408,409],{"class":240},"print",[194,411,319],{"class":204},[194,413,414],{"class":233},"\"actions:\"",[194,416,417],{"class":204},", [a.name() ",[194,419,265],{"class":200},[194,421,422],{"class":204}," a ",[194,424,271],{"class":200},[194,426,427],{"class":204}," layer.actions().actions()])\n",[194,429,431,433,435,438],{"class":196,"line":430},14,[194,432,409],{"class":240},[194,434,319],{"class":204},[194,436,437],{"class":233},"\"layout:\"",[194,439,440],{"class":204},", layer.editFormConfig().layout())\n",[14,442,443,447,448,452],{},[444,445,446],"strong",{},"Breakdown:"," This is the whole form configuration of a layer in eight lines of output per field — the best first step when inheriting a project, and the natural basis for a check that a deployed layer still matches its specification. An empty widget type means the default for the field type; an empty default expression means none. The constraint value is a bit mask of not-null, unique and expression flags, decoded properly in ",[21,449,451],{"href":450},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fset-default-values-and-field-constraints-pyqgis\u002F","setting default values and field constraints",".",[157,454,456],{"id":455},"controlling-input-with-editor-widgets","Controlling input with editor widgets",[14,458,459],{},"Each field has one editor widget, defined by a type name and a configuration dictionary. The choice is the single most effective data-quality decision on a layer, because it turns open-ended typing into selection from valid options.",[14,461,462],{},[29,463,466,469,472,475,479,486,492,497,501,505,509,512,516,519,523,526,530,533,536,539],{"viewBox":464,"role":32,"ariaLabel":465,"xmlns":34},"0 0 760 270","Matching a field to an editor widget: coded values to ValueMap, lookups to ValueRelation, bounded numbers to Range, yes-no to CheckBox, dates to DateTime, file paths to ExternalResource, technical fields to Hidden",[36,467,468],{},"Pick the widget from what the field holds",[40,470,471],{},"Seven field types each point to a widget. Short fixed list of codes goes to ValueMap. Values maintained in another table go to ValueRelation. Measured numbers with bounds go to Range. Yes or no go to CheckBox. Dates and times go to DateTime with a calendar. Paths to photos or documents go to ExternalResource with a preview. Ids and audit columns go to Hidden.",[44,473],{"x":46,"y":46,"width":47,"height":474,"fill":49},"270",[51,476,478],{"x":53,"y":477,"style":55,"fill":56,"textAnchor":57},"26","What the field holds → which widget",[44,480],{"x":481,"y":482,"width":483,"height":484,"rx":485,"fill":118,"stroke":146,"style":68},"24","44","712","206","10",[51,487,491],{"x":488,"y":489,"style":490,"fill":74,"textAnchor":57},"220","72","text-anchor:middle;font-size:10.5px;font-family:sans-serif","a short fixed list of codes",[51,493,496],{"x":494,"y":489,"style":495,"fill":87,"textAnchor":57},"540","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:monospace","ValueMap",[51,498,500],{"x":488,"y":499,"style":490,"fill":74,"textAnchor":57},"98","values kept in another table",[51,502,504],{"x":494,"y":499,"style":495,"fill":503,"textAnchor":57},"#2563eb","ValueRelation",[51,506,508],{"x":488,"y":507,"style":490,"fill":74,"textAnchor":57},"124","a measurement with limits",[51,510,511],{"x":494,"y":507,"style":495,"fill":87,"textAnchor":57},"Range",[51,513,515],{"x":488,"y":514,"style":490,"fill":74,"textAnchor":57},"150","yes or no",[51,517,518],{"x":494,"y":514,"style":495,"fill":87,"textAnchor":57},"CheckBox",[51,520,522],{"x":488,"y":521,"style":490,"fill":74,"textAnchor":57},"176","a date or time",[51,524,525],{"x":494,"y":521,"style":495,"fill":87,"textAnchor":57},"DateTime",[51,527,529],{"x":488,"y":528,"style":490,"fill":74,"textAnchor":57},"202","a path to a photo or document",[51,531,532],{"x":494,"y":528,"style":495,"fill":67,"textAnchor":57},"ExternalResource",[51,534,535],{"x":488,"y":101,"style":490,"fill":74,"textAnchor":57},"an id or audit column",[51,537,538],{"x":494,"y":101,"style":495,"fill":146,"textAnchor":57},"Hidden",[196,540],{"x1":53,"y1":144,"x2":53,"y2":541,"stroke":542,"style":543},"236","#d9d3c4","stroke-width:1.5",[14,545,546,547,551],{},"Value maps suit short lists that are part of the schema; value relations suit long or shared lists kept in a lookup layer, and can filter their choices by an expression to build cascading drop-downs. Ranges, checkboxes and date pickers make typed input impossible to get wrong, and the attachment widget turns a stored path into an image preview. ",[21,548,550],{"href":549},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fconfigure-editor-widgets-pyqgis\u002F","Configuring editor widgets"," gives the configuration keys for each and a scheme-driven function that applies them across many layers.",[185,553,555],{"className":187,"code":554,"language":189,"meta":190,"style":190},"from qgis.core import QgsEditorWidgetSetup\n\nidx = fields.indexOf(\"condition\")\nlayer.setEditorWidgetSetup(idx, QgsEditorWidgetSetup(\"ValueMap\", {\n    \"map\": [{\"1 – dead or dying\": 1}, {\"2 – poor\": 2}, {\"3 – fair\": 3},\n            {\"4 – good\": 4}, {\"5 – excellent\": 5}],\n}))\n",[165,556,557,568,572,587,598,639,664],{"__ignoreMap":190},[194,558,559,561,563,565],{"class":196,"line":197},[194,560,201],{"class":200},[194,562,205],{"class":204},[194,564,208],{"class":200},[194,566,567],{"class":204}," QgsEditorWidgetSetup\n",[194,569,570],{"class":196,"line":214},[194,571,218],{"emptyLinePlaceholder":217},[194,573,574,577,579,582,585],{"class":196,"line":221},[194,575,576],{"class":204},"idx ",[194,578,227],{"class":200},[194,580,581],{"class":204}," fields.indexOf(",[194,583,584],{"class":233},"\"condition\"",[194,586,398],{"class":204},[194,588,589,592,595],{"class":196,"line":246},[194,590,591],{"class":204},"layer.setEditorWidgetSetup(idx, QgsEditorWidgetSetup(",[194,593,594],{"class":233},"\"ValueMap\"",[194,596,597],{"class":204},", {\n",[194,599,600,603,606,609,612,615,618,621,623,626,628,631,633,636],{"class":196,"line":257},[194,601,602],{"class":233},"    \"map\"",[194,604,605],{"class":204},": [{",[194,607,608],{"class":233},"\"1 – dead or dying\"",[194,610,611],{"class":204},": ",[194,613,614],{"class":240},"1",[194,616,617],{"class":204},"}, {",[194,619,620],{"class":233},"\"2 – poor\"",[194,622,611],{"class":204},[194,624,625],{"class":240},"2",[194,627,617],{"class":204},[194,629,630],{"class":233},"\"3 – fair\"",[194,632,611],{"class":204},[194,634,635],{"class":240},"3",[194,637,638],{"class":204},"},\n",[194,640,641,644,647,649,651,653,656,658,661],{"class":196,"line":262},[194,642,643],{"class":204},"            {",[194,645,646],{"class":233},"\"4 – good\"",[194,648,611],{"class":204},[194,650,175],{"class":240},[194,652,617],{"class":204},[194,654,655],{"class":233},"\"5 – excellent\"",[194,657,611],{"class":204},[194,659,660],{"class":240},"5",[194,662,663],{"class":204},"}],\n",[194,665,666],{"class":196,"line":280},[194,667,668],{"class":204},"}))\n",[14,670,671,673,674,676],{},[444,672,446],{}," The stored values are integers to match the field type, and the labels carry the meaning so users never need to remember what ",[165,675,625],{}," stands for. The attribute table displays the labels too, and so do identify results, which removes a whole class of \"what does this code mean\" questions.",[157,678,680],{"id":679},"defaults-and-constraints","Defaults and constraints",[14,682,683,684,688,689,692],{},"Widgets constrain the user interface; defaults and constraints act on the values themselves. A default expression fills a field when a feature is created — a UUID, a timestamp, the editor's login — and can recalculate on every later edit for audit fields and derived values such as area. A constraint is a rule checked before saving: not null, unique, or any expression, with ",[685,686,687],"em",{},"hard"," strength to block and ",[685,690,691],{},"soft"," strength to warn.",[14,694,695],{},[29,696,699,702,705,708,721,724,729,735,739,743,747,751,755,758,762,765,769,773,776,778,781],{"viewBox":697,"role":32,"ariaLabel":698,"xmlns":34},"0 0 760 250","Defaults and constraints around the save button: defaults run before the user sees the form, constraints run as the user edits and when saving, and script-created features need QgsVectorLayerUtils to get the same treatment",[36,700,701],{},"Both ends of an edit",[40,703,704],{},"On the left, defaults fill uuid, created_at and created_by as the feature is created. In the middle the user edits. On the right, constraints check condition range, a required asset id and a conditional note before the save is accepted. Below, a separate path shows a script creating a feature: without QgsVectorLayerUtils neither defaults nor constraints apply; with createFeature and validateAttribute both do.",[44,706],{"x":46,"y":46,"width":47,"height":707,"fill":49},"250",[709,710,711],"defs",{},[712,713,717],"marker",{"id":714,"viewBox":715,"refX":65,"refY":660,"markerWidth":171,"markerHeight":171,"orient":716},"afDcArrow","0 0 10 10","auto-start-reverse",[718,719],"path",{"d":720,"fill":74},"M0 0 L10 5 L0 10 z",[51,722,723],{"x":53,"y":477,"style":55,"fill":56,"textAnchor":57},"Defaults open the edit, constraints close it",[44,725],{"x":481,"y":726,"width":727,"height":728,"rx":485,"fill":86,"stroke":87,"style":68},"48","210","96",[51,730,734],{"x":731,"y":732,"style":733,"fill":87,"textAnchor":57},"129","74","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","defaults",[51,736,738],{"x":731,"y":499,"style":737,"fill":74,"textAnchor":57},"text-anchor:middle;font-size:9.5px;font-family:monospace","uuid, now()",[51,740,742],{"x":731,"y":741,"style":737,"fill":74,"textAnchor":57},"118","@user_account_name",[196,744],{"x1":140,"y1":728,"x2":745,"y2":728,"stroke":74,"style":746},"276","stroke-width:1.8;marker-end:url(#afDcArrow)",[44,748],{"x":749,"y":726,"width":750,"height":728,"rx":485,"fill":118,"stroke":146,"style":68},"284","192",[51,752,754],{"x":53,"y":753,"style":733,"fill":56,"textAnchor":57},"92","user edits",[51,756,757],{"x":53,"y":81,"style":129,"fill":74,"textAnchor":57},"through the widgets",[196,759],{"x1":760,"y1":728,"x2":761,"y2":728,"stroke":74,"style":746},"476","518",[44,763],{"x":764,"y":726,"width":727,"height":728,"rx":485,"fill":66,"stroke":67,"style":68},"526",[51,766,768],{"x":767,"y":732,"style":733,"fill":67,"textAnchor":57},"631","constraints",[51,770,772],{"x":767,"y":499,"style":771,"fill":74,"textAnchor":57},"text-anchor:middle;font-size:9.5px;font-family:sans-serif","hard: block the save",[51,774,775],{"x":767,"y":741,"style":771,"fill":74,"textAnchor":57},"soft: warn, allow",[44,777],{"x":481,"y":133,"width":483,"height":144,"rx":485,"fill":118,"stroke":503,"style":68},[51,779,780],{"x":53,"y":150,"style":124,"fill":503,"textAnchor":57},"features created by scripts skip both — unless created through QgsVectorLayerUtils",[51,782,785],{"x":53,"y":783,"style":784,"fill":74,"textAnchor":57},"216","text-anchor:middle;font-size:10px;font-family:monospace","createFeature(…) then validateAttribute(…)",[14,787,788,789,792,793,796,797,800],{},"The important distinction is between constraints that come from the database and constraints set on the layer. Provider constraints — ",[165,790,791],{},"NOT NULL",", ",[165,794,795],{},"UNIQUE"," and ",[165,798,799],{},"CHECK"," in PostGIS or GeoPackage — are read by QGIS automatically and protect the data from every client. Layer constraints can use any QGIS expression and can be soft, but only apply to edits made through QGIS. Put the rules that must never be broken in the database, and use layer constraints for the richer, conditional rules that make forms helpful.",[185,802,804],{"className":187,"code":803,"language":189,"meta":190,"style":190},"from qgis.core import QgsDefaultValue, QgsFieldConstraints\n\nlayer.setDefaultValueDefinition(fields.indexOf(\"created_at\"), QgsDefaultValue(\"now()\"))\nlayer.setDefaultValueDefinition(fields.indexOf(\"modified_at\"), QgsDefaultValue(\"now()\", True))\n\ncond = fields.indexOf(\"condition\")\nlayer.setConstraintExpression(cond, '\"condition\" BETWEEN 1 AND 5', \"Score 1 to 5\")\nlayer.setFieldConstraint(\n    cond,\n    QgsFieldConstraints.Constraint.ConstraintExpression,\n    QgsFieldConstraints.ConstraintStrength.ConstraintStrengthHard,\n)\n",[165,805,806,817,821,838,856,860,873,888,893,898,903,908],{"__ignoreMap":190},[194,807,808,810,812,814],{"class":196,"line":197},[194,809,201],{"class":200},[194,811,205],{"class":204},[194,813,208],{"class":200},[194,815,816],{"class":204}," QgsDefaultValue, QgsFieldConstraints\n",[194,818,819],{"class":196,"line":214},[194,820,218],{"emptyLinePlaceholder":217},[194,822,823,826,829,832,835],{"class":196,"line":221},[194,824,825],{"class":204},"layer.setDefaultValueDefinition(fields.indexOf(",[194,827,828],{"class":233},"\"created_at\"",[194,830,831],{"class":204},"), QgsDefaultValue(",[194,833,834],{"class":233},"\"now()\"",[194,836,837],{"class":204},"))\n",[194,839,840,842,845,847,849,851,854],{"class":196,"line":246},[194,841,825],{"class":204},[194,843,844],{"class":233},"\"modified_at\"",[194,846,831],{"class":204},[194,848,834],{"class":233},[194,850,792],{"class":204},[194,852,853],{"class":240},"True",[194,855,837],{"class":204},[194,857,858],{"class":196,"line":257},[194,859,218],{"emptyLinePlaceholder":217},[194,861,862,865,867,869,871],{"class":196,"line":262},[194,863,864],{"class":204},"cond ",[194,866,227],{"class":200},[194,868,581],{"class":204},[194,870,584],{"class":233},[194,872,398],{"class":204},[194,874,875,878,881,883,886],{"class":196,"line":280},[194,876,877],{"class":204},"layer.setConstraintExpression(cond, ",[194,879,880],{"class":233},"'\"condition\" BETWEEN 1 AND 5'",[194,882,792],{"class":204},[194,884,885],{"class":233},"\"Score 1 to 5\"",[194,887,398],{"class":204},[194,889,890],{"class":196,"line":291},[194,891,892],{"class":204},"layer.setFieldConstraint(\n",[194,894,895],{"class":196,"line":302},[194,896,897],{"class":204},"    cond,\n",[194,899,900],{"class":196,"line":313},[194,901,902],{"class":204},"    QgsFieldConstraints.Constraint.ConstraintExpression,\n",[194,904,905],{"class":196,"line":362},[194,906,907],{"class":204},"    QgsFieldConstraints.ConstraintStrength.ConstraintStrengthHard,\n",[194,909,910],{"class":196,"line":401},[194,911,398],{"class":204},[14,913,914,916,917,920,921,924,925,928],{},[444,915,446],{}," The second argument to ",[165,918,919],{},"QgsDefaultValue"," is ",[165,922,923],{},"applyOnUpdate","; true for ",[165,926,927],{},"modified_at"," so every edit refreshes it. An expression constraint needs both calls — one for the rule and its message, one for its strength — and forgetting the second is the usual reason a rule appears in the form but never blocks anything.",[157,930,932],{"id":931},"layout-tabs-groups-and-conditional-sections","Layout: tabs, groups and conditional sections",[14,934,935],{},"For layers with more than a handful of fields, the generated single-column form is a wall of inputs. The drag-and-drop layout replaces it with a tree: tabs under an invisible root, group boxes inside tabs, fields inside both, and optional visibility expressions on any container so a section appears only when the answers so far make it relevant.",[14,937,938],{},[29,939,942,945,948,951,954,959,963,966,969,972,975,978,981,984,988,991,995,1000,1007,1011,1015,1018,1022,1026,1029,1031,1033,1036,1038],{"viewBox":940,"role":32,"ariaLabel":941,"xmlns":34},"0 0 760 244","An inspection form before and after layout: forty fields in one scrolling column become three tabs, two columns on the first tab, and a damage section that only appears for poor condition",[36,943,944],{},"From a scrolling list to a designed form",[40,946,947],{},"Left, a tall narrow column of forty field rows with a scrollbar. Right, a form with three tabs labelled Site, Condition and Follow-up. The Site tab shows fields in two columns. A small inset shows the Condition tab with a Damage group that appears only when condition is poor, and the Follow-up tab embedding a table of related repair records.",[44,949],{"x":46,"y":46,"width":47,"height":950,"fill":49},"244",[51,952,953],{"x":53,"y":477,"style":55,"fill":56,"textAnchor":57},"Same fields, far less scrolling",[44,955],{"x":481,"y":482,"width":956,"height":957,"rx":485,"fill":118,"stroke":958,"style":68},"200","182","#b91c1c",[44,960],{"x":961,"y":962,"width":514,"height":485,"rx":625,"fill":542},"40","58",[44,964],{"x":961,"y":965,"width":514,"height":485,"rx":625,"fill":542},"76",[44,967],{"x":961,"y":968,"width":514,"height":485,"rx":625,"fill":542},"94",[44,970],{"x":961,"y":971,"width":514,"height":485,"rx":625,"fill":542},"112",[44,973],{"x":961,"y":974,"width":514,"height":485,"rx":625,"fill":542},"130",[44,976],{"x":961,"y":977,"width":514,"height":485,"rx":625,"fill":542},"148",[44,979],{"x":961,"y":980,"width":514,"height":485,"rx":625,"fill":542},"166",[44,982],{"x":956,"y":962,"width":485,"height":514,"rx":635,"fill":983},"#e8e4d8",[44,985],{"x":956,"y":962,"width":485,"height":986,"rx":635,"fill":987},"30","#9aa39d",[51,989,990],{"x":507,"y":727,"style":129,"fill":958,"textAnchor":57},"40 fields, one column",[44,992],{"x":104,"y":482,"width":993,"height":957,"rx":485,"fill":118,"stroke":994,"style":68},"484","#15803d",[44,996],{"x":997,"y":998,"width":123,"height":481,"rx":175,"fill":999,"stroke":994,"style":543},"268","56","#e8efe6",[51,1001,1006],{"x":1002,"y":1003,"style":1004,"fill":1005,"textAnchor":57},"308","73","text-anchor:middle;font-size:10px;font-weight:bold;font-family:sans-serif","#166534","Site",[44,1008],{"x":1009,"y":998,"width":78,"height":481,"rx":175,"fill":118,"stroke":987,"style":1010},"354","stroke-width:1.2",[51,1012,1014],{"x":1013,"y":1003,"style":129,"fill":74,"textAnchor":57},"399","Condition",[44,1016],{"x":1017,"y":998,"width":78,"height":481,"rx":175,"fill":118,"stroke":987,"style":1010},"450",[51,1019,1021],{"x":1020,"y":1003,"style":129,"fill":74,"textAnchor":57},"495","Follow-up",[44,1023],{"x":997,"y":728,"width":727,"height":1024,"rx":625,"fill":1025},"12","#c9d6cf",[44,1027],{"x":1028,"y":728,"width":727,"height":1024,"rx":625,"fill":1025},"498",[44,1030],{"x":997,"y":61,"width":727,"height":1024,"rx":625,"fill":1025},[44,1032],{"x":1028,"y":61,"width":727,"height":1024,"rx":625,"fill":1025},[44,1034],{"x":997,"y":1035,"width":727,"height":1024,"rx":625,"fill":1025},"144",[44,1037],{"x":1028,"y":1035,"width":727,"height":1024,"rx":625,"fill":1025},[51,1039,1042],{"x":1040,"y":1041,"style":490,"fill":1005,"textAnchor":57},"494","190","tabs, two columns, sections that appear when relevant",[14,1044,1045,1046,1050],{},"Relation elements bring child records — repairs, photos, follow-up visits — inside the parent's form, and aliases give every field a readable label everywhere it is shown. ",[21,1047,1049],{"href":1048},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fbuild-tabbed-attribute-form-layout-pyqgis\u002F","Building a tabbed attribute form layout"," constructs the tree from containers and fields, turns the layout into a specification applied by one function, and reads hand-designed forms back into that specification.",[157,1052,1054],{"id":1053},"behaviour-python-inside-the-form","Behaviour: Python inside the form",[14,1056,1057],{},"When a rule cannot be written declaratively — filling several fields from a lookup when an asset id is typed, checking a value against an external service before saving — a form can run a Python init function when it opens. The function receives the form, the layer and the feature, and connects handlers to the form's signals.",[14,1059,1060],{},[29,1061,1064,1067,1070,1072,1079,1082,1086,1090,1093,1098,1101,1105,1108,1111,1114,1118,1121,1125,1128,1132,1135,1139,1142,1146,1149,1153],{"viewBox":1062,"role":32,"ariaLabel":1063,"xmlns":34},"0 0 760 236","A decision ladder before writing form init code: try a default value, then a constraint expression, then a visibility expression, then a value relation filter, and only then Python init code",[36,1065,1066],{},"Exhaust the declarative options first",[40,1068,1069],{},"Five steps from left to right. Can a default expression fill it? Can a constraint expression check it? Can a visibility expression show or hide it? Can a value relation filter restrict the choices? Only if all four answers are no, write Python init code, preferably shipped as a module in a plugin.",[44,1071],{"x":46,"y":46,"width":47,"height":541,"fill":49},[709,1073,1074],{},[712,1075,1077],{"id":1076,"viewBox":715,"refX":65,"refY":660,"markerWidth":171,"markerHeight":171,"orient":716},"afLadderArrow",[718,1078],{"d":720,"fill":74},[51,1080,1081],{"x":53,"y":477,"style":55,"fill":56,"textAnchor":57},"Only reach for Python when every “no” is honest",[44,1083],{"x":1084,"y":72,"width":1085,"height":123,"rx":485,"fill":86,"stroke":87,"style":68},"16","128",[51,1087,1089],{"x":123,"y":1088,"style":124,"fill":87,"textAnchor":57},"102","default",[51,1091,1092],{"x":123,"y":507,"style":129,"fill":74,"textAnchor":57},"fill it?",[196,1094],{"x1":1035,"y1":1095,"x2":1096,"y2":1095,"stroke":74,"style":1097},"110","162","stroke-width:1.8;marker-end:url(#afLadderArrow)",[44,1099],{"x":1100,"y":72,"width":1085,"height":123,"rx":485,"fill":86,"stroke":87,"style":68},"168",[51,1102,1104],{"x":1103,"y":1088,"style":124,"fill":87,"textAnchor":57},"232","constraint",[51,1106,1107],{"x":1103,"y":507,"style":129,"fill":74,"textAnchor":57},"check it?",[196,1109],{"x1":111,"y1":1095,"x2":1110,"y2":1095,"stroke":74,"style":1097},"314",[44,1112],{"x":1113,"y":72,"width":1085,"height":123,"rx":485,"fill":86,"stroke":87,"style":68},"320",[51,1115,1117],{"x":1116,"y":1088,"style":124,"fill":87,"textAnchor":57},"384","visibility",[51,1119,1120],{"x":1116,"y":507,"style":129,"fill":74,"textAnchor":57},"show or hide it?",[196,1122],{"x1":1123,"y1":1095,"x2":1124,"y2":1095,"stroke":74,"style":1097},"448","466",[44,1126],{"x":1127,"y":72,"width":1085,"height":123,"rx":485,"fill":86,"stroke":87,"style":68},"472",[51,1129,1131],{"x":1130,"y":1088,"style":124,"fill":87,"textAnchor":57},"536","relation filter",[51,1133,1134],{"x":1130,"y":507,"style":129,"fill":74,"textAnchor":57},"restrict choices?",[196,1136],{"x1":1137,"y1":1095,"x2":1138,"y2":1095,"stroke":74,"style":1097},"600","618",[44,1140],{"x":1141,"y":72,"width":61,"height":123,"rx":485,"fill":66,"stroke":67,"style":68},"624",[51,1143,1145],{"x":1144,"y":1088,"style":124,"fill":67,"textAnchor":57},"684","init code",[51,1147,1148],{"x":1144,"y":507,"style":129,"fill":74,"textAnchor":57},"last resort",[44,1150],{"x":61,"y":1151,"width":1152,"height":64,"rx":65,"fill":118,"stroke":146,"style":119},"178","520",[51,1154,1155],{"x":53,"y":528,"style":490,"fill":74,"textAnchor":57},"declarative rules also work in QField and in the attribute table",[14,1157,1158,1159,1163],{},"Python in forms is powerful and has real costs. It does not run in mobile apps, it only runs for feature forms and not for edits in the attribute table, and code embedded in a project runs on the machine of whoever opens it. ",[21,1160,1162],{"href":1161},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fadd-python-logic-to-attribute-form-pyqgis\u002F","Adding Python logic to an attribute form"," shows how to write init functions that react through the form rather than fragile individual widgets, how to intercept saving for checks nothing else can express, and why shipping the code as a module in a plugin is better than pasting it into a project.",[185,1165,1167],{"className":187,"code":1166,"language":189,"meta":190,"style":190},"from qgis.core import Qgis\n\nconfig = layer.editFormConfig()\nconfig.setInitCodeSource(Qgis.AttributeFormPythonInitCodeSource.Environment)\nconfig.setInitFunction(\"survey_forms.inspections.form_open\")\nlayer.setEditFormConfig(config)\n",[165,1168,1169,1180,1184,1194,1199,1209],{"__ignoreMap":190},[194,1170,1171,1173,1175,1177],{"class":196,"line":197},[194,1172,201],{"class":200},[194,1174,205],{"class":204},[194,1176,208],{"class":200},[194,1178,1179],{"class":204}," Qgis\n",[194,1181,1182],{"class":196,"line":214},[194,1183,218],{"emptyLinePlaceholder":217},[194,1185,1186,1189,1191],{"class":196,"line":221},[194,1187,1188],{"class":204},"config ",[194,1190,227],{"class":200},[194,1192,1193],{"class":204}," layer.editFormConfig()\n",[194,1195,1196],{"class":196,"line":246},[194,1197,1198],{"class":204},"config.setInitCodeSource(Qgis.AttributeFormPythonInitCodeSource.Environment)\n",[194,1200,1201,1204,1207],{"class":196,"line":257},[194,1202,1203],{"class":204},"config.setInitFunction(",[194,1205,1206],{"class":233},"\"survey_forms.inspections.form_open\"",[194,1208,398],{"class":204},[194,1210,1211],{"class":196,"line":262},[194,1212,1213],{"class":204},"layer.setEditFormConfig(config)\n",[14,1215,1216,1218,1219,1222],{},[444,1217,446],{}," The ",[165,1220,1221],{},"Environment"," source names a function importable from QGIS's Python path, such as a module inside an installed plugin. The project stores only the dotted name, so the logic is versioned, tested and updated with the plugin rather than copied into every project file.",[157,1224,1226],{"id":1225},"layer-actions","Layer actions",[14,1228,1229,1230,1233],{},"Actions are commands attached to a layer and run against a feature: open the asset's record in another system, select every parcel with the same owner, copy a value, export the selection for review. They appear in the ",[685,1231,1232],{},"Run Feature Action"," map tool, as buttons in the attribute table, and in feature forms, depending on their scopes. URL actions open a link built from the feature's attributes; Python actions run code inside QGIS.",[14,1235,1236],{},[29,1237,1239,1242,1245,1247,1254,1257,1261,1266,1269,1273,1276,1279,1285,1289,1293,1297,1300,1303,1306],{"viewBox":1062,"role":32,"ariaLabel":1238,"xmlns":34},"When to use a layer action versus a plugin: an action lives with one layer and needs nothing installed, a plugin lives with the user's QGIS and serves every layer, with a graduation path from one to the other",[36,1240,1241],{},"Action or plugin?",[40,1243,1244],{},"A layer action is stored with one layer, needs nothing installed, is ideal for a link to another system or a small selection helper, and is limited to one feature or the layer. A plugin is installed per user, serves every layer and project, can add dialogs, map tools and background tasks, and is the right home once several layers need the same command. An arrow shows graduating an action into a plugin when it is copied into a third project.",[44,1246],{"x":46,"y":46,"width":47,"height":541,"fill":49},[709,1248,1249],{},[712,1250,1252],{"id":1251,"viewBox":715,"refX":65,"refY":660,"markerWidth":171,"markerHeight":171,"orient":716},"afGradArrow",[718,1253],{"d":720,"fill":74},[51,1255,1256],{"x":53,"y":477,"style":55,"fill":56,"textAnchor":57},"Start small, graduate when it spreads",[44,1258],{"x":481,"y":62,"width":1259,"height":133,"rx":485,"fill":86,"stroke":87,"style":1260},"300","stroke-width:2.5",[51,1262,1265],{"x":1263,"y":489,"style":1264,"fill":87,"textAnchor":57},"174","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","layer action",[51,1267,1268],{"x":1263,"y":1088,"style":490,"fill":74,"textAnchor":57},"stored with one layer",[51,1270,1272],{"x":1263,"y":1271,"style":490,"fill":74,"textAnchor":57},"126","nothing to install",[51,1274,1275],{"x":1263,"y":514,"style":490,"fill":74,"textAnchor":57},"links, small helpers",[51,1277,1278],{"x":1263,"y":750,"style":129,"fill":146,"textAnchor":57},"copied into a third project?",[196,1280],{"x1":1281,"y1":1282,"x2":1283,"y2":1282,"stroke":74,"style":1284},"324","131","428","stroke-width:2;marker-end:url(#afGradArrow)",[51,1286,1288],{"x":1287,"y":61,"style":129,"fill":146,"textAnchor":57},"376","graduate",[44,1290],{"x":1291,"y":62,"width":1259,"height":133,"rx":485,"fill":1292,"stroke":503,"style":1260},"436","#eff3ff",[51,1294,1296],{"x":1295,"y":489,"style":1264,"fill":503,"textAnchor":57},"586","plugin",[51,1298,1299],{"x":1295,"y":1088,"style":490,"fill":74,"textAnchor":57},"installed per user",[51,1301,1302],{"x":1295,"y":1271,"style":490,"fill":74,"textAnchor":57},"serves every layer",[51,1304,1305],{"x":1295,"y":514,"style":490,"fill":74,"textAnchor":57},"dialogs, tools, tasks",[51,1307,1308],{"x":1295,"y":750,"style":129,"fill":146,"textAnchor":57},"tested and versioned",[14,1310,1311,1312,1316,1317,1321],{},"The rule that keeps Python actions safe is simple and often broken: substitute identifiers into action text, never free-text attribute values. A placeholder is replaced by the bare text of the value, so an owner called O'Brien breaks the code, and a value chosen maliciously can run anything. Passing the layer id and feature id and reading attributes inside the code keeps data as data. ",[21,1313,1315],{"href":1314},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fcreate-layer-actions-pyqgis\u002F","Creating layer actions"," builds URL and Python actions with that pattern, explains the four scopes, runs actions from scripts, and installs them idempotently. For commands that belong to the layer tree rather than to one layer's features, ",[21,1318,1320],{"href":1319},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fadd-layer-context-menu-action-pyqgis\u002F","adding a layer context menu action"," is the plugin-side equivalent.",[157,1323,1325],{"id":1324},"packaging-a-configuration-so-it-travels","Packaging a configuration so it travels",[14,1327,1328,1329,1332,1333,1336,1337,1340],{},"Everything in this guide is stored in the project by default, which is fine for a single project and wrong for a team. The durable approach is to save the relevant style categories — ",[165,1330,1331],{},"Fields"," for aliases, defaults and constraints, ",[165,1334,1335],{},"Forms"," for widgets and layout, ",[165,1338,1339],{},"Actions"," for actions — into the data source as the layer's default style, and to keep the script that builds them in version control.",[185,1342,1344],{"className":187,"code":1343,"language":189,"meta":190,"style":190},"from qgis.core import QgsMapLayer\n\ncategories = (QgsMapLayer.StyleCategory.Fields\n              | QgsMapLayer.StyleCategory.Forms\n              | QgsMapLayer.StyleCategory.Actions)\nerror = layer.saveStyleToDatabase(\"survey-form\", \"form, constraints and actions\",\n                                  True, \"\", categories)\nprint(\"saved as default\" if not error else error)\n",[165,1345,1346,1357,1361,1371,1379,1386,1407,1420],{"__ignoreMap":190},[194,1347,1348,1350,1352,1354],{"class":196,"line":197},[194,1349,201],{"class":200},[194,1351,205],{"class":204},[194,1353,208],{"class":200},[194,1355,1356],{"class":204}," QgsMapLayer\n",[194,1358,1359],{"class":196,"line":214},[194,1360,218],{"emptyLinePlaceholder":217},[194,1362,1363,1366,1368],{"class":196,"line":221},[194,1364,1365],{"class":204},"categories ",[194,1367,227],{"class":200},[194,1369,1370],{"class":204}," (QgsMapLayer.StyleCategory.Fields\n",[194,1372,1373,1376],{"class":196,"line":246},[194,1374,1375],{"class":200},"              |",[194,1377,1378],{"class":204}," QgsMapLayer.StyleCategory.Forms\n",[194,1380,1381,1383],{"class":196,"line":257},[194,1382,1375],{"class":200},[194,1384,1385],{"class":204}," QgsMapLayer.StyleCategory.Actions)\n",[194,1387,1388,1391,1393,1396,1399,1401,1404],{"class":196,"line":262},[194,1389,1390],{"class":204},"error ",[194,1392,227],{"class":200},[194,1394,1395],{"class":204}," layer.saveStyleToDatabase(",[194,1397,1398],{"class":233},"\"survey-form\"",[194,1400,792],{"class":204},[194,1402,1403],{"class":233},"\"form, constraints and actions\"",[194,1405,1406],{"class":204},",\n",[194,1408,1409,1412,1414,1417],{"class":196,"line":280},[194,1410,1411],{"class":240},"                                  True",[194,1413,792],{"class":204},[194,1415,1416],{"class":233},"\"\"",[194,1418,1419],{"class":204},", categories)\n",[194,1421,1422,1424,1426,1429,1432,1435,1438,1441],{"class":196,"line":291},[194,1423,409],{"class":240},[194,1425,319],{"class":204},[194,1427,1428],{"class":233},"\"saved as default\"",[194,1430,1431],{"class":200}," if",[194,1433,1434],{"class":200}," not",[194,1436,1437],{"class":204}," error ",[194,1439,1440],{"class":200},"else",[194,1442,1443],{"class":204}," error)\n",[14,1445,1446,1448,1449,1452,1453,1457],{},[444,1447,446],{}," Combining categories with ",[165,1450,1451],{},"|"," saves exactly the data-entry configuration and nothing else, so cartographers can keep changing symbology without overwriting the form. Marking the style as default means QGIS applies it whenever the layer is added from that GeoPackage table or PostGIS table — in a new project, on a colleague's machine, or in a mobile app that reads the layer. The plugin that builds the configuration can re-run on every release, so a changed rule reaches every user the next time they add the layer; ",[21,1454,1456],{"href":1455},"\u002Fqgis-plugin-development\u002Fpublishing-to-the-qgis-plugin-repository\u002Fversion-and-changelog-qgis-plugin\u002F","versioning a plugin"," is then also versioning the data model's rules.",[157,1459,1461],{"id":1460},"checking-a-deployed-configuration","Checking a deployed configuration",[14,1463,1464],{},"Configuration drifts. Someone opens Layer Properties to fix a typo in a drop-down and saves the project; a new field is added to the table but never to the form; a colleague copies a layer into another project and edits its constraints there. A check that compares each layer against the specification the plugin applies catches that drift before it shows up as bad data.",[185,1466,1468],{"className":187,"code":1467,"language":189,"meta":190,"style":190},"from qgis.core import QgsFieldConstraints\n\nEXPECTED = {\n    \"condition\": {\"widget\": \"ValueMap\", \"hard_expression\": True},\n    \"asset_id\": {\"widget\": \"TextEdit\", \"not_null\": True},\n    \"surveyed_on\": {\"widget\": \"DateTime\", \"default\": \"now()\"},\n}\n\ndef audit_form(layer, expected):\n    problems = []\n    names = layer.fields().names()\n    for name, spec in expected.items():\n        if name not in names:\n            problems.append(f\"{name}: field missing\")\n            continue\n        idx = names.index(name)\n        field = layer.fields().at(idx)\n        widget = layer.editorWidgetSetup(idx).type()\n        if widget != spec[\"widget\"]:\n            problems.append(f\"{name}: widget {widget or 'default'} != {spec['widget']}\")\n        if \"default\" in spec and layer.defaultValueDefinition(idx).expression() != spec[\"default\"]:\n            problems.append(f\"{name}: default changed\")\n        c = field.constraints()\n        if spec.get(\"not_null\") and not (c.constraints() & QgsFieldConstraints.Constraint.ConstraintNotNull):\n            problems.append(f\"{name}: not-null constraint missing\")\n        if spec.get(\"hard_expression\") and not c.constraintExpression():\n            problems.append(f\"{name}: expression constraint missing\")\n    unplaced = set(names) - {e.name() for tab in layer.editFormConfig().invisibleRootContainer().children()\n                             for e in tab.children()}\n    return problems, sorted(unplaced)\n\nproblems, unplaced = audit_form(layer, EXPECTED)\nprint(problems or \"matches specification\")\nprint(\"fields not on any tab:\", unplaced)\n",[165,1469,1470,1481,1485,1496,1522,1547,1572,1577,1581,1593,1603,1613,1626,1643,1664,1670,1681,1692,1703,1722,1772,1799,1819,1830,1856,1876,1894,1914,1944,1958,1973,1978,1993,2008],{"__ignoreMap":190},[194,1471,1472,1474,1476,1478],{"class":196,"line":197},[194,1473,201],{"class":200},[194,1475,205],{"class":204},[194,1477,208],{"class":200},[194,1479,1480],{"class":204}," QgsFieldConstraints\n",[194,1482,1483],{"class":196,"line":214},[194,1484,218],{"emptyLinePlaceholder":217},[194,1486,1487,1490,1493],{"class":196,"line":221},[194,1488,1489],{"class":240},"EXPECTED",[194,1491,1492],{"class":200}," =",[194,1494,1495],{"class":204}," {\n",[194,1497,1498,1501,1504,1507,1509,1511,1513,1516,1518,1520],{"class":196,"line":246},[194,1499,1500],{"class":233},"    \"condition\"",[194,1502,1503],{"class":204},": {",[194,1505,1506],{"class":233},"\"widget\"",[194,1508,611],{"class":204},[194,1510,594],{"class":233},[194,1512,792],{"class":204},[194,1514,1515],{"class":233},"\"hard_expression\"",[194,1517,611],{"class":204},[194,1519,853],{"class":240},[194,1521,638],{"class":204},[194,1523,1524,1527,1529,1531,1533,1536,1538,1541,1543,1545],{"class":196,"line":257},[194,1525,1526],{"class":233},"    \"asset_id\"",[194,1528,1503],{"class":204},[194,1530,1506],{"class":233},[194,1532,611],{"class":204},[194,1534,1535],{"class":233},"\"TextEdit\"",[194,1537,792],{"class":204},[194,1539,1540],{"class":233},"\"not_null\"",[194,1542,611],{"class":204},[194,1544,853],{"class":240},[194,1546,638],{"class":204},[194,1548,1549,1552,1554,1556,1558,1561,1563,1566,1568,1570],{"class":196,"line":262},[194,1550,1551],{"class":233},"    \"surveyed_on\"",[194,1553,1503],{"class":204},[194,1555,1506],{"class":233},[194,1557,611],{"class":204},[194,1559,1560],{"class":233},"\"DateTime\"",[194,1562,792],{"class":204},[194,1564,1565],{"class":233},"\"default\"",[194,1567,611],{"class":204},[194,1569,834],{"class":233},[194,1571,638],{"class":204},[194,1573,1574],{"class":196,"line":280},[194,1575,1576],{"class":204},"}\n",[194,1578,1579],{"class":196,"line":291},[194,1580,218],{"emptyLinePlaceholder":217},[194,1582,1583,1586,1590],{"class":196,"line":302},[194,1584,1585],{"class":200},"def",[194,1587,1589],{"class":1588},"svObZ"," audit_form",[194,1591,1592],{"class":204},"(layer, expected):\n",[194,1594,1595,1598,1600],{"class":196,"line":313},[194,1596,1597],{"class":204},"    problems ",[194,1599,227],{"class":200},[194,1601,1602],{"class":204}," []\n",[194,1604,1605,1608,1610],{"class":196,"line":362},[194,1606,1607],{"class":204},"    names ",[194,1609,227],{"class":200},[194,1611,1612],{"class":204}," layer.fields().names()\n",[194,1614,1615,1618,1621,1623],{"class":196,"line":401},[194,1616,1617],{"class":200},"    for",[194,1619,1620],{"class":204}," name, spec ",[194,1622,271],{"class":200},[194,1624,1625],{"class":204}," expected.items():\n",[194,1627,1628,1631,1634,1637,1640],{"class":196,"line":406},[194,1629,1630],{"class":200},"        if",[194,1632,1633],{"class":204}," name ",[194,1635,1636],{"class":200},"not",[194,1638,1639],{"class":200}," in",[194,1641,1642],{"class":204}," names:\n",[194,1644,1645,1648,1650,1652,1654,1657,1659,1662],{"class":196,"line":430},[194,1646,1647],{"class":204},"            problems.append(",[194,1649,322],{"class":200},[194,1651,325],{"class":233},[194,1653,328],{"class":240},[194,1655,1656],{"class":204},"name",[194,1658,337],{"class":240},[194,1660,1661],{"class":233},": field missing\"",[194,1663,398],{"class":204},[194,1665,1667],{"class":196,"line":1666},15,[194,1668,1669],{"class":200},"            continue\n",[194,1671,1673,1676,1678],{"class":196,"line":1672},16,[194,1674,1675],{"class":204},"        idx ",[194,1677,227],{"class":200},[194,1679,1680],{"class":204}," names.index(name)\n",[194,1682,1684,1687,1689],{"class":196,"line":1683},17,[194,1685,1686],{"class":204},"        field ",[194,1688,227],{"class":200},[194,1690,1691],{"class":204}," layer.fields().at(idx)\n",[194,1693,1695,1698,1700],{"class":196,"line":1694},18,[194,1696,1697],{"class":204},"        widget ",[194,1699,227],{"class":200},[194,1701,1702],{"class":204}," layer.editorWidgetSetup(idx).type()\n",[194,1704,1706,1708,1711,1714,1717,1719],{"class":196,"line":1705},19,[194,1707,1630],{"class":200},[194,1709,1710],{"class":204}," widget ",[194,1712,1713],{"class":200},"!=",[194,1715,1716],{"class":204}," spec[",[194,1718,1506],{"class":233},[194,1720,1721],{"class":204},"]:\n",[194,1723,1725,1727,1729,1731,1733,1735,1737,1740,1742,1745,1747,1750,1752,1755,1757,1760,1763,1766,1768,1770],{"class":196,"line":1724},20,[194,1726,1647],{"class":204},[194,1728,322],{"class":200},[194,1730,325],{"class":233},[194,1732,328],{"class":240},[194,1734,1656],{"class":204},[194,1736,337],{"class":240},[194,1738,1739],{"class":233},": widget ",[194,1741,328],{"class":240},[194,1743,1744],{"class":204},"widget ",[194,1746,348],{"class":200},[194,1748,1749],{"class":233}," 'default'",[194,1751,337],{"class":240},[194,1753,1754],{"class":233}," != ",[194,1756,328],{"class":240},[194,1758,1759],{"class":204},"spec[",[194,1761,1762],{"class":233},"'widget'",[194,1764,1765],{"class":204},"]",[194,1767,337],{"class":240},[194,1769,325],{"class":233},[194,1771,398],{"class":204},[194,1773,1775,1777,1780,1782,1785,1788,1791,1793,1795,1797],{"class":196,"line":1774},21,[194,1776,1630],{"class":200},[194,1778,1779],{"class":233}," \"default\"",[194,1781,1639],{"class":200},[194,1783,1784],{"class":204}," spec ",[194,1786,1787],{"class":200},"and",[194,1789,1790],{"class":204}," layer.defaultValueDefinition(idx).expression() ",[194,1792,1713],{"class":200},[194,1794,1716],{"class":204},[194,1796,1565],{"class":233},[194,1798,1721],{"class":204},[194,1800,1802,1804,1806,1808,1810,1812,1814,1817],{"class":196,"line":1801},22,[194,1803,1647],{"class":204},[194,1805,322],{"class":200},[194,1807,325],{"class":233},[194,1809,328],{"class":240},[194,1811,1656],{"class":204},[194,1813,337],{"class":240},[194,1815,1816],{"class":233},": default changed\"",[194,1818,398],{"class":204},[194,1820,1822,1825,1827],{"class":196,"line":1821},23,[194,1823,1824],{"class":204},"        c ",[194,1826,227],{"class":200},[194,1828,1829],{"class":204}," field.constraints()\n",[194,1831,1833,1835,1838,1840,1843,1845,1847,1850,1853],{"class":196,"line":1832},24,[194,1834,1630],{"class":200},[194,1836,1837],{"class":204}," spec.get(",[194,1839,1540],{"class":233},[194,1841,1842],{"class":204},") ",[194,1844,1787],{"class":200},[194,1846,1434],{"class":200},[194,1848,1849],{"class":204}," (c.constraints() ",[194,1851,1852],{"class":200},"&",[194,1854,1855],{"class":204}," QgsFieldConstraints.Constraint.ConstraintNotNull):\n",[194,1857,1859,1861,1863,1865,1867,1869,1871,1874],{"class":196,"line":1858},25,[194,1860,1647],{"class":204},[194,1862,322],{"class":200},[194,1864,325],{"class":233},[194,1866,328],{"class":240},[194,1868,1656],{"class":204},[194,1870,337],{"class":240},[194,1872,1873],{"class":233},": not-null constraint missing\"",[194,1875,398],{"class":204},[194,1877,1879,1881,1883,1885,1887,1889,1891],{"class":196,"line":1878},26,[194,1880,1630],{"class":200},[194,1882,1837],{"class":204},[194,1884,1515],{"class":233},[194,1886,1842],{"class":204},[194,1888,1787],{"class":200},[194,1890,1434],{"class":200},[194,1892,1893],{"class":204}," c.constraintExpression():\n",[194,1895,1897,1899,1901,1903,1905,1907,1909,1912],{"class":196,"line":1896},27,[194,1898,1647],{"class":204},[194,1900,322],{"class":200},[194,1902,325],{"class":233},[194,1904,328],{"class":240},[194,1906,1656],{"class":204},[194,1908,337],{"class":240},[194,1910,1911],{"class":233},": expression constraint missing\"",[194,1913,398],{"class":204},[194,1915,1917,1920,1922,1925,1928,1931,1934,1936,1939,1941],{"class":196,"line":1916},28,[194,1918,1919],{"class":204},"    unplaced ",[194,1921,227],{"class":200},[194,1923,1924],{"class":240}," set",[194,1926,1927],{"class":204},"(names) ",[194,1929,1930],{"class":200},"-",[194,1932,1933],{"class":204}," {e.name() ",[194,1935,265],{"class":200},[194,1937,1938],{"class":204}," tab ",[194,1940,271],{"class":200},[194,1942,1943],{"class":204}," layer.editFormConfig().invisibleRootContainer().children()\n",[194,1945,1947,1950,1953,1955],{"class":196,"line":1946},29,[194,1948,1949],{"class":200},"                             for",[194,1951,1952],{"class":204}," e ",[194,1954,271],{"class":200},[194,1956,1957],{"class":204}," tab.children()}\n",[194,1959,1961,1964,1967,1970],{"class":196,"line":1960},30,[194,1962,1963],{"class":200},"    return",[194,1965,1966],{"class":204}," problems, ",[194,1968,1969],{"class":240},"sorted",[194,1971,1972],{"class":204},"(unplaced)\n",[194,1974,1976],{"class":196,"line":1975},31,[194,1977,218],{"emptyLinePlaceholder":217},[194,1979,1981,1984,1986,1989,1991],{"class":196,"line":1980},32,[194,1982,1983],{"class":204},"problems, unplaced ",[194,1985,227],{"class":200},[194,1987,1988],{"class":204}," audit_form(layer, ",[194,1990,1489],{"class":240},[194,1992,398],{"class":204},[194,1994,1996,1998,2001,2003,2006],{"class":196,"line":1995},33,[194,1997,409],{"class":240},[194,1999,2000],{"class":204},"(problems ",[194,2002,348],{"class":200},[194,2004,2005],{"class":233}," \"matches specification\"",[194,2007,398],{"class":204},[194,2009,2011,2013,2015,2018],{"class":196,"line":2010},34,[194,2012,409],{"class":240},[194,2014,319],{"class":204},[194,2016,2017],{"class":233},"\"fields not on any tab:\"",[194,2019,2020],{"class":204},", unplaced)\n",[14,2022,2023,2025,2026,2029],{},[444,2024,446],{}," The expected dictionary is deliberately small — only the rules that matter — so the audit does not fail on harmless cosmetic changes such as a reworded alias. Checking for fields that appear on no tab catches the most common drift of all: a column added to the table months after the form was designed, which drag-and-drop layouts silently leave out. The unplaced check here looks one level deep, which suits forms built from the tab-and-field specification in this guide; forms with nested groups need the recursive walk shown in ",[21,2027,2028],{"href":1048},"building a tabbed form layout",". Run the audit in the plugin's test suite against a template GeoPackage, and again as a scheduled job against production layers.",[157,2031,2033],{"id":2032},"offline-and-multi-user-editing","Offline and multi-user editing",[14,2035,2036],{},"Forms are usually configured for one person at a desk and then used by a team in the field. Several choices in this guide behave differently once that happens, and it is cheaper to decide them up front than to discover them after a week of surveys have been merged.",[14,2038,2039,2040,2043,2044,2047,2048,2051],{},"Identifiers are the first. A default of ",[165,2041,2042],{},"maximum(\"id\") + 1"," gives two offline editors the same number; ",[165,2045,2046],{},"uuid()"," never does. Timestamps are the second: ",[165,2049,2050],{},"now()"," is evaluated on the device, so a phone with the wrong time zone stamps every record hours off — store UTC where the data will be compared across devices. Unique constraints are the third: they are checked against the copy of the layer on the device, so two offline editors can each create a \"unique\" value that collides on sync, and only a unique index in the central database catches it. And value relations need their lookup layer packaged alongside the data for offline use, or the drop-down arrives empty in the field. None of these are exotic; they are the ordinary consequences of the form running on many machines rather than one, and every one of them is decided by configuration you can set and check from Python.",[157,2053,2055],{"id":2054},"key-takeaways","Key takeaways",[2057,2058,2059,2063,2066,2072,2075,2078,2081,2084],"ul",{},[2060,2061,2062],"li",{},"Treat widgets, defaults and constraints as part of the data model, and build them with scripts rather than by hand.",[2060,2064,2065],{},"Give every hand-edited field a widget that only accepts valid input; use value relations for lists that change.",[2060,2067,2068,2069,2071],{},"Use defaults for identifiers and audit fields, ",[165,2070,923],{}," for anything that must follow edits.",[2060,2073,2074],{},"Put unbreakable rules in the database; use layer constraints for conditional and soft rules.",[2060,2076,2077],{},"Lay out long forms with tabs, columns and visibility expressions, built from a specification.",[2060,2079,2080],{},"Write Python form code only when declarative options run out, and ship it in a plugin.",[2060,2082,2083],{},"In Python actions, substitute ids, never attribute text.",[2060,2085,2086,2087,792,2089,796,2091,2093],{},"Save ",[165,2088,1331],{},[165,2090,1335],{},[165,2092,1339],{}," style categories to the data source so the configuration travels with the layer.",[157,2095,2097],{"id":2096},"frequently-asked-questions","Frequently Asked Questions",[14,2099,2100,2103],{},[444,2101,2102],{},"Do these configurations work in QField and other mobile apps?","\nWidgets, defaults, constraints, layouts and visibility expressions generally do, because they are read from the project or style. Python init code and Python actions do not.",[14,2105,2106,2109,2110,2113,2114,2117],{},[444,2107,2108],{},"Are features created by scripts checked against constraints?","\nNot automatically. Create them with ",[165,2111,2112],{},"QgsVectorLayerUtils.createFeature"," and check with ",[165,2115,2116],{},"validateAttribute"," to apply the same defaults and rules.",[14,2119,2120,2123],{},[444,2121,2122],{},"Can one form configuration be shared by layers with different field names?","\nNot directly — configuration is keyed by field name and index. Map names in your specification, or align the schemas first.",[14,2125,2126,2129],{},[444,2127,2128],{},"Where is the configuration stored if I do nothing special?","\nIn the project file. Saving a style to the data source or a QML file is what makes it portable.",[14,2131,2132,2135,2136,792,2139,2142,2143,452],{},[444,2133,2134],{},"Can I test form configuration in CI?","\nYes. Load the layer headless, apply the configuration, and assert on ",[165,2137,2138],{},"editorWidgetSetup",[165,2140,2141],{},"defaultValueDefinition"," and constraints, as in ",[21,2144,2146],{"href":2145},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Funit-test-qgis-plugin-with-pytest\u002F","testing a plugin with pytest",[157,2148,2150],{"id":2149},"related","Related",[2057,2152,2153,2159,2164,2169,2174,2179,2184,2190],{},[2060,2154,2155,2158],{},[21,2156,2157],{"href":23},"QGIS Plugin Development Guide"," — the section this guide belongs to",[2060,2160,2161],{},[21,2162,2163],{"href":549},"Configure Editor Widgets in PyQGIS",[2060,2165,2166],{},[21,2167,2168],{"href":450},"Set Default Values and Field Constraints in PyQGIS",[2060,2170,2171],{},[21,2172,2173],{"href":1048},"Build a Tabbed Attribute Form Layout in PyQGIS",[2060,2175,2176],{},[21,2177,2178],{"href":1161},"Add Python Logic to an Attribute Form in PyQGIS",[2060,2180,2181],{},[21,2182,2183],{"href":1314},"Create Layer Actions in PyQGIS",[2060,2185,2186],{},[21,2187,2189],{"href":2188},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002F","Qt Designer for QGIS Plugin Interfaces",[2060,2191,2192],{},[21,2193,2195],{"href":2194},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-projects\u002Fdefine-layer-relations-pyqgis\u002F","Define Layer Relations in PyQGIS",[2197,2198,2199],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}",{"title":190,"searchDepth":214,"depth":214,"links":2201},[2202,2203,2204,2205,2206,2207,2208,2209,2210,2211,2212,2213],{"id":159,"depth":214,"text":160},{"id":455,"depth":214,"text":456},{"id":679,"depth":214,"text":680},{"id":931,"depth":214,"text":932},{"id":1053,"depth":214,"text":1054},{"id":1225,"depth":214,"text":1226},{"id":1324,"depth":214,"text":1325},{"id":1460,"depth":214,"text":1461},{"id":2032,"depth":214,"text":2033},{"id":2054,"depth":214,"text":2055},{"id":2096,"depth":214,"text":2097},{"id":2149,"depth":214,"text":2150},"Configure how people enter and use data in QGIS from Python — editor widgets, default values and constraints, tabbed form layouts, form init code and layer actions — and package the whole configuration so it travels with the layer.","md",{"slug":2217,"type":2218,"breadcrumb":2219,"datePublished":2220,"dateModified":2220},"attribute-forms-and-layer-actions","guide","Attribute Forms & Layer Actions","2026-09-17","\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions",{"title":5,"description":2214},"qgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Findex","woN2dq-UKZDaMFSNhUDr6hq-8ZJljiWh6b9TJ1vNsP4",1789632907411]