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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-23 05:22 +0000
1"""
2Dropcam (dscm) QA/QC endpoints
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
12import requests
13from flask import current_app, flash, redirect, render_template, request, send_file, session
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
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
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)
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
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)
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
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
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
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