Coverage for application / qaqc / tator / routes.py: 10%

201 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-07 06:46 +0000

1""" 

2Tator-specific QA/QC endpoints 

3 

4/qaqc/tator/checklist [GET, PATCH] 

5/qaqc/tator/check/<check> [GET] 

6/qaqc/tator/attracted-list [GET] 

7/qaqc/tator/attracted [POST] 

8/qaqc/tator/attracted/<concept> [PATCH, DELETE] 

9""" 

10 

11from io import BytesIO 

12 

13import tator 

14import requests 

15from flask import current_app, flash, redirect, render_template, request, send_file, session 

16 

17from . import tator_qaqc_bp 

18from .tator_qaqc_processor import TatorQaqcProcessor 

19from ...util.tator_localization_type import TatorLocalizationType 

20 

21 

22# view QA/QC checklist for a specified project & section 

23@tator_qaqc_bp.get('/checklist') 

24def tator_qaqc_checklist(): 

25 project_id = int(request.args.get('project')) 

26 section_ids = request.args.getlist('section') 

27 deployment_names = [] 

28 expedition_name = None 

29 if not project_id or not section_ids: 

30 flash('Please select a project and section', 'info') 

31 return redirect('/') 

32 if 'tator_token' not in session.keys(): 

33 flash('Please log in to Tator', 'info') 

34 return redirect('/') 

35 try: 

36 api = tator.get_api( 

37 host=current_app.config.get('TATOR_URL'), 

38 token=session['tator_token'], 

39 ) 

40 for section_id in section_ids: 

41 section = api.get_section(id=int(section_id)) 

42 deployment_names.append(section.name) 

43 if expedition_name is None: 

44 expedition_name = section.path.split('.')[0] 

45 except tator.openapi.tator_openapi.exceptions.ApiException: 

46 flash('Please log in to Tator', 'info') 

47 return redirect('/') 

48 localizations = [] 

49 individual_count = 0 

50 with requests.get( 

51 url=f'{current_app.config.get("DARC_REVIEW_URL")}/tator-qaqc-checklist/{"&".join(deployment_names)}', 

52 headers=current_app.config.get('DARC_REVIEW_HEADERS'), 

53 ) as checklist_res: 

54 if checklist_res.status_code == 200: 

55 checklist = checklist_res.json() 

56 else: 

57 print('ERROR: Unable to get QAQC checklist from external review server') 

58 checklist = {} 

59 # REST is much faster than Python API for large queries 

60 for section_id in section_ids: 

61 res = requests.get( 

62 url=f'{current_app.config.get("TATOR_URL")}/rest/Localizations/{project_id}?section={section_id}', 

63 headers={ 

64 'Content-Type': 'application/json', 

65 'Authorization': f'Token {session["tator_token"]}', 

66 }) 

67 localizations += res.json() 

68 for localization in localizations: 

69 if localization['type'] == TatorLocalizationType.DOT.value: 

70 individual_count += 1 

71 if localization['attributes']['Categorical Abundance'] != '--': 

72 match localization['attributes']['Categorical Abundance']: 

73 case '20-49': 

74 individual_count += 35 

75 case '50-99': 

76 individual_count += 75 

77 case '100-999': 

78 individual_count += 500 

79 case '1000+': 

80 individual_count += 1000 

81 data = { 

82 'title': expedition_name, 

83 'tab_title': deployment_names[0] if len(deployment_names) == 1 else expedition_name, 

84 'deployment_names': deployment_names, 

85 'localization_count': len(localizations), 

86 'individual_count': individual_count, 

87 'checklist': checklist, 

88 } 

89 return render_template('qaqc/tator/qaqc-checklist.html', data=data) 

90 

91 

92# update tator qaqc checklist 

93@tator_qaqc_bp.patch('/checklist') 

94def patch_tator_qaqc_checklist(): 

95 req_json = request.json 

96 deployments = req_json.get('deployments') 

97 if not deployments: 

98 return {}, 400 

99 req_json.pop('deployments') 

100 res = requests.patch( 

101 url=f'{current_app.config.get("DARC_REVIEW_URL")}/tator-qaqc-checklist/{deployments}', 

102 headers=current_app.config.get('DARC_REVIEW_HEADERS'), 

103 json=req_json, 

104 ) 

105 return res.json(), res.status_code 

106 

107 

108# individual qaqc checks 

109@tator_qaqc_bp.get('/check/<check>') 

110def tator_qaqc(check): 

111 project_id = int(request.args.get('project')) 

112 section_ids = request.args.getlist('section') 

113 deployment_names = [] 

114 expedition_name = None 

115 if not project_id or not section_ids: 

116 flash('Please select a project and section', 'info') 

117 return redirect('/') 

118 if 'tator_token' not in session.keys(): 

119 flash('Please log in to Tator', 'info') 

120 return redirect('/') 

121 try: 

122 api = tator.get_api( 

123 host=current_app.config.get('TATOR_URL'), 

124 token=session['tator_token'], 

125 ) 

126 for section_id in section_ids: 

127 section = api.get_section(id=int(section_id)) 

128 deployment_names.append(section.name) 

129 if expedition_name is None: 

130 expedition_name = section.path.split('.')[0] 

131 except tator.openapi.tator_openapi.exceptions.ApiException: 

132 flash('Please log in to Tator', 'info') 

133 return redirect('/') 

134 # get comments and image references from external review db 

135 comments = {} 

136 image_refs = {} 

137 try: 

138 for deployment in deployment_names: 

139 comment_res = requests.get( 

140 url=f'{current_app.config.get("DARC_REVIEW_URL")}/comment/sequence/{deployment}', 

141 headers=current_app.config.get('DARC_REVIEW_HEADERS'), 

142 ) 

143 if comment_res.status_code != 200: 

144 raise requests.exceptions.ConnectionError 

145 comments |= comment_res.json() # merge dicts 

146 image_ref_res = requests.get(f'{current_app.config.get("DARC_REVIEW_URL")}/image-reference/quick') 

147 if image_ref_res.status_code != 200: 

148 raise requests.exceptions.ConnectionError 

149 image_refs = image_ref_res.json() 

150 except requests.exceptions.ConnectionError: 

151 print('\nERROR: unable to connect to external review server\n') 

152 tab_title = deployment_names[0] if len(deployment_names) == 1 else expedition_name 

153 data = { 

154 'concepts': session.get('vars_concepts', []), 

155 'title': check.replace('-', ' ').title(), 

156 'tab_title': f'{tab_title} {check.replace("-", " ").title()}', 

157 'deployment_names': deployment_names, 

158 'reviewers': session.get('reviewers', []), 

159 'comments': comments, 

160 'image_refs': image_refs, 

161 

162 } 

163 if check == 'media-attributes': 

164 # the one case where we don't want to initialize a TatorQaqcProcessor (no need to fetch localizations) 

165 media_attributes = {} 

166 for section_id in section_ids: 

167 media_attributes[section_id] = [] 

168 res = requests.get( # REST API is much faster than Python API for large queries 

169 url=f'{current_app.config.get("TATOR_URL")}/rest/Medias/{project_id}?section={section_id}', 

170 headers={ 

171 'Content-Type': 'application/json', 

172 'Authorization': f'Token {session["tator_token"]}', 

173 }) 

174 if res.status_code != 200: 

175 raise tator.openapi.tator_openapi.exceptions.ApiException 

176 for media in res.json(): 

177 media_attributes[section_id].append(media) 

178 data['page_title'] = 'Media attributes' 

179 data['media_attributes'] = media_attributes 

180 return render_template('qaqc/tator/qaqc-tables.html', data=data) 

181 qaqc_annos = TatorQaqcProcessor( 

182 project_id=project_id, 

183 section_ids=section_ids, 

184 api=api, 

185 darc_review_url=current_app.config.get('DARC_REVIEW_URL'), 

186 tator_url=current_app.config.get('TATOR_URL'), 

187 ) 

188 qaqc_annos.fetch_localizations() 

189 qaqc_annos.load_phylogeny() 

190 match check: 

191 case 'names-accepted': 

192 qaqc_annos.check_names_accepted() 

193 data['page_title'] = 'Scientific names/tentative IDs not accepted in WoRMS' 

194 case 'missing-qualifier': 

195 qaqc_annos.check_missing_qualifier() 

196 data['page_title'] = 'Records classified higher than species missing qualifier' 

197 case 'stet-missing-reason': 

198 qaqc_annos.check_stet_reason() 

199 data['page_title'] = 'Records with a qualifier of \'stet\' missing \'Reason\'' 

200 case 'attracted-not-attracted': 

201 attracted_concepts = requests.get(url=f'{current_app.config.get("DARC_REVIEW_URL")}/attracted').json() 

202 qaqc_annos.check_attracted_not_attracted(attracted_concepts) 

203 data['page_title'] = 'Attracted/not attracted match expected taxa list' 

204 data['attracted_concepts'] = attracted_concepts 

205 case 'exists-in-image-references': 

206 qaqc_annos.check_exists_in_image_references(image_refs) 

207 data['page_title'] = 'Records that do not exist in image references' 

208 case 'same-name-qualifier': 

209 qaqc_annos.check_same_name_qualifier() 

210 data['page_title'] = 'Records with the same scientific name/tentative ID but different qualifiers' 

211 case 'non-target-not-attracted': 

212 qaqc_annos.check_non_target_not_attracted() 

213 data['page_title'] = '"Non-target" records marked as "attracted"' 

214 case 'all-tentative-ids': 

215 qaqc_annos.get_all_tentative_ids() 

216 data['page_title'] = 'Records with a tentative ID (also checks phylogeny vs. scientific name)' 

217 case 'notes-and-remarks': 

218 qaqc_annos.get_all_notes_and_remarks() 

219 data['page_title'] = 'Records with notes and/or remarks' 

220 case 're-examined': 

221 qaqc_annos.get_re_examined() 

222 data['page_title'] = 'Records marked "to be re-examined"' 

223 case 'unique-taxa': 

224 qaqc_annos.get_unique_taxa() 

225 data['page_title'] = 'All unique taxa' 

226 data['unique_taxa'] = qaqc_annos.final_records 

227 return render_template('qaqc/tator/qaqc-tables.html', data=data) 

228 case 'summary': 

229 qaqc_annos.get_summary() 

230 data['page_title'] = 'Summary' 

231 data['annotations'] = qaqc_annos.final_records 

232 return render_template('qaqc/tator/qaqc-tables.html', data=data) 

233 case 'max-n': 

234 qaqc_annos.get_max_n() 

235 data['page_title'] = 'Max N' 

236 data['max_n'] = qaqc_annos.final_records 

237 return render_template('qaqc/tator/qaqc-tables.html', data=data) 

238 case 'tofa': 

239 qaqc_annos.get_tofa() 

240 data['page_title'] = 'Time of First Arrival' 

241 data['tofa'] = qaqc_annos.final_records 

242 return render_template('qaqc/tator/qaqc-tables.html', data=data) 

243 case 'image-guide': 

244 presentation_data = BytesIO() 

245 qaqc_annos.download_image_guide(current_app).save(presentation_data) 

246 presentation_data.seek(0) 

247 return send_file(presentation_data, as_attachment=True, download_name='image-guide.pptx') 

248 case _: 

249 return render_template('errors/404.html', err=''), 404 

250 data['annotations'] = qaqc_annos.final_records 

251 return render_template('qaqc/tator/qaqc.html', data=data) 

252 

253 

254# view list of saved attracted/non-attracted taxa 

255@tator_qaqc_bp.get('/attracted-list') 

256def attracted_list(): 

257 res = requests.get(url=f'{current_app.config.get("DARC_REVIEW_URL")}/attracted') 

258 return render_template('qaqc/tator/attracted-list.html', attracted_concepts=res.json()), 200 

259 

260 

261# add a new concept to the attracted collection 

262@tator_qaqc_bp.post('/attracted') 

263def add_attracted(): 

264 res = requests.post( 

265 url=f'{current_app.config.get("DARC_REVIEW_URL")}/attracted', 

266 headers=current_app.config.get('DARC_REVIEW_HEADERS'), 

267 data={ 

268 'scientific_name': request.values.get('concept'), 

269 'attracted': request.values.get('attracted'), 

270 }, 

271 ) 

272 if res.status_code == 201: 

273 flash(f'Added {request.values.get("concept")}', 'success') 

274 else: 

275 flash(f'Failed to add {request.values.get("concept")}', 'danger') 

276 return res.json(), res.status_code 

277 

278 

279# update an existing attracted concept 

280@tator_qaqc_bp.patch('/attracted/<concept>') 

281def update_attracted(concept): 

282 res = requests.patch( 

283 url=f'{current_app.config.get("DARC_REVIEW_URL")}/attracted/{concept}', 

284 headers=current_app.config.get('DARC_REVIEW_HEADERS'), 

285 data={ 

286 'attracted': request.values.get('attracted'), 

287 } 

288 ) 

289 if res.status_code == 200: 

290 flash(f'Updated {concept}', 'success') 

291 return res.json(), res.status_code 

292 

293 

294# delete an attracted concept 

295@tator_qaqc_bp.delete('/attracted/<concept>') 

296def delete_attracted(concept): 

297 res = requests.delete( 

298 url=f'{current_app.config.get("DARC_REVIEW_URL")}/attracted/{concept}', 

299 headers=current_app.config.get('DARC_REVIEW_HEADERS'), 

300 ) 

301 if res.status_code == 200: 

302 flash(f'Deleted {concept}', 'success') 

303 return res.json(), res.status_code