Coverage for application / qaqc / tator / sub / routes.py: 12%
117 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"""
2Sub/transect QA/QC endpoints
4/qaqc/tator/sub/checklist [GET, PATCH]
5/qaqc/tator/sub/check/<check> [GET]
6"""
7import requests
8from flask import current_app, flash, redirect, render_template, request, session
10from application.tator.tator_sub_qaqc_processor import TatorSubQaqcProcessor
11from . import sub_qaqc_bp
12from application.tator.tator_rest_client import TatorRestClient
13from application.tator.tator_type import TatorLocalizationType
14from application.qaqc.tator.util import init_tator_api, get_comments_and_image_refs
17def _get_deployment_info(tator_client: TatorRestClient, section_ids: list[str], transect_ids: list[str]):
18 deployment_names = []
19 expedition_name = None
20 for section_id in section_ids:
21 section = tator_client.get_section_by_id(section_id)
22 deployment_names.append(section['name'])
23 if expedition_name is None:
24 expedition_name = section['path'].split('.')[0]
25 transect_media = [tator_client.get_media_by_id(transect_id) for transect_id in transect_ids]
26 return transect_media, deployment_names, expedition_name
29@sub_qaqc_bp.get('/checklist')
30def sub_qaqc_checklist():
31 project_id = request.args.get('project', type=int)
32 section_ids = request.args.getlist('section')
33 transect_ids = request.args.getlist('transect')
34 if not project_id or not section_ids or not transect_ids:
35 flash('Please select a project, section, and transect', 'info')
36 return redirect('/')
37 tator_client = TatorRestClient(current_app.config.get('TATOR_URL'), session['tator_token'])
38 transect_media, _, expedition_name = _get_deployment_info(
39 tator_client=tator_client,
40 section_ids=section_ids,
41 transect_ids=transect_ids,
42 )
43 media_names = [media['name'] for media in transect_media]
44 localizations = []
45 for i in range(0, len(transect_ids), 300):
46 batch = [int(tid) for tid in transect_ids[i:i + 50]]
47 localizations += tator_client.get_localizations(project_id, media_id=batch)
48 individual_count = sum(1 for loco in localizations if TatorLocalizationType.is_dot(loco['type']))
49 with requests.get(
50 url=f'{current_app.config.get("DARC_REVIEW_URL")}/qaqc-checklist/tator-sub/{"&".join(transect_ids)}',
51 headers=current_app.config.get('DARC_REVIEW_HEADERS'),
52 ) as checklist_res:
53 if checklist_res.status_code == 200:
54 checklist = checklist_res.json()
55 else:
56 print('ERROR: Unable to get QAQC checklist from external review server')
57 checklist = {}
58 data = {
59 'title': expedition_name,
60 'tab_title': media_names[0] if len(media_names) == 1 else expedition_name,
61 'media_names': media_names,
62 'transect_ids': transect_ids,
63 'localization_count': len(localizations),
64 'individual_count': individual_count,
65 'checklist': checklist,
66 }
67 return render_template('qaqc/tator/sub/qaqc-checklist.html', data=data)
70@sub_qaqc_bp.patch('/checklist')
71def patch_sub_qaqc_checklist():
72 req_json = request.json
73 transects = req_json.get('transectIds')
74 if not transects:
75 return {'error': 'transectIds is required'}, 400
76 req_json.pop('transectIds')
77 res = requests.patch(
78 url=f'{current_app.config.get("DARC_REVIEW_URL")}/qaqc-checklist/tator-sub/{transects}',
79 headers=current_app.config.get('DARC_REVIEW_HEADERS'),
80 json=req_json,
81 )
82 return res.json(), res.status_code
84# individual qaqc checks
85@sub_qaqc_bp.get('/check/<check>')
86def sub_qaqc(check):
87 project_id = request.args.get('project', type=int)
88 section_ids = request.args.getlist('section')
89 transect_ids = request.args.getlist('transect')
90 if not project_id or not section_ids or not transect_ids:
91 flash('Please select a project, section, and transect', 'info')
92 return redirect('/')
93 tator_api, err = init_tator_api()
94 if err:
95 return err
96 tator_client = TatorRestClient(current_app.config.get('TATOR_URL'), session['tator_token'])
97 transect_media, deployment_names, expedition_name = _get_deployment_info(
98 tator_client=tator_client,
99 section_ids=section_ids,
100 transect_ids=transect_ids,
101 )
102 media_names = [media['name'] for media in transect_media]
103 comments, image_refs = get_comments_and_image_refs(deployment_names)
104 tab_title = media_names[0] if len(media_names) == 1 else expedition_name
105 data = {
106 'concepts': session.get('vars_concepts', []),
107 'title': check.replace('-', ' ').title(),
108 'tab_title': f'{tab_title} {check.replace("-", " ").title()}',
109 'media_names': media_names,
110 'transect_ids': transect_ids,
111 'reviewers': session.get('reviewers', []),
112 'comments': comments,
113 'image_refs': image_refs,
114 'qaqc_js': 'qaqc.tator_qaqc.sub_qaqc.static',
115 }
116 if check == 'media-attributes':
117 # the one case where we don't want to initialize a TatorSubQaqcProcessor (no need to fetch localizations)
118 data['substrates'] = tator_client.get_substrates_for_medias(project_id, transect_media)
119 data['page_title'] = 'Media attributes'
120 data['media_attributes'] = transect_media
121 return render_template('qaqc/tator/qaqc-tables.html', data=data)
122 qaqc_annos = TatorSubQaqcProcessor(
123 project_id=project_id,
124 section_ids=section_ids,
125 transect_media_ids=[int(transect_id) for transect_id in transect_ids],
126 api=tator_api,
127 darc_review_url=current_app.config.get('DARC_REVIEW_URL'),
128 tator_url=current_app.config.get('TATOR_URL'),
129 )
130 qaqc_annos.fetch_localizations()
131 match check:
132 case 'names-accepted':
133 qaqc_annos.check_names_accepted()
134 data['page_title'] = 'Scientific names/tentative IDs not accepted in WoRMS'
135 case 'missing-qualifier':
136 qaqc_annos.check_missing_qualifier()
137 data['page_title'] = 'Records classified higher than species missing qualifier'
138 case 'stet-missing-reason':
139 qaqc_annos.check_stet_reason()
140 data['page_title'] = 'Records with a qualifier of \'stet\' missing \'Reason\''
141 case 'missing-ancillary-data':
142 qaqc_annos.check_missing_ancillary_data()
143 data['page_title'] = 'Records missing ancillary data'
144 case 'missing-upon':
145 qaqc_annos.check_missing_upon_and_not_fish()
146 data['page_title'] = 'Records missing upon and not a fish'
147 case 'upon-not-substrate':
148 qaqc_annos.check_upons_are_current_substrate_or_previous_animal(transect_media=transect_media)
149 data['page_title'] = 'Records where upon is not the current substrate or an animal that was previously recorded'
150 case 'suspicious-hosts':
151 qaqc_annos.get_suspicious_records()
152 data['page_title'] = 'Records with a suspicious upon (host upon itself)'
153 case 'host-associate-time-diff':
154 qaqc_annos.find_long_host_associate_time_diff()
155 data['page_title'] = 'Records where host recorded more than one minute ago or cannot be found'
156 case 'all-tentative-ids':
157 qaqc_annos.get_all_tentative_ids_and_morphospecies()
158 data['page_title'] = 'Records with a tentative ID or morphospecies'
159 data['subtitle'] = '(also checks phylogeny vs. scientific name)'
160 case 'notes-and-remarks':
161 qaqc_annos.get_all_notes_and_remarks()
162 data['page_title'] = 'Records with notes and/or remarks'
163 case 're-examined':
164 qaqc_annos.get_re_examined()
165 data['page_title'] = 'Records marked "to be re-examined"'
167 case 'unique-taxa':
168 qaqc_annos.get_unique_taxa()
169 data['page_title'] = 'All unique taxa'
170 data['unique_taxa'] = qaqc_annos.final_records
171 return render_template('qaqc/tator/qaqc-tables.html', data=data)
173 case _:
174 return render_template('errors/404.html', err=''), 404
175 data['annotations'] = qaqc_annos.final_records
176 return render_template('qaqc/tator/qaqc.html', data=data)