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
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-07 06:46 +0000
1"""
2Tator-specific QA/QC endpoints
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"""
11from io import BytesIO
13import tator
14import requests
15from flask import current_app, flash, redirect, render_template, request, send_file, session
17from . import tator_qaqc_bp
18from .tator_qaqc_processor import TatorQaqcProcessor
19from ...util.tator_localization_type import TatorLocalizationType
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)
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
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,
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)
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
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
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
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