Coverage for api/handlers/misc/documents.py: 16%

153 statements  

« prev     ^ index     » next       coverage.py v7.6.2, created at 2024-10-10 03:02 +0300

1import datetime 

2from typing import Any, Callable 

3 

4from bson import ObjectId 

5from config import EMBEDDED_DOCUMENTS_LIMIT 

6from database.entity import update_etag_and_text_search 

7from database.integration_1c.entity_checker import ( 

8 check_resource_movable_to_updates, 

9) 

10from database.integration_1c.savers import save_entity_to_updates 

11from dbcc import MongoTableEngine 

12from exceptions import ( 

13 BadParameterHTTPError, 

14 NoAccessHTTPError, 

15 NotAcceptableHTTPError, 

16 NotFoundHTTPError, 

17) 

18from handlers.authorization.check_role import has_role 

19from handlers.grabbers.documents import docs_grabber 

20from mongodb import ( 

21 db, 

22 docs_col, 

23 drivers_col, 

24 orders_col, 

25 orgs_col, 

26 trailers_col, 

27 trucks_col, 

28) 

29from pymongo import ReturnDocument 

30from pymongo.errors import WriteError 

31from sotrans_models.models._mongo import PydanticObjectId 

32from sotrans_models.models.misc.document import ( 

33 DocumentCreateModel, 

34 DocumentDBModel, 

35 DocumentTypeModel, 

36 DocumentTypingAlias, 

37 DocumentUpdateModel, 

38 DriverDocumentType, 

39 OrganizationDocumentType, 

40 RequestDocumentType, 

41 TrailerDocumentType, 

42 TruckDocumentType, 

43 UserDocumentType, 

44) 

45from sotrans_models.models.resources.trucks import TruckDBModel 

46from sotrans_models.models.responses import GenericGetListResponse 

47from sotrans_models.models.roles import SotransRole 

48from sotrans_models.models.users import SotransOIDCUserModel 

49from sotrans_models.utils.text_mappers import get_documents_text_search 

50from utils.data_grabber import ( 

51 BaseGetListQueryParams, 

52 BaseGetOneQueryParams, 

53 update_search_query, 

54) 

55from utils.dt_utils import get_current_datetime 

56from utils.helper import EntityHash, etag_detalizer, get_hash, get_org_oid 

57 

58 

59def docs_restriction_query_builder(executor: SotransOIDCUserModel): 

60 restriction_query = {DocumentDBModel.deleted_at: None} 

61 if not has_role(executor, SotransRole.company_logistician): 

62 restriction_query.update( 

63 {DocumentDBModel.organization_id: get_org_oid(executor)} 

64 ) 

65 if not has_role(executor, SotransRole.carrier_director): 

66 restriction_query.update( 

67 { 

68 DocumentDBModel.type: { 

69 "$nin": [t.value for t in OrganizationDocumentType] 

70 } 

71 } 

72 ) 

73 return restriction_query 

74 

75 

76async def type_document_strategy( 

77 document_type: DocumentTypingAlias, coro: Callable 

78): 

79 if isinstance(document_type, (UserDocumentType, RequestDocumentType)): 

80 await coro(orders_col) 

81 elif isinstance(document_type, TruckDocumentType): 

82 await coro(trucks_col) 

83 elif isinstance(document_type, TrailerDocumentType): 

84 await coro(trailers_col) 

85 elif isinstance(document_type, DriverDocumentType): 

86 await coro(drivers_col) 

87 

88 

89def check_valid_until( 

90 document: DocumentCreateModel, current_dt: datetime.datetime 

91): 

92 if document.valid_until: 

93 naive_current = current_dt.replace(tzinfo=None) 

94 valid_until_comparable = document.valid_until.replace(tzinfo=None) 

95 if document.valid_until and valid_until_comparable <= naive_current: 

96 raise BadParameterHTTPError("Документ просрочен") 

97 

98 

99async def get_related_with_checks( 

100 object_id: ObjectId, col_name: str 

101) -> dict[str, Any]: 

102 related = await db[col_name].find_single( 

103 "_id", 

104 object_id, 

105 ) 

106 if related is None: 

107 raise NotFoundHTTPError("сущность под object_id") 

108 return related 

109 

110 

111def try_fill_org_id( 

112 related: dict, document_data: dict[str, Any], user: SotransOIDCUserModel 

113): 

114 org_oid = related.get(TruckDBModel.organization_id) 

115 if org_oid is not None: 

116 if not has_role( 

117 user, SotransRole.company_logistician 

118 ) and user.organization_id != str(org_oid): 

119 raise NoAccessHTTPError("другая организация") 

120 document_data["organization_id"] = org_oid 

121 

122 

123async def on_create_document( 

124 document: DocumentCreateModel, user: SotransOIDCUserModel 

125): 

126 if document.collection and document.collection not in ( 

127 trucks_col.collection_name, 

128 trailers_col.collection_name, 

129 drivers_col.collection_name, 

130 orders_col.collection_name, 

131 orgs_col.collection_name, 

132 docs_col.collection_name, 

133 ): 

134 raise BadParameterHTTPError("несуществующая коллекция") 

135 current_time = get_current_datetime() 

136 check_valid_until(document, current_time) 

137 

138 document_data = document.model_dump() 

139 if not document.collection: 

140 if document.object_id: 

141 if isinstance(document.type, TruckDocumentType): 

142 col = trucks_col.collection_name 

143 elif isinstance(document.type, TrailerDocumentType): 

144 col = trailers_col.collection_name 

145 elif isinstance(document.type, DriverDocumentType): 

146 col = drivers_col.collection_name 

147 elif isinstance(document.type, UserDocumentType): 

148 col = orders_col.collection_name 

149 else: 

150 col = orgs_col.collection_name 

151 else: 

152 col = docs_col.collection_name 

153 else: 

154 col = document.collection 

155 

156 if document.object_id: 

157 if ( 

158 not has_role(user, SotransRole.company_logistician) 

159 and col == orgs_col.collection_name 

160 ): 

161 org_oid = get_org_oid(user) 

162 if org_oid != document.object_id: 

163 raise NoAccessHTTPError("организация") 

164 related = await get_related_with_checks(document.object_id, col) 

165 if col == orgs_col.collection_name: 

166 document_data["organization_id"] = document.object_id 

167 else: 

168 try_fill_org_id(related, document_data, user) 

169 document_data["verification"] = None 

170 document_data["created_at"] = current_time 

171 document_data["collection"] = col 

172 etag = get_hash(document_data) 

173 inserted_main_doc = await docs_col.create( 

174 { 

175 **document_data, 

176 **{ 

177 "etag": etag, 

178 DocumentDBModel.text_search: get_documents_text_search( 

179 DocumentDBModel(**document_data) 

180 ), 

181 }, 

182 } 

183 ) 

184 

185 async def update_or_create_documents(collection: MongoTableEngine): 

186 try: 

187 await collection.collection.update_one( 

188 { 

189 "_id": document.object_id, 

190 }, 

191 {"$push": {TruckDBModel.documents[0]: inserted_main_doc}}, 

192 ) 

193 except ( 

194 WriteError, 

195 AttributeError, 

196 ): # MongoMock throws AttributeError 

197 await collection.update_by_id( 

198 document.object_id, {"documents": [inserted_main_doc]} 

199 ) 

200 

201 if document.object_id: 

202 if (docs := related.get("documents")) is None or len( 

203 docs 

204 ) < EMBEDDED_DOCUMENTS_LIMIT: 

205 await update_or_create_documents(db[col]) 

206 if col in ( 

207 trucks_col.collection_name, 

208 trailers_col.collection_name, 

209 drivers_col.collection_name, 

210 ) and await check_resource_movable_to_updates( 

211 related, col, document.type 

212 ): 

213 # this is impossible as check_resource_movable_to_updates needs 1 doc at least 

214 # if related.get("documents") is None: 

215 # related["documents"] = [inserted_main_doc] 

216 # else: 

217 related["documents"].append(inserted_main_doc) 

218 await save_entity_to_updates(col, related) 

219 

220 return inserted_main_doc 

221 

222 

223async def on_delete_document( 

224 executor: SotransOIDCUserModel, doc_id: ObjectId, etag: EntityHash | None 

225): 

226 restriction_query = docs_restriction_query_builder(executor) 

227 etag_q = {"etag": etag} if etag is not None else {} 

228 document = await docs_col.collection.find_one_and_update( 

229 {"_id": doc_id} | restriction_query | etag_q, 

230 {"$set": {DocumentDBModel.deleted_at: datetime.datetime.utcnow()}}, 

231 ) 

232 if document is None: 

233 await etag_detalizer( 

234 docs_col, etag_q, {"_id": doc_id} | restriction_query 

235 ) 

236 raise NotFoundHTTPError("документ") 

237 doc_type = DocumentTypeModel(document_type=document["type"]) 

238 

239 async def remove_from_collection(collection: MongoTableEngine): 

240 await collection.collection.update_one( 

241 {TruckDBModel.documents[0].id: doc_id}, 

242 {"$pull": {TruckDBModel.documents[0]: {"_id": doc_id}}}, 

243 ) 

244 

245 await type_document_strategy( 

246 doc_type.document_type, remove_from_collection 

247 ) 

248 

249 

250async def on_get_documents( 

251 executor: SotransOIDCUserModel, params: BaseGetListQueryParams 

252): 

253 documents_filter: dict = {DocumentDBModel.deleted_at: None} 

254 if not ( 

255 has_role(executor, SotransRole.carrier_director) 

256 or has_role(executor, SotransRole.company_logistician) 

257 ): 

258 db_doc_types = [t.value for t in OrganizationDocumentType] 

259 documents_filter.update({"type": {"$nin": db_doc_types}}) 

260 if not has_role(executor, SotransRole.company_logistician): 

261 org_oid = get_org_oid(executor) 

262 organization = await orgs_col.collection.find_one( 

263 {"_id": org_oid}, 

264 projection={"_id": 1}, 

265 ) 

266 documents_filter.update({"organization_id": organization["_id"]}) 

267 params.where = update_search_query(params.where, documents_filter) 

268 return await docs_grabber.get_list(params, executor) 

269 

270 

271async def on_get_documents_new( 

272 executor: SotransOIDCUserModel, 

273 collection: str, 

274 object_id: PydanticObjectId, 

275 params: BaseGetListQueryParams, 

276) -> GenericGetListResponse[DocumentDBModel]: 

277 if collection and collection not in ( 

278 trucks_col.collection_name, 

279 trailers_col.collection_name, 

280 drivers_col.collection_name, 

281 orders_col.collection_name, 

282 orgs_col.collection_name, 

283 docs_col.collection_name, 

284 ): 

285 raise BadParameterHTTPError("несуществующая коллекция") 

286 if not has_role(executor, SotransRole.company_logistician): 

287 params.where = update_search_query( 

288 params.where, 

289 # types are correct as serialized! 

290 {DocumentDBModel.organization_id: executor.organization_id}, 

291 ) 

292 params.where = update_search_query( 

293 params.where, {DocumentDBModel.object_id: str(object_id)} 

294 ) 

295 params.where = update_search_query( 

296 params.where, {DocumentDBModel.collection: collection} 

297 ) 

298 params.where = update_search_query( 

299 params.where, {DocumentDBModel.deleted_at: None} 

300 ) 

301 return await docs_grabber.get_list(params) 

302 

303 

304async def on_get_document( 

305 executor: SotransOIDCUserModel, 

306 doc_id: ObjectId, 

307 params: BaseGetOneQueryParams, 

308) -> DocumentDBModel | dict[str, Any]: 

309 restriction_query = docs_restriction_query_builder(executor) 

310 return await docs_grabber.get_one_by_id_with_pattern( 

311 doc_id, params, restriction_query 

312 ) 

313 

314 

315async def on_update_document( 

316 executor: SotransOIDCUserModel, doc_id: ObjectId, doc: DocumentUpdateModel 

317) -> DocumentDBModel: 

318 if doc.object_id: 

319 raise NotAcceptableHTTPError("id объекта не подлежит изменению") 

320 restriction_query = docs_restriction_query_builder(executor) 

321 up_payload = doc.model_dump(exclude_none=True) 

322 etag_q = {"etag": doc.etag} if doc.etag else {} 

323 document = await docs_col.collection.find_one_and_update( 

324 {"_id": doc_id} | restriction_query | etag_q, 

325 {"$set": up_payload}, 

326 return_document=ReturnDocument.AFTER, 

327 ) 

328 if document is None: 

329 await etag_detalizer( 

330 docs_col, etag_q, {"_id": doc_id} | restriction_query 

331 ) 

332 raise NotFoundHTTPError("документ") 

333 await update_etag_and_text_search( 

334 document, docs_col, DocumentDBModel, get_documents_text_search 

335 ) 

336 doc_type = DocumentTypeModel(document_type=document["type"]) 

337 

338 async def update_by_type(collection: MongoTableEngine): 

339 array_update = {f"documents.$.{k}": up_payload[k] for k in up_payload} 

340 await collection.collection.update_one( 

341 {TruckDBModel.documents[0].id: doc_id}, {"$set": array_update} 

342 ) 

343 

344 await type_document_strategy(doc_type.document_type, update_by_type) 

345 

346 return document