Coverage for postrfp/ref/handlers/subjects.py: 100%

73 statements  

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

1""" 

2HTTP endpoints for Subject CRUD operations 

3""" 

4 

5from typing import Optional, List 

6from sqlalchemy.orm import Session 

7from sqlalchemy import or_ 

8 

9from postrfp.shared.pager import Pager 

10from postrfp.shared.decorators import http 

11from postrfp.model import User 

12from postrfp.model.ref import Subject, SubjectType, Content 

13from postrfp.shared.serial.refmodels import ( 

14 SubjectDocument, 

15 ListResponse, 

16 DeletionResponse, 

17 SubjectSummary, 

18) 

19 

20 

21@http 

22def get_subjects( 

23 session: Session, 

24 user: User, 

25 q_name: Optional[str] = None, 

26 q_type: Optional[str] = None, 

27 q_parent_id: Optional[int] = None, 

28 pager: Optional[Pager] = None, 

29) -> ListResponse: 

30 """ 

31 List subjects with optional filtering 

32 

33 Subjects represent entities that content items can be about, such as countries, 

34 organizations, markets, industry sectors, or products. They can have hierarchical 

35 relationships and carry their own metadata and permission systems. 

36 

37 **Query Parameters:** 

38 - `q_name`: Filter subjects by name (case-insensitive partial match) 

39 - `q_type`: Filter by subject type (country, organization, market, sector, product, other) 

40 - `q_parent_id`: Filter to show only direct children of the specified parent subject 

41 - Standard pagination parameters supported 

42 

43 **Returns:** Paginated list of subjects matching the filter criteria 

44 

45 **Examples:** 

46 - Get all banking organizations: `?q_type=organization&q_name=bank` 

47 - Get all cities in a country: `?q_parent_id=123&q_type=location` 

48 """ 

49 if pager is None: 

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

51 

52 query = session.query(Subject) 

53 

54 # Apply filters 

55 if q_name: 

56 query = query.filter(Subject.name.ilike(f"%{q_name}%")) 

57 

58 if q_type: 

59 try: 

60 subject_type = SubjectType(q_type.lower()) 

61 query = query.filter(Subject.subject_type == subject_type) 

62 except ValueError: 

63 pass # Invalid type, ignore filter 

64 

65 if q_parent_id is not None: 

66 query = query.filter(Subject.parent_id == q_parent_id) 

67 

68 # Get total count for pagination 

69 total_count = query.count() 

70 

71 # Apply pagination - use startfrom instead of offset 

72 subjects = query.offset(pager.startfrom).limit(pager.page_size).all() 

73 

74 # Convert to response models 

75 items = [SubjectSummary.model_validate(subject) for subject in subjects] 

76 pagination = pager.as_pagination(total_count, len(items)) 

77 

78 return ListResponse(items=items, pagination=pagination) 

79 

80 

81@http 

82def get_subject(session: Session, user: User, subject_id: int) -> SubjectDocument: 

83 """ 

84 Get a specific subject by ID 

85 

86 Retrieves complete information about a subject including its name, type, 

87 description, hierarchical position, managing organization, and metadata. 

88 

89 **Path Parameters:** 

90 - `subject_id`: Unique identifier of the subject 

91 

92 **Returns:** Complete subject information 

93 

94 **Raises:** 

95 - `NoResultFound`: If the subject ID does not exist 

96 

97 **Subject Types:** 

98 - `country`: Geographic countries or regions 

99 - `organization`: Companies, banks, institutions 

100 - `market`: Stock exchanges, trading venues 

101 - `sector`: Industry classifications 

102 - `product`: Financial products or services 

103 - `other`: Custom subject types 

104 """ 

105 subject = session.get_one(Subject, subject_id) 

106 return SubjectDocument.model_validate(subject) 

107 

108 

109@http 

110def post_subject( 

111 session: Session, user: User, subject_data: SubjectDocument 

112) -> SubjectDocument: 

113 """ 

114 Create a new subject 

115 

116 Creates a new subject that can be used to categorize content items. Subjects 

117 can have hierarchical relationships with other subjects and their own permission 

118 systems for controlling access to related content. 

119 

120 **Request Body:** Subject data including: 

121 - `name`: Display name for the subject (required) 

122 - `code`: Optional short identifier or symbol 

123 - `description`: Optional detailed description 

124 - `subject_type`: Type classification (required) 

125 - `parent_id`: Optional parent subject for hierarchical relationships 

126 - `managing_org_id`: Optional organization that manages this subject 

127 - `subject_metadata`: Optional key-value metadata (country codes, market identifiers, etc.) 

128 

129 **Returns:** Created subject with assigned ID and timestamps 

130 

131 **Permission Impact:** 

132 When content references this subject, the subject's permissions may affect 

133 content access depending on the content's permission inheritance mode. 

134 

135 **Hierarchical Relationships:** 

136 Use `parent_id` to create hierarchies like: 

137 - City → State → Country 

138 - Subsidiary → Parent Company 

139 - Sub-sector → Industry Sector 

140 """ 

141 # Convert string to enum 

142 try: 

143 subject_type_enum = SubjectType(subject_data.subject_type.lower()) 

144 except ValueError: 

145 raise ValueError(f"Invalid subject_type: {subject_data.subject_type}") 

146 

147 # Create new subject 

148 subject = Subject( 

149 name=subject_data.name, 

150 code=subject_data.code, 

151 description=subject_data.description, 

152 subject_type=subject_type_enum, 

153 parent_id=subject_data.parent_id, 

154 managing_org_id=subject_data.managing_org_id, 

155 subject_metadata=subject_data.subject_metadata or {}, 

156 ) 

157 

158 session.add(subject) 

159 session.flush() # Get the ID 

160 

161 return SubjectDocument.model_validate(subject) 

162 

163 

164@http 

165def put_subject( 

166 session: Session, user: User, subject_id: int, subject_data: SubjectDocument 

167) -> SubjectDocument: 

168 """ 

169 Update an existing subject 

170 

171 Modifies all fields of an existing subject. Changes to subject properties 

172 may affect access to content that references this subject, depending on 

173 those content items' permission inheritance settings. 

174 

175 **Path Parameters:** 

176 - `subject_id`: ID of the subject to update 

177 

178 **Request Body:** Complete subject data (all fields will be updated) 

179 

180 **Returns:** Updated subject information 

181 

182 **Raises:** 

183 - `NoResultFound`: If the subject ID does not exist 

184 - `ValueError`: If invalid subject_type provided 

185 

186 **Important Notes:** 

187 - Changing hierarchical relationships (`parent_id`) affects content categorization 

188 - Updates to managing organization may impact permission inheritance 

189 - Metadata changes can affect external system integrations 

190 """ 

191 subject = session.get_one(Subject, subject_id) 

192 

193 # Convert string to enum 

194 try: 

195 subject_type_enum = SubjectType(subject_data.subject_type.lower()) 

196 except ValueError: 

197 raise ValueError(f"Invalid subject_type: {subject_data.subject_type}") 

198 

199 # Update fields 

200 subject.name = subject_data.name 

201 subject.code = subject_data.code 

202 subject.description = subject_data.description 

203 subject.subject_type = subject_type_enum 

204 subject.parent_id = subject_data.parent_id 

205 subject.managing_org_id = subject_data.managing_org_id 

206 subject.subject_metadata = subject_data.subject_metadata or {} 

207 

208 session.flush() 

209 

210 return SubjectDocument.model_validate(subject) 

211 

212 

213@http 

214def delete_subject(session: Session, user: User, subject_id: int) -> DeletionResponse: 

215 """ 

216 Delete a subject 

217 

218 Permanently removes a subject from the system. This operation is protected 

219 by referential integrity checks to prevent data inconsistency. 

220 

221 **Path Parameters:** 

222 - `subject_id`: ID of the subject to delete 

223 

224 **Returns:** Success confirmation with deletion details 

225 

226 **Raises:** 

227 - `NoResultFound`: If the subject ID does not exist 

228 - `ValueError`: If the subject has dependencies that prevent deletion: 

229 - Subject has child subjects in the hierarchy 

230 - Subject is referenced as primary subject by content items 

231 - Subject is referenced in content subject associations 

232 

233 **Protection Mechanisms:** 

234 The system prevents deletion of subjects that would break data integrity: 

235 

236 1. **Hierarchical Protection:** Cannot delete subjects with children 

237 2. **Content Protection:** Cannot delete subjects referenced by content 

238 3. **Permission Protection:** Associated permissions are cascade-deleted 

239 

240 **Recommendation:** 

241 Before deleting, consider if the subject should be: 

242 - Marked as inactive/deprecated instead of deleted 

243 - Replaced by transferring references to another subject 

244 - Have its child subjects reassigned to a different parent 

245 """ 

246 subject = session.get_one(Subject, subject_id) 

247 

248 # Check if subject has children 

249 children_count = ( 

250 session.query(Subject).filter(Subject.parent_id == subject_id).count() 

251 ) 

252 if children_count > 0: 

253 raise ValueError(f"Cannot delete subject with {children_count} child subjects") 

254 

255 # Check if subject is used by content 

256 content_count = ( 

257 session.query(Content) 

258 .filter( 

259 or_( 

260 Content.primary_subject_id == subject_id, 

261 Content.subjects.any(id=subject_id), 

262 ) 

263 ) 

264 .count() 

265 ) 

266 

267 if content_count > 0: 

268 raise ValueError( 

269 f"Cannot delete subject referenced by {content_count} content items" 

270 ) 

271 

272 session.delete(subject) 

273 

274 return DeletionResponse(success=True, message=f"Subject {subject_id} deleted") 

275 

276 

277@http 

278def get_subject_children( 

279 session: Session, user: User, subject_id: int 

280) -> List[SubjectDocument]: 

281 """ 

282 Get all direct children of a subject 

283 

284 Retrieves subjects that have the specified subject as their direct parent 

285 in the hierarchical relationship. This is useful for building tree views 

286 or navigation interfaces. 

287 

288 **Path Parameters:** 

289 - `subject_id`: ID of the parent subject 

290 

291 **Returns:** List of direct child subjects (not recursive) 

292 

293 **Use Cases:** 

294 - Building hierarchical navigation menus 

295 - Showing organizational structure 

296 - Geographic drill-down (country → states → cities) 

297 - Industry classification browsing 

298 

299 **Note:** 

300 This endpoint only returns direct children. For the complete hierarchy, 

301 you may need to make recursive calls or use the main subjects list endpoint 

302 with appropriate filtering. 

303 

304 **Example Hierarchies:** 

305 - Geographic: USA → California → San Francisco 

306 - Corporate: Holding Company → Subsidiaries → Divisions 

307 - Market: Region → Exchange → Trading Segments 

308 """ 

309 children = session.query(Subject).filter(Subject.parent_id == subject_id).all() 

310 return [SubjectDocument.model_validate(child) for child in children]