Coverage for postrfp/ref/handlers/editors.py: 97%

142 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-22 21:34 +0000

1""" 

2HTTP endpoints for managing Content objects 

3""" 

4 

5from typing import Optional 

6 

7from sqlalchemy.orm import Session, joinedload 

8 

9from postrfp.shared.pager import Pager 

10from postrfp.authorisation import perms 

11from postrfp.shared.decorators import http 

12from postrfp.model import User 

13from postrfp.model.ref import Content, ContentSpec, ContentSpecMap, ContentQElementPair 

14from postrfp.shared.serial.refmodels import ( 

15 ContentDocument, 

16 ListResponse, 

17 TagSummary, 

18 SubjectSummary, 

19 ContentSpecMapDocument, 

20 ContentQElementPairDocument, 

21 SchemaPointersResponse, 

22) 

23from postrfp.ref.service.content_service import ( 

24 update_content, 

25 create_content, 

26 fetch_content_spec, 

27) 

28from postrfp.ref.service.reference_validator import ContentReferenceValidator 

29from postrfp.ref.permissions import check_content_authorization 

30from postrfp.shared.exceptions import AuthorizationFailure 

31from postrfp.model.exc import ValidationFailure 

32 

33 

34def build_saved_content(content: Content) -> ContentDocument: 

35 """ 

36 Build a ContentDocument response from a Content model instance. 

37 Converts Tag and Subject relationships to their summary representations. 

38 """ 

39 # Convert tags to TagSummary objects 

40 tags = [ 

41 TagSummary(id=tag.id, name=tag.name, description=tag.description) 

42 for tag in content.tags 

43 ] 

44 

45 # Convert subjects to SubjectSummary objects 

46 subjects = [ 

47 SubjectSummary( 

48 id=subject.id, 

49 name=subject.name, 

50 code=subject.code, 

51 description=subject.description, 

52 subject_type=subject.subject_type.value, 

53 ) 

54 for subject in content.subjects 

55 ] 

56 

57 return ContentDocument( 

58 id=content.id, 

59 date_created=content.date_created, 

60 date_updated=content.date_updated, 

61 auth_policy=content.auth_policy, 

62 title=content.title, 

63 content_doc=content.content_doc, 

64 schema_id=content.schema_id, 

65 primary_subject_id=content.primary_subject_id, 

66 tags=tags, 

67 subjects=subjects, 

68 author_org_id=content.author_org_id, 

69 last_updated_by_id=content.last_updated_by_id, 

70 ) 

71 

72 

73@http 

74def get_contents( 

75 session: Session, 

76 user: User, 

77 q_name: Optional[str] = None, 

78 q_spec_id: Optional[int] = None, 

79 pager: Optional[Pager] = None, 

80) -> ListResponse: 

81 """ 

82 List all content items accessible to the user's organization 

83 

84 Returns content items that the user's organization has permission to view, 

85 including authored content, public content, and content with explicit permissions. 

86 Results can be filtered by title and schema, and are paginated for performance. 

87 

88 **Query Parameters:** 

89 - `q_name`: Filter content by title (case-insensitive partial match) 

90 - `q_spec_id`: Filter by content schema/specification ID 

91 - Standard pagination parameters supported 

92 

93 **Returns:** Paginated list of content summaries accessible to the user 

94 

95 

96 **Content Summary includes:** 

97 - Basic identification (ID, title, dates) 

98 - Schema information 

99 - Author organization 

100 - Associated tags and subjects 

101 

102 **Examples:** 

103 - Get all content: no query parameters 

104 - Search by title: `?q_name=financial report` 

105 - Filter by schema: `?q_spec_id=5` 

106 """ 

107 if pager is None: 

108 pager = Pager(page=1, page_size=20) 

109 

110 # Get all content that might be accessible, then filter by authorization 

111 query = session.query(Content).order_by(Content.date_updated.desc()) 

112 

113 if q_name: 

114 query = query.filter(Content.title.ilike(f"%{q_name}%")) 

115 

116 if q_spec_id: 

117 query = query.filter(Content.schema_id == q_spec_id) 

118 

119 # Get all content first, then filter by authorization 

120 all_content = query.all() 

121 

122 # Filter content based on authorization 

123 authorized_content = [] 

124 for content in all_content: 

125 authorization_result = check_content_authorization(content, user, "view") 

126 if authorization_result.granted: 

127 authorized_content.append(content) 

128 

129 # Apply pagination to the authorized results 

130 total_records = len(authorized_content) 

131 start_idx = pager.startfrom 

132 end_idx = min(pager.goto, total_records) 

133 records = authorized_content[start_idx:end_idx] 

134 

135 return ListResponse( 

136 items=[ContentDocument.model_validate(content) for content in records], 

137 pagination=pager.as_pagination(total_records, len(records)), 

138 ) 

139 

140 

141@http 

142def get_content(session: Session, user: User, content_id: int) -> ContentDocument: 

143 """ 

144 Get a single content item by ID 

145 

146 Retrieves complete information about a content item including all structured 

147 data, metadata, relationships, and associated tags and subjects. The user's 

148 organization must have appropriate permissions to view this content. 

149 

150 **Path Parameters:** 

151 - `content_id`: Unique identifier of the content item 

152 

153 **Raises:** 

154 - `NoResultFound`: If the content ID does not exist 

155 - `AuthorizationFailure`: If the user lacks permission to view this content 

156 """ 

157 content = session.get_one(Content, content_id) 

158 

159 # Enhanced permission check with detailed error reporting 

160 authorization_result = check_content_authorization(content, user, "view") 

161 if not authorization_result.granted: 

162 raise AuthorizationFailure( 

163 f"Cannot view Content {content_id}. {authorization_result.reason}. " 

164 "You do not have permission to perform the requested action." 

165 ) 

166 

167 return build_saved_content(content) 

168 

169 

170@http 

171def post_content( 

172 session: Session, user: User, content_doc: ContentDocument 

173) -> ContentDocument: 

174 """ 

175 Create a new content item 

176 

177 Creates a new structured content item validated against its schema. The content 

178 is authored by the user's organization and can be associated with subjects, 

179 tags, and custom permission settings. 

180 

181 **Returns:** Complete created content with assigned ID and metadata 

182 

183 **Schema Validation:** 

184 The `content_doc` structure must conform to the JSON schema defined by `schema_id`. 

185 Validation errors will be returned if the content doesn't match the schema. 

186 

187 

188 

189 **@permissions REF_CONTENT_SAVE** 

190 """ 

191 user.check_permission(perms.REF_CONTENT_SAVE) 

192 

193 # Additional authorization checks could be added here for: 

194 # - Schema access permissions 

195 # - Subject association permissions 

196 # - Organization content creation policies 

197 # For now, REF_CONTENT_SAVE permission is sufficient 

198 

199 # Use the service function to create the content 

200 new_content = create_content( 

201 session, content_doc, author_org_id=user.org_id, created_by_id=user.id 

202 ) 

203 

204 session.flush() 

205 

206 return build_saved_content(new_content) 

207 

208 

209@http 

210def put_content( 

211 session: Session, user: User, content_id: int, content_doc: ContentDocument 

212) -> ContentDocument: 

213 """ 

214 Update an existing content item 

215 

216 Modifies an existing content item with new data. The user's organization must 

217 have edit permissions for this content. All changes are tracked in the revision 

218 history for auditing purposes. 

219 

220 **Path Parameters:** 

221 - `content_id`: ID of the content item to update 

222 

223 

224 **Returns:** Updated content with new timestamps and metadata 

225 

226 

227 **Schema Validation:** 

228 Updated content must still conform to the original schema. Schema changes 

229 require updating the `schema_id` if needed. 

230 

231 **Revision Tracking:** 

232 Changes are automatically tracked in the revision history, including: 

233 - What fields changed 

234 - Who made the changes 

235 - When changes occurred 

236 - Optional change comments 

237 

238 **@permissions REF_CONTENT_SAVE** 

239 

240 **Raises:** 

241 - `NoResultFound`: If the content ID does not exist 

242 - `AuthorizationFailure`: If the user lacks edit permission 

243 """ 

244 user.check_permission(perms.REF_CONTENT_SAVE) 

245 

246 content = session.get_one(Content, content_id) 

247 

248 # auth_policy is NOT checked here; only edit permission is required 

249 

250 # Use the service function to update the content 

251 update_content(session, content, content_doc, user.id) 

252 

253 return build_saved_content(content) 

254 

255 

256@http 

257def delete_content(session: Session, user: User, content_id: int) -> None: 

258 """ 

259 Delete a content item 

260 

261 Permanently removes a content item and all its associated data including 

262 relationships, permissions, and revision history. This action cannot be undone. 

263 The user's organization must have edit permissions for this content. 

264 

265 **Path Parameters:** 

266 - `content_id`: ID of the content item to delete 

267 

268 **Returns:** None (204 No Content status) 

269 

270 **What Gets Deleted:** 

271 - The content item itself 

272 - All tag associations 

273 - All subject associations 

274 - All content relationships (both incoming and outgoing) 

275 - Complete revision history 

276 - Any comments or annotations 

277 

278 

279 **Important Warnings:** 

280 - **This action is irreversible** - deleted content cannot be recovered 

281 - Other content items that reference this content will have broken relationships 

282 - Organizations that had access will lose that access immediately 

283 - Any workflows or external systems referencing this content may break 

284 

285 **@permissions REF_CONTENT_SAVE** 

286 

287 **Raises:** 

288 - `NoResultFound`: If the content ID does not exist 

289 - `AuthorizationFailure`: If the user lacks edit permission 

290 """ 

291 user.check_permission(perms.REF_CONTENT_SAVE) 

292 

293 content = session.get_one(Content, content_id) 

294 

295 # Check authorization to delete this content 

296 authorization_result = check_content_authorization(content, user, "edit") 

297 if not authorization_result.granted: 

298 raise AuthorizationFailure( 

299 f"Cannot delete Content {content_id}. {authorization_result.reason}. " 

300 "You do not have permission to perform the requested action." 

301 ) 

302 

303 session.delete(content) 

304 

305 

306# ============================================================================ 

307# ContentSpecMap Endpoints - Managing Question-to-Content Field Mappings 

308# ============================================================================ 

309 

310 

311def build_content_spec_map_document( 

312 content_map: ContentSpecMap, 

313) -> ContentSpecMapDocument: 

314 """ 

315 Build a ContentSpecMapDocument response from a ContentSpecMap model instance. 

316 Includes all nested ContentQElementPair objects. 

317 """ 

318 pairs = [ 

319 ContentQElementPairDocument( 

320 id=pair.id, 

321 question_element_id=pair.question_element_id, 

322 content_reference=pair.content_reference, 

323 content_map_id=pair.content_map_id, 

324 ) 

325 for pair in content_map.pairs 

326 ] 

327 

328 return ContentSpecMapDocument( 

329 id=content_map.id, 

330 name=content_map.name, 

331 description=content_map.description, 

332 content_spec_id=content_map.content_spec_id, 

333 content_spec_name=content_map.content_spec.name, 

334 pairs=pairs, 

335 ) 

336 

337 

338def _require_same_org( 

339 content_spec: ContentSpec, user: User, action: str, entity_id: Optional[int] = None 

340) -> None: 

341 """ 

342 Check that a ContentSpec belongs to the user's organization. 

343 

344 Raises AuthorizationFailure if the ContentSpec belongs to a different organization. 

345 

346 Args: 

347 content_spec: The ContentSpec to check 

348 user: The user making the request 

349 action: Description of the action being attempted (e.g., "access ContentSpecMap", "create ContentSpecMap") 

350 entity_id: Optional ID to include in error message (e.g., content_map_id) 

351 """ 

352 if content_spec.org_id != user.org_id: 

353 entity_ref = f" {entity_id}" if entity_id else "" 

354 raise AuthorizationFailure( 

355 f"Cannot {action}{entity_ref}. " 

356 "This ContentSpec belongs to another organization." 

357 ) 

358 

359 

360@http 

361def get_content_maps( 

362 session: Session, 

363 user: User, 

364 q_name: Optional[str] = None, 

365 q_spec_id: Optional[int] = None, 

366 pager: Optional[Pager] = None, 

367) -> ListResponse: 

368 """ 

369 List all content maps accessible to the user's organization 

370 

371 Content maps define how questionnaire responses populate structured content. 

372 Each map connects question elements to specific fields in a content document 

373 using JSON Pointer expressions. Maps are filtered to show only those belonging 

374 to content schemas owned by the user's organization. 

375 

376 **Query Parameters:** 

377 - `q_name`: Filter maps by name (case-insensitive partial match) 

378 - `q_spec_id`: Filter by content schema/specification ID 

379 - Standard pagination parameters supported 

380 

381 **Returns:** Paginated list of content maps accessible to the user 

382 

383 **Access Control:** 

384 Maps are filtered to show only: 

385 - Maps associated with ContentSpecs owned by the user's organization 

386 - This ensures organizations only see mappings for their own content structures 

387 

388 **Map Information Includes:** 

389 - Map identification (ID, name, description) 

390 - Associated ContentSpec ID 

391 - List of all question-to-field mapping pairs 

392 - JSON Pointer expressions for each field mapping 

393 

394 **Use Cases:** 

395 - Browse available content population mappings 

396 - Find mappings for specific content types 

397 - Understand how questionnaires populate content 

398 - Manage organization's content integration workflows 

399 

400 **Examples:** 

401 - Get all maps: no query parameters 

402 - Search by name: `?q_name=vendor SLA` 

403 - Filter by schema: `?q_spec_id=5` 

404 """ 

405 if pager is None: 

406 pager = Pager(page=1, page_size=20) 

407 

408 # Filter maps by user's org through ContentSpec relationship 

409 query = ( 

410 session.query(ContentSpecMap) 

411 .join(ContentSpec, ContentSpecMap.content_spec_id == ContentSpec.id) 

412 .options(joinedload(ContentSpecMap.content_spec).load_only(ContentSpec.name)) 

413 .filter(ContentSpec.org_id == user.org_id) 

414 .order_by(ContentSpecMap.name) 

415 ) 

416 

417 if q_name: 

418 query = query.filter(ContentSpecMap.name.ilike(f"%{q_name}%")) 

419 

420 if q_spec_id: 

421 query = query.filter(ContentSpecMap.content_spec_id == q_spec_id) 

422 

423 total_records = query.count() 

424 records = query.slice(pager.startfrom, pager.goto).all() 

425 

426 return ListResponse( 

427 items=[build_content_spec_map_document(map_obj) for map_obj in records], 

428 pagination=pager.as_pagination(total_records, len(records)), 

429 ) 

430 

431 

432@http 

433def get_content_map( 

434 session: Session, user: User, content_map_id: int 

435) -> ContentSpecMapDocument: 

436 """ 

437 Get a single content map by ID 

438 

439 Retrieves complete information about a content map including all question-to-field 

440 mapping pairs. Each pair specifies which question element maps to which field in 

441 the content document via a JSON Pointer expression. 

442 

443 **Path Parameters:** 

444 - `content_map_id`: Unique identifier of the content map 

445 

446 **JSON Pointer Expression Examples:** 

447 - `$.sla.uptime` → Maps to content_doc["sla"]["uptime"] 

448 - `$.contact.email` → Maps to content_doc["contact"]["email"] 

449 - `$.features[0].name` → Maps to content_doc["features"][0]["name"] 

450 

451 **Use Cases:** 

452 - Understand complete mapping structure 

453 - Review question-to-field connections 

454 - Debug content population issues 

455 - Document integration workflows 

456 

457 **Raises:** 

458 - `NoResultFound`: If the content map ID does not exist 

459 - `AuthorizationFailure`: If the map belongs to another organization's ContentSpec 

460 """ 

461 content_map = session.get_one(ContentSpecMap, content_map_id) 

462 

463 # Check that the map belongs to a ContentSpec owned by user's org 

464 _require_same_org( 

465 content_map.content_spec, user, "access ContentSpecMap", content_map_id 

466 ) 

467 

468 return build_content_spec_map_document(content_map) 

469 

470 

471@http 

472def post_content_map( 

473 session: Session, user: User, map_doc: ContentSpecMapDocument 

474) -> ContentSpecMapDocument: 

475 """ 

476 Create a new content map with mapping pairs 

477 

478 Creates a new mapping between questionnaire elements and content fields. 

479 The map can include multiple pairs in a single request, defining how various 

480 question responses populate different fields in the content document. 

481 

482 **Pair Structure:** 

483 Each pair in the `pairs` array must include: 

484 - `question_element_id`: ID of the question element 

485 - `content_reference`: JSONPointer expression for the content field 

486 

487 **Returns:** Created map with all pairs, assigned IDs, and complete structure 

488 

489 **JSON Pointer Requirements:** 

490 - Must be valid JSON Pointer expressions starting with `$` 

491 - Should reference fields that exist in the associated ContentSpec 

492 - Can target nested objects, arrays, and complex structures 

493 - Examples: `$.name`, `$.address.city`, `$.items[0].quantity` 

494 

495 **Validation:** 

496 The system validates: 

497 - ContentSpec ID exists and belongs to user's organization 

498 - Question element IDs exist (if provided) 

499 - JSON Pointer expressions are well-formed 

500 - No duplicate question elements in the same map 

501 

502 **Ownership:** 

503 - Map is associated with the ContentSpec's organization 

504 - Only users from the ContentSpec's organization can create/edit maps 

505 

506 **@permissions REF_CONTENT_SAVE** 

507 

508 **Example:** 

509 ```json 

510 { 

511 "name": "Vendor SLA Mapping", 

512 "description": "Maps vendor questionnaire to SLA content", 

513 "content_spec_id": 5, 

514 "pairs": [ 

515 { 

516 "question_element_id": 123, 

517 "content_reference": "$.sla.uptime_guarantee" 

518 }, 

519 { 

520 "question_element_id": 124, 

521 "content_reference": "$.sla.response_time" 

522 } 

523 ] 

524 } 

525 ``` 

526 

527 **Raises:** 

528 - `NoResultFound`: If the ContentSpec ID does not exist 

529 - `AuthorizationFailure`: If the ContentSpec belongs to another organization 

530 - `ValidationFailure`: If content_references are invalid against the schema 

531 """ 

532 user.check_permission(perms.REF_CONTENT_SAVE) 

533 

534 # Verify ContentSpec exists and belongs to user's org 

535 content_spec = session.get_one(ContentSpec, map_doc.content_spec_id) 

536 _require_same_org(content_spec, user, "create ContentSpecMap for ContentSpec") 

537 

538 # Validate all content_references against the schema 

539 if map_doc.pairs: 

540 validator = ContentReferenceValidator(content_spec) 

541 is_valid, error_messages = validator.validate_pairs(map_doc.pairs) 

542 

543 if not is_valid: 

544 raise ValidationFailure( 

545 "Invalid content references in mapping pairs", error_messages 

546 ) 

547 

548 # Create the content map 

549 new_map = ContentSpecMap( 

550 name=map_doc.name, 

551 description=map_doc.description, 

552 content_spec_id=map_doc.content_spec_id, 

553 ) 

554 session.add(new_map) 

555 session.flush() # Get the map ID for the pairs 

556 

557 # Create the pairs 

558 for pair_doc in map_doc.pairs: 

559 pair = ContentQElementPair( 

560 content_map_id=new_map.id, 

561 question_element_id=pair_doc.question_element_id, 

562 content_reference=pair_doc.content_reference, 

563 ) 

564 session.add(pair) 

565 

566 session.flush() 

567 return build_content_spec_map_document(new_map) 

568 

569 

570@http 

571def put_content_map( 

572 session: Session, user: User, content_map_id: int, map_doc: ContentSpecMapDocument 

573) -> ContentSpecMapDocument: 

574 """ 

575 Update an existing content map and replace all mapping pairs 

576 

577 Modifies an existing content map with new data. This operation replaces ALL 

578 existing mapping pairs with the new set provided in the request. Any pairs 

579 not included in the update will be deleted. 

580 

581 **Path Parameters:** 

582 - `content_map_id`: ID of the content map to update 

583 

584 **Returns:** Updated map with new structure and all pairs 

585 

586 **Important - Pair Replacement Behavior:** 

587 - ALL existing pairs are deleted 

588 - ALL pairs in the request are created as new 

589 - To keep a pair, include it in the update request 

590 - To remove a pair, omit it from the pairs array 

591 - To add a pair, include it in the pairs array 

592 

593 **Use Cases:** 

594 - Rename or re-describe the mapping 

595 - Add new question-to-field mappings 

596 - Remove obsolete mappings 

597 - Reorganize mapping structure 

598 - Move map to a different ContentSpec 

599 

600 **Best Practices:** 

601 - Include all pairs you want to keep in the update 

602 - Review existing pairs before updating (use GET first) 

603 - Test mapping changes with sample questionnaire data 

604 - Document reasons for structural changes 

605 

606 **@permissions REF_CONTENT_SAVE** 

607 

608 **Raises:** 

609 - `NoResultFound`: If the content map ID does not exist 

610 - `AuthorizationFailure`: If the map or target ContentSpec belongs to another organization 

611 - `ValidationFailure`: If content_references are invalid against the schema 

612 

613 **Example - Adding a new pair while keeping existing ones:** 

614 ```json 

615 { 

616 "name": "Vendor SLA Mapping", 

617 "description": "Updated mapping with additional field", 

618 "content_spec_id": 5, 

619 "pairs": [ 

620 {"question_element_id": 123, "content_reference": "$.sla.uptime_guarantee"}, 

621 {"question_element_id": 124, "content_reference": "$.sla.response_time"}, 

622 {"question_element_id": 125, "content_reference": "$.sla.support_hours"} 

623 ] 

624 } 

625 ``` 

626 """ 

627 user.check_permission(perms.REF_CONTENT_SAVE) 

628 

629 content_map = session.get_one(ContentSpecMap, content_map_id) 

630 

631 # Check that the current map belongs to user's org 

632 _require_same_org( 

633 content_map.content_spec, user, "update ContentSpecMap", content_map_id 

634 ) 

635 

636 # If changing content_spec_id, verify the new one also belongs to user's org 

637 target_content_spec = content_map.content_spec 

638 if map_doc.content_spec_id != content_map.content_spec_id: 

639 target_content_spec = fetch_content_spec(session, map_doc.content_spec_id) 

640 _require_same_org( 

641 target_content_spec, user, "move ContentSpecMap to ContentSpec" 

642 ) 

643 

644 # Validate all content_references against the (potentially new) schema 

645 if map_doc.pairs: 

646 validator = ContentReferenceValidator(target_content_spec) 

647 is_valid, error_messages = validator.validate_pairs(map_doc.pairs) 

648 

649 if not is_valid: 

650 raise ValidationFailure( 

651 "Invalid content references in mapping pairs", error_messages 

652 ) 

653 

654 # Update basic fields 

655 content_map.name = map_doc.name 

656 content_map.description = map_doc.description 

657 content_map.content_spec_id = map_doc.content_spec_id 

658 

659 # Delete all existing pairs 

660 content_map.pairs.clear() 

661 

662 # Create new pairs 

663 for pair_doc in map_doc.pairs: 

664 pair = ContentQElementPair( 

665 content_map_id=content_map.id, 

666 question_element_id=pair_doc.question_element_id, 

667 content_reference=pair_doc.content_reference, 

668 ) 

669 content_map.pairs.append(pair) 

670 

671 session.flush() 

672 return build_content_spec_map_document(content_map) 

673 

674 

675@http 

676def delete_content_map(session: Session, user: User, content_map_id: int) -> None: 

677 """ 

678 Delete a content map and all its mapping pairs 

679 

680 Permanently removes a content map and all associated question-to-field mappings. 

681 This action cannot be undone. The deletion only affects the mapping definition; 

682 it does not affect existing content or questionnaires. 

683 

684 **Path Parameters:** 

685 - `content_map_id`: ID of the content map to delete 

686 

687 **Returns:** None (204 No Content status) 

688 

689 **What Gets Deleted:** 

690 - The content map itself 

691 - All question-to-field mapping pairs 

692 - All mapping metadata and documentation 

693 

694 **What Is NOT Affected:** 

695 - Content items (existing content remains unchanged) 

696 - ContentSpec (the schema remains intact) 

697 - Question elements (questions remain in questionnaires) 

698 - Questionnaire responses (answers are preserved) 

699 

700 **Impact:** 

701 - Future questionnaire responses cannot use this mapping to populate content 

702 - Existing content created via this mapping remains valid 

703 - Manual content population is still possible 

704 - Other mappings for the same ContentSpec continue to work 

705 

706 **Permission Requirements:** 

707 - User must have REF_CONTENT_SAVE permission 

708 - Map must belong to a ContentSpec owned by user's organization 

709 

710 **Use Cases:** 

711 - Remove obsolete or incorrect mappings 

712 - Clean up unused mapping definitions 

713 - Reorganize mapping structure (delete then recreate) 

714 

715 **@permissions REF_CONTENT_SAVE** 

716 

717 **Raises:** 

718 - `NoResultFound`: If the content map ID does not exist 

719 - `AuthorizationFailure`: If the map belongs to another organization's ContentSpec 

720 

721 **Note:** This is a safe operation that only removes the mapping instructions. 

722 No actual content or questionnaire data is deleted. 

723 """ 

724 user.check_permission(perms.REF_CONTENT_SAVE) 

725 

726 content_map = session.get_one(ContentSpecMap, content_map_id) 

727 

728 # Check that the map belongs to user's org 

729 _require_same_org( 

730 content_map.content_spec, user, "delete ContentSpecMap", content_map_id 

731 ) 

732 

733 session.delete(content_map) 

734 

735 

736@http 

737def get_spec_pointers( 

738 session: Session, 

739 user: User, 

740 spec_id: int, 

741) -> SchemaPointersResponse: 

742 """ 

743 Get all valid JSON Pointer paths for a ContentSpec's schema 

744 

745 Returns a comprehensive list of all valid JSON Pointer expressions that can be 

746 used when creating ContentQElementPair mappings. This endpoint is designed to 

747 help UI developers provide auto-complete, validation, or selection interfaces 

748 when users are configuring question-to-field mappings. 

749 

750 **Path Parameters:** 

751 - `spec_id`: The ID of the ContentSpec to get pointers for 

752 

753 

754 **Access Control:** 

755 - Requires read access to the ContentSpec (must belong to user's organization) 

756 - This ensures organizations only access pointer information for their own schemas 

757 

758 **Pointer Extraction:** 

759 - Recursively extracts all valid paths from the JSON Schema 

760 - Handles nested objects, arrays, $ref definitions, and combinators 

761 - Array indices are normalized (e.g., `/items/0` represents any array element) 

762 - Paths are sorted alphabetically for consistent presentation 

763 

764 **Use Cases:** 

765 - Populate dropdowns or auto-complete fields in mapping UIs 

766 - Validate user input when creating/editing mappings 

767 - Display available fields when configuring content population 

768 - Document available fields for API consumers 

769 

770 **Examples:** 

771 For a schema with company and financial data, returns paths like: 

772 - `/company/name` 

773 - `/company/legal_status/jurisdiction` 

774 - `/financial/auditing/external_auditor` 

775 - `/metadata/assessment_date` 

776 

777 **Raises:** 

778 - `NoResultFound`: If the content_spec_id does not exist 

779 - `AuthorizationFailure`: If the ContentSpec belongs to another organization 

780 

781 **Performance Notes:** 

782 - Pointers are cached by the validator for efficiency 

783 - Safe to call frequently in UI workflows 

784 

785 **Access:** Requires authentication; no explicit permission needed for read operations 

786 """ 

787 # Fetch the content spec and verify ownership 

788 content_spec = session.get_one(ContentSpec, spec_id) 

789 _require_same_org(content_spec, user, "access ContentSpec", spec_id) 

790 

791 # Use the validator to extract all valid paths 

792 validator = ContentReferenceValidator(content_spec) 

793 pointers = sorted(validator.get_valid_paths()) 

794 

795 return SchemaPointersResponse( 

796 content_spec_id=content_spec.id, 

797 content_spec_name=content_spec.name, 

798 pointers=pointers, 

799 )