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

1""" 

2Sub/transect QA/QC endpoints 

3 

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 

9 

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 

15 

16 

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 

27 

28 

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) 

68 

69 

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 

83 

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"' 

166 

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) 

172 

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)