Coverage for application / qaqc / tator / dropcam / routes.py: 13%

175 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-03-23 05:22 +0000

1""" 

2Dropcam (dscm) QA/QC endpoints 

3 

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

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

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

7/qaqc/tator/dropcam/attracted [POST] 

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

9""" 

10from io import BytesIO 

11 

12import requests 

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

14 

15from . import dropcam_qaqc_bp 

16from application.tator.tator_dropcam_qaqc_processor import TatorDropcamQaqcProcessor 

17from application.tator.tator_type import TatorLocalizationType 

18from application.tator.tator_rest_client import TatorRestClient 

19from application.qaqc.tator.util import init_tator_api, get_comments_and_image_refs 

20 

21 

22# TODO this is dumb. Cache this or at least call on different threads 

23def _get_deployment_info(tator_api, section_ids): 

24 deployment_names = [] 

25 expedition_name = None 

26 for section_id in section_ids: 

27 print(f'Getting deployment info for section {section_id}') 

28 section = tator_api.get_section(id=int(section_id)) 

29 deployment_names.append(section.name) 

30 if expedition_name is None: 

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

32 return deployment_names, expedition_name 

33 

34 

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

36@dropcam_qaqc_bp.get('/checklist') 

37def dropcam_qaqc_checklist(): 

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

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

40 if not project_id or not section_ids: 

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

42 return redirect('/') 

43 tator_api, err = init_tator_api() 

44 if err: 

45 return err 

46 deployment_names, expedition_name = _get_deployment_info(tator_api, section_ids) 

47 tator_client = TatorRestClient(current_app.config.get('TATOR_URL'), session['tator_token']) 

48 localizations = [] 

49 individual_count = 0 

50 with requests.get( 

51 url=f'{current_app.config.get("DARC_REVIEW_URL")}/qaqc-checklist/tator-dropcam/{"&".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 for section_id in section_ids: 

60 localizations += tator_client.get_localizations(project_id, section=section_id) 

61 for localization in localizations: 

62 if TatorLocalizationType.is_dot(localization['type']): 

63 individual_count += 1 

64 if localization['attributes'].get('Categorical Abundance', '--') != '--': 

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

66 case '20-49': 

67 individual_count += 35 

68 case '50-99': 

69 individual_count += 75 

70 case '100-999': 

71 individual_count += 500 

72 case '1000+': 

73 individual_count += 1000 

74 data = { 

75 'title': expedition_name, 

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

77 'deployment_names': deployment_names, 

78 'localization_count': len(localizations), 

79 'individual_count': individual_count, 

80 'checklist': checklist, 

81 } 

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

83 

84 

85# update tator qaqc checklist 

86@dropcam_qaqc_bp.patch('/checklist') 

87def patch_dropcam_qaqc_checklist(): 

88 req_json = request.json 

89 deployments = req_json.get('deployments') 

90 if not deployments: 

91 return {}, 400 

92 req_json.pop('deployments') 

93 res = requests.patch( 

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

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

96 json=req_json, 

97 ) 

98 return res.json(), res.status_code 

99 

100 

101# individual qaqc checks 

102@dropcam_qaqc_bp.get('/check/<check>') 

103def dropcam_qaqc(check): 

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

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

106 if not project_id or not section_ids: 

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

108 return redirect('/') 

109 tator_api, err = init_tator_api() 

110 if err: 

111 return err 

112 deployment_names, expedition_name = _get_deployment_info(tator_api, section_ids) 

113 tator_client = TatorRestClient(current_app.config.get('TATOR_URL'), session['tator_token']) 

114 comments, image_refs = get_comments_and_image_refs(deployment_names) 

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

116 data = { 

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

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

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

120 'deployment_names': deployment_names, 

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

122 'comments': comments, 

123 'image_refs': image_refs, 

124 'qaqc_js': 'qaqc.tator_qaqc.dropcam_qaqc.static', 

125 } 

126 if check == 'media-attributes': 

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

128 media_attributes = {} 

129 for section_id in section_ids: 

130 media_attributes[section_id] = tator_client.get_medias_for_section(project_id, section=section_id) 

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

132 data['media_attributes'] = media_attributes 

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

134 qaqc_annos = TatorDropcamQaqcProcessor( 

135 project_id=project_id, 

136 section_ids=section_ids, 

137 api=tator_api, 

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

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

140 ) 

141 qaqc_annos.fetch_localizations() 

142 match check: 

143 case 'names-accepted': 

144 qaqc_annos.check_names_accepted() 

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

146 case 'missing-qualifier': 

147 qaqc_annos.check_missing_qualifier() 

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

149 case 'stet-missing-reason': 

150 qaqc_annos.check_stet_reason() 

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

152 case 'attracted-not-attracted': 

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

154 qaqc_annos.check_attracted_not_attracted(attracted_concepts) 

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

156 data['subtitle'] = '(also flags records with taxa that can be either)' 

157 data['attracted_concepts'] = attracted_concepts 

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

159 qaqc_annos.check_exists_in_image_references(image_refs) 

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

161 data['subtitle'] = '(also flags records that have both a tentative ID and a morphospecies)' 

162 case 'same-name-qualifier': 

163 qaqc_annos.check_same_name_qualifier() 

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

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

166 qaqc_annos.check_non_target_not_attracted() 

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

168 case 'all-tentative-ids': 

169 qaqc_annos.get_all_tentative_ids_and_morphospecies() 

170 data['page_title'] = 'Records with a tentative ID or morphospecies' 

171 data['subtitle'] = '(also checks phylogeny vs. scientific name)' 

172 case 'notes-and-remarks': 

173 qaqc_annos.get_all_notes_and_remarks() 

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

175 case 're-examined': 

176 qaqc_annos.get_re_examined() 

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

178 case 'unique-taxa': 

179 qaqc_annos.get_unique_taxa() 

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

181 data['unique_taxa'] = qaqc_annos.final_records 

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

183 case 'summary': 

184 qaqc_annos.get_summary() 

185 data['page_title'] = 'Summary' 

186 data['annotations'] = qaqc_annos.final_records 

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

188 case 'max-n': 

189 qaqc_annos.get_max_n() 

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

191 data['max_n'] = qaqc_annos.final_records 

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

193 case 'tofa': 

194 qaqc_annos.get_tofa() 

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

196 data['tofa'] = qaqc_annos.final_records 

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

198 case 'image-guide': 

199 presentation_data = BytesIO() 

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

201 presentation_data.seek(0) 

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

203 case _: 

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

205 data['annotations'] = qaqc_annos.final_records 

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

207 

208 

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

210@dropcam_qaqc_bp.get('/attracted-list') 

211def attracted_list(): 

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

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

214 

215 

216# add a new concept to the attracted collection 

217@dropcam_qaqc_bp.post('/attracted') 

218def add_attracted(): 

219 res = requests.post( 

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

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

222 data={ 

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

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

225 }, 

226 ) 

227 if res.status_code == 201: 

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

229 else: 

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

231 return res.json(), res.status_code 

232 

233 

234# update an existing attracted concept 

235@dropcam_qaqc_bp.patch('/attracted/<concept>') 

236def update_attracted(concept): 

237 res = requests.patch( 

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

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

240 data={'attracted': request.values.get('attracted')}, 

241 ) 

242 if res.status_code == 200: 

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

244 return res.json(), res.status_code 

245 

246 

247# delete an attracted concept 

248@dropcam_qaqc_bp.delete('/attracted/<concept>') 

249def delete_attracted(concept): 

250 res = requests.delete( 

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

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

253 ) 

254 if res.status_code == 200: 

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

256 return res.json(), res.status_code