Skip to content

Commit 39480a9

Browse files
authored
Merge pull request #878 from MerginMaps/fix-photo-widget-validation
Fix photo widget validation
2 parents ce40f1d + 3170978 commit 39480a9

2 files changed

Lines changed: 80 additions & 22 deletions

File tree

Mergin/test/test_validations.py

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def test_attachment_widget(self):
6767
"FileWidgetFilter": "",
6868
"PropertyCollection": {
6969
"name": None,
70-
"properties": {},
70+
"properties": None,
7171
"type": "collection",
7272
},
7373
"RelativeStorage": QgsFileWidget.RelativeStorage.Absolute,
@@ -141,6 +141,45 @@ def test_attachment_widget(self):
141141
validator.check_attachment_widget()
142142
self.assertTrue(len(validator.issues) == 0)
143143

144+
# right setup, field-based override (Type 2)
145+
config["PropertyCollection"]["properties"]["propertyRootPath"] = {
146+
"active": True,
147+
"field": "photo",
148+
"expression": "",
149+
"type": 2,
150+
}
151+
widget_setup = QgsEditorWidgetSetup("ExternalResource", config)
152+
self.mem_layer.setEditorWidgetSetup(photo_field_idx, widget_setup)
153+
validator.check_attachment_widget()
154+
self.assertTrue(len(validator.issues) == 0)
155+
156+
# right setup, expression-based override with quoted field name (Type 3)
157+
config["PropertyCollection"]["properties"]["propertyRootPath"] = {
158+
"active": True,
159+
"expression": '"photo"',
160+
"type": 3,
161+
}
162+
widget_setup = QgsEditorWidgetSetup("ExternalResource", config)
163+
self.mem_layer.setEditorWidgetSetup(photo_field_idx, widget_setup)
164+
validator.check_attachment_widget()
165+
self.assertTrue(len(validator.issues) == 0)
166+
167+
# absolute path bypass when storageUrl is active (e.g. Google Drive URLs)
168+
config["RelativeStorage"] = QgsFileWidget.RelativeStorage.Absolute
169+
config["PropertyCollection"]["properties"] = {
170+
"storageUrl": {
171+
"active": True,
172+
"field": "photo",
173+
"type": 2,
174+
}
175+
}
176+
# clear DefaultRoot doesn't trigger the warning
177+
config.pop("DefaultRoot", None)
178+
widget_setup = QgsEditorWidgetSetup("ExternalResource", config)
179+
self.mem_layer.setEditorWidgetSetup(photo_field_idx, widget_setup)
180+
validator.check_attachment_widget()
181+
self.assertTrue(len(validator.issues) == 0)
182+
144183
def test_embedded_svg(self):
145184
symbol = QgsMarkerSymbol()
146185
symbol_layer = QgsSvgMarkerSymbolLayer(os.path.join(test_data_path, "transport_aerodrome.svg"))

Mergin/validation.py

Lines changed: 40 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -242,33 +242,52 @@ def check_attachment_widget(self):
242242
for lid, layer in self.layers.items():
243243
if lid not in self.editable:
244244
continue
245-
fields = layer.fields()
246-
for i in range(fields.count()):
247-
ws = layer.editorWidgetSetup(i)
245+
for field in layer.fields():
246+
ws = field.editorWidgetSetup()
247+
248248
if ws and ws.type() == "ExternalResource":
249249
cfg = ws.config()
250-
field_name = fields[i].name()
251-
# check for relative paths
252-
if "RelativeStorage" in cfg and cfg["RelativeStorage"] == QgsFileWidget.Absolute:
253-
self.issues.append(SingleLayerWarning(lid, Warning.ATTACHMENT_ABSOLUTE_PATH, field_name))
254-
# check that correct expression is set
255-
try:
256-
is_expression_enabled = cfg["PropertyCollection"]["properties"]["propertyRootPath"]["active"]
257-
except (KeyError, TypeError):
258-
is_expression_enabled = False
259-
if is_expression_enabled:
260-
expression = cfg["PropertyCollection"]["properties"]["propertyRootPath"]["expression"]
261-
if not PROJECT_VARS.search(expression):
250+
field_name = field.name()
251+
prop_collection = cfg.get("PropertyCollection") or {}
252+
properties = prop_collection.get("properties") or {}
253+
root_path_prop = properties.get("propertyRootPath") or {}
254+
storage_url_prop = properties.get("storageUrl") or {}
255+
256+
is_root_active = root_path_prop.get("active", False)
257+
is_storage_active = storage_url_prop.get("active", False)
258+
259+
# Check for absolute paths
260+
if not (is_root_active or is_storage_active):
261+
if (
262+
cfg.get("RelativeStorage") == QgsFileWidget.Absolute
263+
): # Absolute= 0, RelativeProject= 1, RelativeDefaultPath= 2
264+
self.issues.append(SingleLayerWarning(lid, Warning.ATTACHMENT_ABSOLUTE_PATH, field_name))
265+
266+
# Check Data Defined Overrides
267+
if is_root_active:
268+
prop_type = root_path_prop.get(
269+
"type"
270+
) # types are more reliable than keys which can be empty - QGIS decides based on type value
271+
if prop_type == 3: # ExpressionBasedProperty
272+
expression = root_path_prop.get("expression", "").strip()
273+
is_field_ref = expression.startswith('"') and expression.endswith('"')
274+
275+
if not (PROJECT_VARS.search(expression) or is_field_ref):
276+
self.issues.append(
277+
SingleLayerWarning(lid, Warning.ATTACHMENT_WRONG_EXPRESSION, field_name)
278+
)
279+
elif prop_type == 2: # FieldBasedProperty
280+
pass
281+
else: # 1 = StaticProperty or 0 = InvalidProperty
262282
self.issues.append(SingleLayerWarning(lid, Warning.ATTACHMENT_WRONG_EXPRESSION, field_name))
263-
# if expression-based path is not set with the data-defined override the app cannot resolve the path to save the photo
264-
else:
283+
284+
# Fallback if expression-based path is not set
285+
elif not is_storage_active:
265286
if "DefaultRoot" in cfg:
266-
# default root should not be set to the local path
267-
if os.path.isabs(cfg["DefaultRoot"]):
287+
if os.path.isabs(cfg.get("DefaultRoot", "")):
268288
self.issues.append(SingleLayerWarning(lid, Warning.ATTACHMENT_LOCAL_PATH, field_name))
269289

270-
# expression must be set with the data-defined override
271-
expr = QgsExpression(cfg["DefaultRoot"])
290+
expr = QgsExpression(cfg.get("DefaultRoot", ""))
272291
if expr.isValid():
273292
self.issues.append(
274293
SingleLayerWarning(

0 commit comments

Comments
 (0)