Coverage for heritrace / utils / shacl_display.py: 97%

292 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-21 12:56 +0000

1# SPDX-FileCopyrightText: 2025 Arcangelo Massari <arcangelo.massari@unibo.it> 

2# 

3# SPDX-License-Identifier: ISC 

4 

5import json 

6import os 

7from collections import OrderedDict, defaultdict 

8from typing import List 

9 

10from flask import Flask 

11from heritrace.utils.filters import Filter 

12from rdflib import Graph, URIRef 

13from rdflib.plugins.sparql import prepareQuery 

14 

15COMMON_SPARQL_QUERY = prepareQuery( 

16 """ 

17 SELECT ?shape ?type ?predicate ?nodeShape ?datatype ?maxCount ?minCount ?hasValue ?objectClass  

18 ?conditionPath ?conditionValue ?pattern ?message 

19 (GROUP_CONCAT(?optionalValue; separator=",") AS ?optionalValues) 

20 (GROUP_CONCAT(?orNode; separator=",") AS ?orNodes) 

21 WHERE { 

22 ?shape sh:targetClass ?type ; 

23 sh:property ?property . 

24 ?property sh:path ?predicate . 

25 OPTIONAL { 

26 ?property sh:node ?nodeShape . 

27 OPTIONAL {?nodeShape sh:targetClass ?objectClass .} 

28 } 

29 OPTIONAL { 

30 ?property sh:or ?orList . 

31 { 

32 ?orList rdf:rest*/rdf:first ?orConstraint . 

33 ?orConstraint sh:datatype ?datatype . 

34 } UNION { 

35 ?orList rdf:rest*/rdf:first ?orNodeShape . 

36 ?orNodeShape sh:node ?orNode . 

37 } UNION { 

38 ?orList rdf:rest*/rdf:first ?orConstraint . 

39 ?orConstraint sh:hasValue ?optionalValue . 

40 } 

41 } 

42 OPTIONAL { ?property sh:datatype ?datatype . } 

43 OPTIONAL { ?property sh:maxCount ?maxCount . } 

44 OPTIONAL { ?property sh:minCount ?minCount . } 

45 OPTIONAL { ?property sh:hasValue ?hasValue . } 

46 OPTIONAL { 

47 ?property sh:in ?list . 

48 ?list rdf:rest*/rdf:first ?optionalValue . 

49 } 

50 OPTIONAL { 

51 ?property sh:condition ?conditionNode . 

52 ?conditionNode sh:path ?conditionPath ; 

53 sh:hasValue ?conditionValue . 

54 } 

55 OPTIONAL { ?property sh:pattern ?pattern . } 

56 OPTIONAL { ?property sh:message ?message . } 

57 FILTER (isURI(?predicate)) 

58 } 

59 GROUP BY ?shape ?type ?predicate ?nodeShape ?datatype ?maxCount ?minCount ?hasValue  

60 ?objectClass ?conditionPath ?conditionValue ?pattern ?message 

61""", 

62 initNs={ 

63 "sh": "http://www.w3.org/ns/shacl#", 

64 "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 

65 }, 

66) 

67 

68 

69def process_query_results(shacl, results, display_rules, processed_shapes, app: Flask, depth=0): 

70 form_fields = defaultdict(dict) 

71 

72 with open(os.path.join(os.path.dirname(__file__), "context.json"), "r") as config_file: 

73 context = json.load(config_file)["@context"] 

74 

75 custom_filter = Filter(context, display_rules, app.config['DATASET_DB_URL']) 

76 

77 for row in results: 

78 subject_shape = str(row.shape) 

79 entity_type = str(row.type) 

80 predicate = str(row.predicate) 

81 nodeShape = str(row.nodeShape) if row.nodeShape else None 

82 hasValue = str(row.hasValue) if row.hasValue else None 

83 objectClass = str(row.objectClass) if row.objectClass else None 

84 minCount = 0 if row.minCount is None else int(row.minCount) 

85 maxCount = None if row.maxCount is None else int(row.maxCount) 

86 datatype = str(row.datatype) if row.datatype else None 

87 optionalValues = [v for v in (row.optionalValues or "").split(",") if v] 

88 orNodes = [v for v in (row.orNodes or "").split(",") if v] 

89 

90 entity_key = (entity_type, subject_shape) 

91 

92 condition_entry = {} 

93 if row.conditionPath and row.conditionValue: 

94 condition_entry["condition"] = { 

95 "path": str(row.conditionPath), 

96 "value": str(row.conditionValue), 

97 } 

98 if row.pattern: 

99 condition_entry["pattern"] = str(row.pattern) 

100 if row.message: 

101 condition_entry["message"] = str(row.message) 

102 

103 if predicate not in form_fields[entity_key]: 

104 form_fields[entity_key][predicate] = [] 

105 

106 nodeShapes = [] 

107 if nodeShape: 

108 nodeShapes.append(nodeShape) 

109 nodeShapes.extend(orNodes) 

110 

111 existing_field = None 

112 for field in form_fields[entity_key][predicate]: 

113 if ( 

114 field.get("nodeShape") == nodeShape 

115 and field.get("nodeShapes") == nodeShapes 

116 and field.get("subjectShape") == subject_shape 

117 and field.get("hasValue") == hasValue 

118 and field.get("objectClass") == objectClass 

119 and field.get("min") == minCount 

120 and field.get("max") == maxCount 

121 and field.get("optionalValues") == optionalValues 

122 ): 

123 existing_field = field 

124 break 

125 

126 if existing_field: 

127 # Aggiorniamo il campo esistente con nuovi datatype o condizioni 

128 if datatype and str(datatype) not in existing_field.get("datatypes", []): 

129 existing_field.setdefault("datatypes", []).append(str(datatype)) 

130 if condition_entry: 

131 existing_field.setdefault("conditions", []).append(condition_entry) 

132 else: 

133 field_info = { 

134 "entityType": entity_type, 

135 "uri": predicate, 

136 "nodeShape": nodeShape, 

137 "nodeShapes": nodeShapes, 

138 "subjectShape": subject_shape, 

139 "entityKey": entity_key, 

140 "datatypes": [datatype] if datatype else [], 

141 "min": minCount, 

142 "max": maxCount, 

143 "hasValue": hasValue, 

144 "objectClass": objectClass, 

145 "optionalValues": optionalValues, 

146 "conditions": [condition_entry] if condition_entry else [], 

147 "inputType": determine_input_type(datatype), 

148 "shouldBeDisplayed": True, # Default value 

149 } 

150 

151 if nodeShape and nodeShape not in processed_shapes: 

152 field_info["nestedShape"] = process_nested_shapes( 

153 shacl, 

154 display_rules, 

155 nodeShape, 

156 app, 

157 depth=depth + 1, 

158 processed_shapes=processed_shapes, 

159 ) 

160 

161 if orNodes: 

162 field_info["or"] = [] 

163 for node in orNodes: 

164 # Process orNode as a field_info 

165 entity_type_or_node = get_shape_target_class(shacl, node) 

166 object_class = get_object_class(shacl, node, predicate) 

167 shape_display_name = custom_filter.human_readable_class( 

168 (entity_type_or_node, node) 

169 ) 

170 or_field_info = { 

171 "entityType": entity_type_or_node, 

172 "uri": predicate, 

173 "displayName": shape_display_name, 

174 "subjectShape": subject_shape, 

175 "nodeShape": node, 

176 "min": minCount, 

177 "max": maxCount, 

178 "hasValue": hasValue, 

179 "objectClass": objectClass, 

180 "optionalValues": optionalValues, 

181 "conditions": [condition_entry] if condition_entry else [], 

182 "shouldBeDisplayed": True, # Default value 

183 } 

184 if node not in processed_shapes: 

185 or_field_info["nestedShape"] = process_nested_shapes( 

186 shacl, 

187 display_rules, 

188 node, 

189 app, 

190 depth=depth + 1, 

191 processed_shapes=processed_shapes, 

192 ) 

193 field_info["or"].append(or_field_info) 

194 

195 form_fields[entity_key][predicate].append(field_info) 

196 

197 return form_fields 

198 

199 

200def process_nested_shapes( 

201 shacl: Graph, display_rules: List[dict], shape_uri: str, app: Flask, depth=0, processed_shapes=None 

202): 

203 """ 

204 Processa ricorsivamente le shape annidate. 

205 

206 Argomenti: 

207 shape_uri (str): L'URI della shape da processare. 

208 depth (int): La profondità corrente della ricorsione. 

209 processed_shapes (set): Un insieme delle shape già processate. 

210 

211 Restituisce: 

212 list: Una lista di dizionari dei campi annidati. 

213 """ 

214 if processed_shapes is None: 

215 processed_shapes = set() 

216 

217 if shape_uri in processed_shapes: 

218 return [] 

219 

220 processed_shapes.add(shape_uri) 

221 init_bindings = {"shape": URIRef(shape_uri)} 

222 nested_results = execute_shacl_query(shacl, COMMON_SPARQL_QUERY, init_bindings) 

223 nested_fields = [] 

224 

225 temp_form_fields = process_query_results( 

226 shacl, nested_results, display_rules, processed_shapes, app=app, depth=depth 

227 ) 

228 

229 # Applica le regole di visualizzazione ai campi annidati 

230 if display_rules: 

231 temp_form_fields = apply_display_rules(shacl, temp_form_fields, display_rules) 

232 temp_form_fields = order_form_fields(temp_form_fields, display_rules) 

233 

234 # Estrai i campi per il tipo di entità 

235 for entity_type in temp_form_fields: 

236 for predicate in temp_form_fields[entity_type]: 

237 nested_fields.extend(temp_form_fields[entity_type][predicate]) 

238 

239 processed_shapes.remove(shape_uri) 

240 return nested_fields 

241 

242 

243def get_property_order(entity_type, display_rules): 

244 """ 

245 Recupera l'ordine delle proprietà per un tipo di entità dalle regole di visualizzazione. 

246 

247 Argomenti: 

248 entity_type (str): L'URI del tipo di entità. 

249 

250 Restituisce: 

251 list: Una lista di URI di proprietà nell'ordine desiderato. 

252 """ 

253 if not display_rules: 

254 return [] 

255 

256 for rule in display_rules: 

257 if rule.get("class") == entity_type and "propertyOrder" in rule: 

258 return rule["propertyOrder"] 

259 elif rule.get("class") == entity_type: 

260 return [ 

261 prop.get("property") or prop.get("virtual_property") 

262 for prop in rule.get("displayProperties", []) 

263 if prop.get("property") or prop.get("virtual_property") 

264 ] 

265 return [] 

266 

267 

268def order_fields(fields, property_order): 

269 """ 

270 Ordina i campi secondo l'ordine specificato delle proprietà. 

271 

272 Argomenti: 

273 fields (list): Una lista di dizionari dei campi da ordinare. 

274 property_order (list): Una lista di URI di proprietà nell'ordine desiderato. 

275 

276 Restituisce: 

277 list: Una lista ordinata di dizionari dei campi. 

278 """ 

279 if not fields: 

280 return [] 

281 if not property_order: 

282 return fields 

283 

284 # Create a dictionary to map predicates to their position in property_order 

285 order_dict = {pred: i for i, pred in enumerate(property_order)} 

286 

287 # Sort fields based on their position in property_order 

288 # Fields not in property_order will be placed at the end 

289 return sorted( 

290 fields, 

291 key=lambda f: order_dict.get(f.get("predicate", f.get("uri", "")), float("inf")), 

292 ) 

293 

294 

295def order_form_fields(form_fields, display_rules): 

296 """ 

297 Ordina i campi del form secondo le regole di visualizzazione. 

298 

299 Argomenti: 

300 form_fields (dict): I campi del form con possibili modifiche dalle regole di visualizzazione. 

301 

302 Restituisce: 

303 OrderedDict: I campi del form ordinati. 

304 """ 

305 ordered_form_fields = OrderedDict() 

306 if display_rules: 

307 for rule in display_rules: 

308 target = rule.get("target", {}) 

309 entity_class = target.get("class") 

310 entity_shape = target.get("shape") 

311 

312 # Case 1: Both class and shape are specified (exact match) 

313 if entity_class and entity_shape: 

314 entity_key = (entity_class, entity_shape) 

315 if entity_key in form_fields: 

316 ordered_properties = [ 

317 prop_rule.get("property") or prop_rule.get("virtual_property") 

318 for prop_rule in rule.get("displayProperties", []) 

319 if prop_rule.get("property") or prop_rule.get("virtual_property") 

320 ] 

321 ordered_form_fields[entity_key] = OrderedDict() 

322 for prop in ordered_properties: 

323 if prop in form_fields[entity_key]: 

324 ordered_form_fields[entity_key][prop] = form_fields[entity_key][prop] 

325 # Aggiungi le proprietà rimanenti non specificate nell'ordine 

326 for prop in form_fields[entity_key]: 

327 if prop not in ordered_properties: 

328 ordered_form_fields[entity_key][prop] = form_fields[entity_key][prop] 

329 

330 # Case 2: Only class is specified (apply to all matching classes) 

331 elif entity_class: 

332 for key in form_fields: 

333 if key[0] == entity_class: # Check if class part of tuple matches 

334 entity_key = key 

335 ordered_properties = [ 

336 prop_rule.get("property") or prop_rule.get("virtual_property") 

337 for prop_rule in rule.get("displayProperties", []) 

338 if prop_rule.get("property") or prop_rule.get("virtual_property") 

339 ] 

340 ordered_form_fields[entity_key] = OrderedDict() 

341 for prop in ordered_properties: 

342 if prop in form_fields[entity_key]: 

343 ordered_form_fields[entity_key][prop] = form_fields[entity_key][prop] 

344 # Aggiungi le proprietà rimanenti non specificate nell'ordine 

345 for prop in form_fields[entity_key]: 

346 if prop not in ordered_properties: 

347 ordered_form_fields[entity_key][prop] = form_fields[entity_key][prop] 

348 

349 # Case 3: Only shape is specified (apply to all matching shapes) 

350 elif entity_shape: 

351 for key in form_fields: 

352 if key[1] == entity_shape: # Check if shape part of tuple matches 

353 entity_key = key 

354 ordered_properties = [ 

355 prop_rule.get("property") or prop_rule.get("virtual_property") 

356 for prop_rule in rule.get("displayProperties", []) 

357 if prop_rule.get("property") or prop_rule.get("virtual_property") 

358 ] 

359 ordered_form_fields[entity_key] = OrderedDict() 

360 for prop in ordered_properties: 

361 if prop in form_fields[entity_key]: 

362 ordered_form_fields[entity_key][prop] = form_fields[entity_key][prop] 

363 # Aggiungi le proprietà rimanenti non specificate nell'ordine 

364 for prop in form_fields[entity_key]: 

365 if prop not in ordered_properties: 

366 ordered_form_fields[entity_key][prop] = form_fields[entity_key][prop] 

367 else: 

368 ordered_form_fields = form_fields 

369 return ordered_form_fields 

370 

371 

372def apply_display_rules(shacl, form_fields, display_rules): 

373 """ 

374 Applica le regole di visualizzazione ai campi del form. 

375 

376 Argomenti: 

377 form_fields (dict): I campi del form iniziali estratti dalle shape SHACL. 

378 

379 Restituisce: 

380 dict: I campi del form dopo aver applicato le regole di visualizzazione. 

381 """ 

382 for rule in display_rules: 

383 target = rule.get("target", {}) 

384 entity_class = target.get("class") 

385 entity_shape = target.get("shape") 

386 

387 # Handle different cases based on available target information 

388 # Case 1: Both class and shape are specified (exact match) 

389 if entity_class and entity_shape: 

390 entity_key = (entity_class, entity_shape) 

391 if entity_key in form_fields: 

392 apply_rule_to_entity(shacl, form_fields, entity_key, rule) 

393 # Case 2: Only class is specified (apply to all matching classes) 

394 elif entity_class: 

395 for key in list(form_fields.keys()): 

396 if key[0] == entity_class: # Check if class part of tuple matches 

397 apply_rule_to_entity(shacl, form_fields, key, rule) 

398 # Case 3: Only shape is specified (apply to all matching shapes) 

399 elif entity_shape: 

400 for key in list(form_fields.keys()): 

401 if key[1] == entity_shape: # Check if shape part of tuple matches 

402 apply_rule_to_entity(shacl, form_fields, key, rule) 

403 return form_fields 

404 

405 

406def apply_rule_to_entity(shacl, form_fields, entity_key, rule): 

407 """ 

408 Apply a display rule to a specific entity key. 

409  

410 Args: 

411 shacl: The SHACL graph 

412 form_fields: The form fields dictionary 

413 entity_key: The entity key tuple (class, shape) 

414 rule: The display rule to apply 

415 """ 

416 for prop in rule.get("displayProperties", []): 

417 prop_uri = prop.get("property") or prop.get("virtual_property") 

418 if prop_uri and prop_uri in form_fields[entity_key]: 

419 for field_info in form_fields[entity_key][prop_uri]: 

420 add_display_information(field_info, prop) 

421 # Chiamata ricorsiva per le nestedShape 

422 if "nestedShape" in field_info: 

423 apply_display_rules_to_nested_shapes( 

424 field_info["nestedShape"], prop, rule.get("target", {}).get("shape") 

425 ) 

426 if "or" in field_info: 

427 for or_field in field_info["or"]: 

428 apply_display_rules_to_nested_shapes( 

429 [or_field], field_info, rule.get("target", {}).get("shape") 

430 ) 

431 if "intermediateRelation" in prop: 

432 handle_intermediate_relation(shacl, field_info, prop) 

433 if "displayRules" in prop: 

434 handle_sub_display_rules( 

435 shacl, 

436 form_fields, 

437 entity_key, 

438 form_fields[entity_key][prop_uri], 

439 prop, 

440 ) 

441 

442 

443def apply_display_rules_to_nested_shapes(nested_fields, parent_prop, shape_uri): 

444 """Apply display rules to nested shapes.""" 

445 if not nested_fields: 

446 return [] 

447 

448 # Handle case where parent_prop is not a dictionary 

449 if not isinstance(parent_prop, dict): 

450 return nested_fields 

451 

452 # Create a new list to avoid modifying the original 

453 result_fields = [] 

454 for field in nested_fields: 

455 # Create a copy of the field to avoid modifying the original 

456 new_field = field.copy() 

457 result_fields.append(new_field) 

458 

459 # Find the matching shape in the parent property's display rules 

460 found_matching_shape = False 

461 for rule in parent_prop.get("displayRules", []): 

462 if rule.get("shape") == shape_uri and "nestedDisplayRules" in rule: 

463 found_matching_shape = True 

464 # Apply nested display rules to each field 

465 for field in result_fields: 

466 for nested_rule in rule["nestedDisplayRules"]: 

467 # Check both predicate and uri keys to be more flexible 

468 field_key = field.get("predicate", field.get("uri")) 

469 if field_key == nested_rule["property"]: 

470 # Apply display properties from the rule to the field 

471 for key, value in nested_rule.items(): 

472 if key != "property": 

473 field[key] = value 

474 break 

475 

476 return result_fields 

477 

478 

479def determine_input_type(datatype): 

480 """ 

481 Determina il tipo di input appropriato basato sul datatype XSD. 

482 """ 

483 if not datatype: 

484 return "text" 

485 

486 datatype = str(datatype) 

487 datatype_to_input = { 

488 "http://www.w3.org/2001/XMLSchema#string": "text", 

489 "http://www.w3.org/2001/XMLSchema#integer": "number", 

490 "http://www.w3.org/2001/XMLSchema#decimal": "number", 

491 "http://www.w3.org/2001/XMLSchema#float": "number", 

492 "http://www.w3.org/2001/XMLSchema#double": "number", 

493 "http://www.w3.org/2001/XMLSchema#boolean": "checkbox", 

494 "http://www.w3.org/2001/XMLSchema#date": "date", 

495 "http://www.w3.org/2001/XMLSchema#time": "time", 

496 "http://www.w3.org/2001/XMLSchema#dateTime": "datetime-local", 

497 "http://www.w3.org/2001/XMLSchema#anyURI": "url", 

498 "http://www.w3.org/2001/XMLSchema#email": "email", 

499 } 

500 return datatype_to_input.get(datatype, "text") 

501 

502 

503def add_display_information(field_info, prop): 

504 """ 

505 Aggiunge informazioni di visualizzazione dal display_rules ad un campo. 

506 

507 Argomenti: 

508 field_info (dict): Le informazioni del campo da aggiornare. 

509 prop (dict): Le informazioni della proprietà dalle display_rules. 

510 """ 

511 if "displayName" in prop: 

512 field_info["displayName"] = prop["displayName"] 

513 if "shouldBeDisplayed" in prop: 

514 field_info["shouldBeDisplayed"] = prop.get("shouldBeDisplayed", True) 

515 if "orderedBy" in prop: 

516 field_info["orderedBy"] = prop["orderedBy"] 

517 if "inputType" in prop: 

518 field_info["inputType"] = prop["inputType"] 

519 if "supportsSearch" in prop: 

520 field_info["supportsSearch"] = prop["supportsSearch"] 

521 if "minCharsForSearch" in prop: 

522 field_info["minCharsForSearch"] = prop["minCharsForSearch"] 

523 if "searchTarget" in prop: 

524 field_info["searchTarget"] = prop["searchTarget"] 

525 

526 

527def handle_intermediate_relation(shacl, field_info, prop): 

528 """ 

529 Processa 'intermediateRelation' nelle display_rules e aggiorna il campo. 

530 

531 Argomenti: 

532 field_info (dict): Le informazioni del campo da aggiornare. 

533 prop (dict): Le informazioni della proprietà dalle display_rules. 

534 """ 

535 intermediate_relation = prop["intermediateRelation"] 

536 target_entity_type = intermediate_relation.get("targetEntityType") 

537 intermediate_class = intermediate_relation.get("class") 

538 

539 connecting_property_query = prepareQuery( 

540 """ 

541 SELECT ?property 

542 WHERE { 

543 ?shape sh:targetClass ?intermediateClass ; 

544 sh:property ?propertyShape . 

545 ?propertyShape sh:path ?property ; 

546 sh:node ?targetNode . 

547 ?targetNode sh:targetClass ?targetClass. 

548 } 

549 """, 

550 initNs={"sh": "http://www.w3.org/ns/shacl#"}, 

551 ) 

552 

553 connecting_property_results = shacl.query( 

554 connecting_property_query, 

555 initBindings={ 

556 "intermediateClass": URIRef(intermediate_class), 

557 "targetClass": URIRef(target_entity_type), 

558 }, 

559 ) 

560 

561 connecting_property = next( 

562 (str(row.property) for row in connecting_property_results), None 

563 ) 

564 

565 intermediate_properties = {} 

566 target_shape = None 

567 if "nestedShape" in field_info: 

568 for nested_field in field_info["nestedShape"]: 

569 if nested_field.get("uri") == connecting_property and "nestedShape" in nested_field: 

570 if "nestedShape" in nested_field: 

571 for target_field in nested_field["nestedShape"]: 

572 uri = target_field.get("uri") 

573 if uri: 

574 if uri not in intermediate_properties: 

575 intermediate_properties[uri] = [] 

576 intermediate_properties[uri].append(target_field) 

577 if target_field.get("subjectShape"): 

578 target_shape = target_field["subjectShape"] 

579 

580 field_info["intermediateRelation"] = { 

581 "class": intermediate_class, 

582 "targetEntityType": target_entity_type, 

583 "targetShape": target_shape, 

584 "connectingProperty": connecting_property, 

585 "properties": intermediate_properties, 

586 } 

587 

588 

589def handle_sub_display_rules(shacl, form_fields, entity_key, field_info_list, prop): 

590 """ 

591 Gestisce 'displayRules' nelle display_rules, applicando la regola corretta in base allo shape. 

592 

593 Argomenti: 

594 form_fields (dict): I campi del form da aggiornare. 

595 entity_key (tuple): La chiave dell'entità (class, shape). 

596 field_info_list (list): Le informazioni del campo originale. 

597 prop (dict): Le informazioni della proprietà dalle display_rules. 

598 """ 

599 new_field_info_list = [] 

600 entity_class = entity_key[0] if isinstance(entity_key, tuple) else entity_key 

601 

602 for original_field in field_info_list: 

603 # Trova la display rule corrispondente allo shape del campo 

604 matching_rule = next( 

605 ( 

606 rule 

607 for rule in prop["displayRules"] 

608 if rule["shape"] == original_field["nodeShape"] 

609 ), 

610 None, 

611 ) 

612 

613 if matching_rule: 

614 new_field = { 

615 "entityType": entity_class, 

616 "entityKey": entity_key, # Store the tuple key 

617 "objectClass": original_field.get("objectClass"), 

618 "uri": prop["property"], 

619 "datatype": original_field.get("datatype"), 

620 "min": original_field.get("min"), 

621 "max": original_field.get("max"), 

622 "hasValue": original_field.get("hasValue"), 

623 "nodeShape": original_field.get("nodeShape"), 

624 "nodeShapes": original_field.get("nodeShapes"), 

625 "subjectShape": original_field.get("subjectShape"), 

626 "nestedShape": original_field.get("nestedShape"), 

627 "displayName": matching_rule["displayName"], 

628 "optionalValues": original_field.get("optionalValues", []), 

629 "orderedBy": original_field.get("orderedBy"), 

630 "or": original_field.get("or", []), 

631 } 

632 

633 if "intermediateRelation" in original_field: 

634 new_field["intermediateRelation"] = original_field[ 

635 "intermediateRelation" 

636 ] 

637 

638 # Aggiungi proprietà aggiuntive dalla shape SHACL 

639 if "shape" in matching_rule: 

640 shape_uri = matching_rule["shape"] 

641 additional_properties = extract_additional_properties(shacl, shape_uri) 

642 if additional_properties: 

643 new_field["additionalProperties"] = additional_properties 

644 

645 new_field_info_list.append(new_field) 

646 else: 

647 # Se non c'è una regola corrispondente, mantieni il campo originale 

648 new_field_info_list.append(original_field) 

649 

650 form_fields[entity_key][prop["property"]] = new_field_info_list 

651 

652 

653def get_shape_target_class(shacl, shape_uri): 

654 query = prepareQuery( 

655 """ 

656 SELECT ?targetClass 

657 WHERE { 

658 ?shape sh:targetClass ?targetClass . 

659 } 

660 """, 

661 initNs={"sh": "http://www.w3.org/ns/shacl#"}, 

662 ) 

663 results = execute_shacl_query(shacl, query, {"shape": URIRef(shape_uri)}) 

664 for row in results: 

665 return str(row.targetClass) 

666 return None 

667 

668 

669def get_object_class(shacl, shape_uri, predicate_uri): 

670 query = prepareQuery( 

671 """ 

672 SELECT DISTINCT ?targetClass 

673 WHERE { 

674 ?shape sh:property ?propertyShape . 

675 ?propertyShape sh:path ?predicate . 

676 { 

677 # Caso 1: definizione diretta con sh:node 

678 ?propertyShape sh:node ?nodeShape . 

679 ?nodeShape sh:targetClass ?targetClass . 

680 } UNION { 

681 # Caso 2: definizione diretta con sh:class 

682 ?propertyShape sh:class ?targetClass . 

683 } UNION { 

684 # Caso 3: definizione con sh:or che include node shapes 

685 ?propertyShape sh:or ?orList . 

686 ?orList rdf:rest*/rdf:first ?choice . 

687 { 

688 ?choice sh:node ?nodeShape . 

689 ?nodeShape sh:targetClass ?targetClass . 

690 } UNION { 

691 ?choice sh:class ?targetClass . 

692 } 

693 } 

694 } 

695 """, 

696 initNs={ 

697 "sh": "http://www.w3.org/ns/shacl#", 

698 "rdf": "http://www.w3.org/1999/02/22-rdf-syntax-ns#", 

699 }, 

700 ) 

701 

702 results = execute_shacl_query( 

703 shacl, query, {"shape": URIRef(shape_uri), "predicate": URIRef(predicate_uri)} 

704 ) 

705 

706 # Prendiamo il primo risultato valido 

707 for row in results: 

708 if row.targetClass: 

709 return str(row.targetClass) 

710 return None 

711 

712 

713def extract_shacl_form_fields(shacl, display_rules, app: Flask): 

714 """ 

715 Estrae i campi del form dalle shape SHACL. 

716 

717 Args: 

718 shacl: The SHACL graph 

719 display_rules: The display rules configuration 

720 app: Flask application instance 

721 

722 Returns: 

723 defaultdict: A dictionary where the keys are tuples (class, shape) and the values are dictionaries 

724 of form fields with their properties. 

725 """ 

726 if not shacl: 

727 return dict() 

728 

729 processed_shapes = set() 

730 results = execute_shacl_query(shacl, COMMON_SPARQL_QUERY) 

731 results_list = list(results) 

732 

733 form_fields = process_query_results( 

734 shacl, results_list, display_rules, processed_shapes, app=app, depth=0 

735 ) 

736 

737 return form_fields 

738 

739 

740def execute_shacl_query(shacl: Graph, query, init_bindings=None): 

741 """ 

742 Esegue una query SPARQL sul grafo SHACL con eventuali binding iniziali. 

743 

744 Args: 

745 shacl (Graph): The SHACL graph on which to execute the query. 

746 query (PreparedQuery): The prepared SPARQL query. 

747 init_bindings (dict): Initial bindings for the query. 

748 

749 Returns: 

750 Result: The query results. 

751 """ 

752 if init_bindings: 

753 return shacl.query(query, initBindings=init_bindings) 

754 else: 

755 return shacl.query(query) 

756 

757 

758def extract_additional_properties(shacl, shape_uri): 

759 """ 

760 Estrae proprietà aggiuntive da una shape SHACL. 

761 

762 Argomenti: 

763 shape_uri (str): L'URI della shape SHACL. 

764 

765 Restituisce: 

766 dict: Un dizionario delle proprietà aggiuntive. 

767 """ 

768 additional_properties_query = prepareQuery( 

769 """ 

770 SELECT ?predicate ?hasValue 

771 WHERE { 

772 ?shape a sh:NodeShape ; 

773 sh:property ?property . 

774 ?property sh:path ?predicate ; 

775 sh:hasValue ?hasValue . 

776 } 

777 """, 

778 initNs={"sh": "http://www.w3.org/ns/shacl#"}, 

779 ) 

780 

781 additional_properties_results = shacl.query( 

782 additional_properties_query, initBindings={"shape": URIRef(shape_uri)} 

783 ) 

784 

785 additional_properties = {} 

786 for row in additional_properties_results: 

787 predicate = str(row.predicate) 

788 has_value = str(row.hasValue) 

789 additional_properties[predicate] = has_value 

790 

791 return additional_properties