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

194 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-23 02:22 +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 if not request.args.get('project') or not request.args.get('section') or not request.args.getlist('deployment'): 

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

27 return redirect('/') 

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

29 section_id = int(request.args.get('section')) 

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

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

32 return redirect('/') 

33 try: 

34 api = tator.get_api( 

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

36 token=session['tator_token'], 

37 ) 

38 section_name = api.get_section(id=section_id).name 

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

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

41 return redirect('/') 

42 media_ids = [] 

43 localizations = [] 

44 individual_count = 0 

45 deployments = request.args.getlist('deployment') 

46 for deployment in deployments: 

47 media_ids += session[f'{project_id}_{section_id}'][deployment] 

48 with requests.get( 

49 url=f'{current_app.config.get("DARC_REVIEW_URL")}/tator-qaqc-checklist/{"&".join(request.args.getlist("deployment"))}', 

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

51 ) as checklist_res: 

52 if checklist_res.status_code == 200: 

53 checklist = checklist_res.json() 

54 else: 

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

56 checklist = {} 

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

58 # adding too many media ids results in a query that is too long, so we have to break it up 

59 for i in range(0, len(media_ids), 300): 

60 chunk = media_ids[i:i + 300] 

61 res = requests.get( 

62 url=f'{current_app.config.get("TATOR_URL")}/rest/Localizations/{project_id}?media_id={",".join(map(str, chunk))}', 

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': section_name, 

83 'tab_title': deployments[0] if len(deployments) == 1 else f'{deployments[0]} - {deployments[-1].split("_")[-1]}', 

84 'localization_count': len(localizations), 

85 'individual_count': individual_count, 

86 'checklist': checklist, 

87 } 

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

89 

90 

91# update tator qaqc checklist 

92@tator_qaqc_bp.patch('/checklist') 

93def patch_tator_qaqc_checklist(): 

94 req_json = request.json 

95 deployments = req_json.get('deployments') 

96 if not deployments: 

97 return {}, 400 

98 req_json.pop('deployments') 

99 res = requests.patch( 

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

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

102 json=req_json, 

103 ) 

104 return res.json(), res.status_code 

105 

106 

107# individual qaqc checks 

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

109def tator_qaqc(check): 

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

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

112 return redirect('/') 

113 try: 

114 api = tator.get_api( 

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

116 token=session['tator_token'], 

117 ) 

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

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

120 return redirect('/') 

121 # get comments and image references from external review db 

122 comments = {} 

123 image_refs = {} 

124 if not request.args.get('project') or not request.args.get('section') or not request.args.getlist('deployment'): 

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

126 return redirect('/') 

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

128 section_id = int(request.args.get('section')) 

129 deployments = request.args.getlist('deployment') 

130 try: 

131 for deployment in deployments: 

132 comment_res = requests.get( 

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

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

135 ) 

136 if comment_res.status_code != 200: 

137 raise requests.exceptions.ConnectionError 

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

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

140 if image_ref_res.status_code != 200: 

141 raise requests.exceptions.ConnectionError 

142 image_refs = image_ref_res.json() 

143 except requests.exceptions.ConnectionError: 

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

145 tab_title = deployments[0] if len(deployments) == 1 else f'{deployments[0]} - {deployments[-1].split("_")[-1]}' 

146 data = { 

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

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

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

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

151 'comments': comments, 

152 'image_refs': image_refs, 

153 } 

154 if check == 'media-attributes': 

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

156 media_attributes = {} 

157 for deployment in request.args.getlist('deployment'): 

158 media_attributes[deployment] = [] 

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

160 url=f'{current_app.config.get("TATOR_URL")}/rest/Medias/{project_id}?section={section_id}&attribute_contains=%24name%3A%3A{deployment}', 

161 headers={ 

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

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

164 }) 

165 if res.status_code != 200: 

166 raise tator.openapi.tator_openapi.exceptions.ApiException 

167 for media in res.json(): 

168 media_attributes[deployment].append(media) 

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

170 data['media_attributes'] = media_attributes 

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

172 qaqc_annos = TatorQaqcProcessor( 

173 project_id=project_id, 

174 section_id=section_id, 

175 api=api, 

176 deployment_list=request.args.getlist('deployment'), 

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

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

179 ) 

180 qaqc_annos.fetch_localizations() 

181 qaqc_annos.load_phylogeny() 

182 match check: 

183 case 'names-accepted': 

184 qaqc_annos.check_names_accepted() 

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

186 case 'missing-qualifier': 

187 qaqc_annos.check_missing_qualifier() 

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

189 case 'stet-missing-reason': 

190 qaqc_annos.check_stet_reason() 

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

192 case 'attracted-not-attracted': 

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

194 qaqc_annos.check_attracted_not_attracted(attracted_concepts) 

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

196 data['attracted_concepts'] = attracted_concepts 

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

198 qaqc_annos.check_exists_in_image_references(image_refs) 

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

200 case 'same-name-qualifier': 

201 qaqc_annos.check_same_name_qualifier() 

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

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

204 qaqc_annos.check_non_target_not_attracted() 

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

206 case 'all-tentative-ids': 

207 qaqc_annos.get_all_tentative_ids() 

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

209 case 'notes-and-remarks': 

210 qaqc_annos.get_all_notes_and_remarks() 

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

212 case 're-examined': 

213 qaqc_annos.get_re_examined() 

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

215 case 'unique-taxa': 

216 qaqc_annos.get_unique_taxa() 

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

218 data['unique_taxa'] = qaqc_annos.final_records 

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

220 case 'summary': 

221 qaqc_annos.get_summary() 

222 data['page_title'] = 'Summary' 

223 data['annotations'] = qaqc_annos.final_records 

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

225 case 'max-n': 

226 qaqc_annos.get_max_n() 

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

228 data['max_n'] = qaqc_annos.final_records 

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

230 case 'tofa': 

231 qaqc_annos.get_tofa() 

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

233 data['tofa'] = qaqc_annos.final_records 

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

235 case 'image-guide': 

236 presentation_data = BytesIO() 

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

238 presentation_data.seek(0) 

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

240 case _: 

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

242 data['annotations'] = qaqc_annos.final_records 

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

244 

245 

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

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

248def attracted_list(): 

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

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

251 

252 

253# add a new concept to the attracted collection 

254@tator_qaqc_bp.post('/attracted') 

255def add_attracted(): 

256 res = requests.post( 

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

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

259 data={ 

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

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

262 }, 

263 ) 

264 if res.status_code == 201: 

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

266 else: 

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

268 return res.json(), res.status_code 

269 

270 

271# update an existing attracted concept 

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

273def update_attracted(concept): 

274 res = requests.patch( 

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

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

277 data={ 

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

279 } 

280 ) 

281 if res.status_code == 200: 

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

283 return res.json(), res.status_code 

284 

285 

286# delete an attracted concept 

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

288def delete_attracted(concept): 

289 res = requests.delete( 

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

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

292 ) 

293 if res.status_code == 200: 

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

295 return res.json(), res.status_code