Coverage for application / image_review / external_review / routes.py: 15%

133 statements  

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

1""" 

2Endpoints related to external reviewers. 

3 

4/image-review/external-review [GET] 

5/image-review/external-review/annotation [POST, DELETE] 

6/image-review/external-review/reviewer-list [GET] 

7/image-review/external-review/reviewer [POST] 

8/image-review/external-review/reviewer/<name> [DELETE] 

9""" 

10 

11import json 

12import sys 

13 

14import tator 

15import requests 

16from flask import current_app, flash, redirect, render_template, request, session 

17 

18from . import external_review_bp 

19from application.vars.annosaurus import Annosaurus 

20from application.image_review.external_review.comment_processor import CommentProcessor 

21 

22 

23# displays comments in the external review db 

24@external_review_bp.get('') 

25def get_external_review(): 

26 comments = [] 

27 unread_comments = 0 

28 read_comments = 0 

29 total_comments = 0 

30 if 'tator_token' in session.keys(): 

31 # verify we're logged in 

32 try: 

33 tator.get_api( 

34 host=current_app.config.get('TATOR_URL'), 

35 token=session['tator_token'], 

36 ) 

37 except tator.openapi.tator_openapi.exceptions.ApiException: 

38 flash('Error connecting to Tator', 'danger') 

39 return redirect('/') 

40 try: 

41 print('Fetching external comments...', end='') 

42 sys.stdout.flush() 

43 with requests.get( 

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

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

46 ) as stats_res: 

47 stats_json = stats_res.json() 

48 unread_comments = stats_json['unread_comments'] 

49 read_comments = stats_json['read_comments'] 

50 total_comments = stats_json['total_comments'] 

51 # get a list of comments from external review db 

52 if request.args.get('reviewer'): 

53 query = '' 

54 if request.args.get('read'): 

55 query += '?read=true' 

56 elif request.args.get('unread'): 

57 query += '?unread=true' 

58 comments_res = requests.get( 

59 url=f'{current_app.config.get("DARC_REVIEW_URL")}/comment/reviewer/{request.args.get("reviewer")}{query}', 

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

61 ) 

62 comments_json = comments_res.json() 

63 comments = comments_json['comments'] 

64 unread_comments = comments_json['unread_comments'] 

65 read_comments = comments_json['read_comments'] 

66 total_comments = comments_json['total_comments'] 

67 elif request.args.get('unread'): 

68 unread_comments_res = requests.get( 

69 url=f'{current_app.config.get("DARC_REVIEW_URL")}/comment/unread', 

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

71 ) 

72 comments = unread_comments_res.json() 

73 elif request.args.get('read'): 

74 read_comments_res = requests.get( 

75 url=f'{current_app.config.get("DARC_REVIEW_URL")}/comment/read', 

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

77 ) 

78 comments = read_comments_res.json() 

79 else: 

80 all_comments_res = requests.get( 

81 url=f'{current_app.config.get("DARC_REVIEW_URL")}/comment/all', 

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

83 ) 

84 comments = all_comments_res.json() 

85 print('fetched!') 

86 image_ref_res = requests.get(f'{current_app.config.get("DARC_REVIEW_URL")}/image-reference/quick') 

87 if image_ref_res.status_code != 200: 

88 raise requests.exceptions.ConnectionError 

89 image_refs = image_ref_res.json() 

90 except requests.exceptions.ConnectionError: 

91 image_refs = [] 

92 print('\nERROR: unable to connect to external review server\n') 

93 comment_loader = CommentProcessor( 

94 comments=comments, 

95 annosaurus_url=current_app.config.get('VARS_ANNOSAURUS_URL'), 

96 vars_kb_url=current_app.config.get("VARS_KNOWLEDGE_BASE_URL"), 

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

98 tator_token=session.get('tator_token'), 

99 ) 

100 if len(comment_loader.distilled_records) < 1: 

101 if request.args.get('unread'): 

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

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

104 data = { 

105 'annotations': comment_loader.distilled_records, 

106 'title': f'External Review {"(" + request.args.get("reviewer") + ")" if request.args.get("reviewer") else ""}', 

107 'tab_title': f'External Review {"(" + request.args.get("reviewer") + ")" if request.args.get("reviewer") else ""}', 

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

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

110 'comments': comments, 

111 'missing_records': comment_loader.missing_records, 

112 'unread_comment_count': unread_comments, 

113 'read_comment_count': read_comments, 

114 'total_comment_count': total_comments, 

115 'image_refs': image_refs, 

116 } 

117 return render_template('/image_review/image-review.html', data=data) 

118 

119 

120# adds an annotation for review/updates the reviewer for an annotation 

121@external_review_bp.post('/annotation') 

122def add_external_review(): 

123 def add_vars_or_tator_comment(status_code): 

124 if not request.values.get('all_localizations'): # VARS annotation, update VARS comment 

125 annosaurus = Annosaurus(current_app.config.get('VARS_ANNOSAURUS_URL')) 

126 annosaurus.update_annotation_comment( 

127 observation_uuid=request.values.get('observation_uuid'), 

128 reviewers=json.loads(request.values.get('reviewers')), 

129 client_secret=current_app.config.get('ANNOSAURUS_CLIENT_SECRET') 

130 ) 

131 else: # Tator localization, update Tator notes 

132 api = tator.get_api( 

133 host=current_app.config.get('TATOR_URL'), 

134 token=session.get('tator_token'), 

135 ) 

136 current_notes = api.get_localization_by_elemental_id( 

137 version=json.loads(request.values.get('all_localizations'))[0].get('version', 45), 

138 elemental_id=request.values.get('observation_uuid'), 

139 ).attributes.get('Notes', '').split('|') 

140 current_notes = [note for note in current_notes if 'send to' not in note.lower()] # get rid of 'send to expert' notes 

141 current_notes = [note for note in current_notes if 'added for review' not in note.lower()] # get rid of old 'added for review' notes 

142 current_notes = '|'.join(current_notes) 

143 new_notes = f'{current_notes + "|" if current_notes else ""}Added for review: {", ".join(json.loads(request.values.get("reviewers")))}' 

144 api.update_localization_by_elemental_id( 

145 version=json.loads(request.values.get('all_localizations'))[0].get('version', 45), 

146 elemental_id=request.values.get('observation_uuid'), 

147 localization_update=tator.models.LocalizationUpdate( 

148 attributes={'Notes': new_notes}, 

149 ) 

150 ) 

151 return {}, status_code 

152 data = { 

153 'uuid': request.values.get('observation_uuid'), 

154 'all_localizations': request.values.get('all_localizations'), 

155 'section_id': request.values.get('section_id'), 

156 'sequence': request.values.get('sequence'), 

157 'timestamp': request.values.get('timestamp'), 

158 'reviewers': request.values.get('reviewers'), 

159 'image_url': request.values.get('image_url'), 

160 'video_url': request.values.get('video_url'), 

161 'annotator': request.values.get('annotator'), 

162 'depth': request.values.get('depth'), 

163 'lat': request.values.get('lat'), 

164 'long': request.values.get('long'), 

165 'temperature': request.values.get('temperature'), 

166 'oxygen_ml_l': request.values.get('oxygen_ml_l'), 

167 } 

168 with requests.post( 

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

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

171 data=data, 

172 ) as post_comment_res: 

173 if post_comment_res.status_code == 409: # comment already exists in the db, update record 

174 put_comment_res = requests.put( 

175 url=f'{current_app.config.get("DARC_REVIEW_URL")}/comment/reviewers/{data["uuid"]}', 

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

177 data=data, 

178 ) 

179 if put_comment_res.status_code == 200: 

180 return add_vars_or_tator_comment(200) 

181 elif post_comment_res.status_code == 201: # comment added to db, update VARS "comment" field 

182 return add_vars_or_tator_comment(201) 

183 return {}, 500 

184 

185 

186# deletes an item from the external review db 

187@external_review_bp.delete('/annotation') 

188def delete_external_review(): 

189 delete_comment_res = requests.delete( 

190 url=f'{current_app.config.get("DARC_REVIEW_URL")}/comment/{request.values.get("uuid")}', 

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

192 ) 

193 if delete_comment_res.status_code == 200: 

194 if request.values.get('tator') and request.values.get('tator') == 'true': # tator localization 

195 api = tator.get_api( 

196 host=current_app.config.get('TATOR_URL'), 

197 token=session.get('tator_token'), 

198 ) 

199 current_notes = api.get_localization_by_elemental_id( 

200 version=request.values.get('tator_version'), 

201 elemental_id=request.values.get('uuid'), 

202 ).attributes.get('Notes', '').split('|') 

203 current_notes = [note for note in current_notes if 'send to' not in note.lower()] # get rid of 'send to expert' notes 

204 current_notes = [note for note in current_notes if 'added for review' not in note.lower()] # get rid of old 'added for review' notes 

205 api.update_localization_by_elemental_id( 

206 version=request.values.get('tator_version'), 

207 elemental_id=request.values.get('uuid'), 

208 localization_update=tator.models.LocalizationUpdate( 

209 attributes={'Notes': '|'.join(current_notes)}, 

210 ) 

211 ) 

212 else: # VARS annotation 

213 annosaurus = Annosaurus(current_app.config.get('VARS_ANNOSAURUS_URL')) 

214 annosaurus.update_annotation_comment( 

215 observation_uuid=request.values.get('uuid'), 

216 reviewers=[], 

217 client_secret=current_app.config.get('ANNOSAURUS_CLIENT_SECRET') 

218 ) 

219 return {}, 200 

220 return {}, 500 

221 

222 

223# displays information about all the reviewers in the hurl db 

224@external_review_bp.get('/reviewer-list') 

225def reviewers(): 

226 return render_template('image_review/external-reviewers.html', reviewers=session.get('reviewers')) 

227 

228 

229# create or update a reviewer's information 

230@external_review_bp.post('/reviewer') 

231def update_reviewer_info(): 

232 success = False 

233 name = request.values.get('ogReviewerName') or 'nobody' 

234 data = { 

235 'new_name': request.values.get('editReviewerName'), 

236 'phylum': request.values.get('editPhylum'), 

237 'focus': request.values.get('editFocus'), 

238 'organization': request.values.get('editOrganization'), 

239 'email': request.values.get('editEmail') 

240 } 

241 patch_reviewer_res = requests.patch( 

242 url=f'{current_app.config.get("DARC_REVIEW_URL")}/reviewer/{name}', 

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

244 data=data, 

245 ) 

246 if patch_reviewer_res.status_code == 404: 

247 data['name'] = data['new_name'] 

248 post_reviewer_res = requests.post( 

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

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

251 data=data, 

252 ) 

253 if post_reviewer_res.status_code == 201: 

254 success = True 

255 flash('Successfully added reviewer', 'success') 

256 else: 

257 flash(post_reviewer_res.json(), 'danger') 

258 elif patch_reviewer_res.status_code == 200: 

259 success = True 

260 flash('Successfully updated reviewer', 'success') 

261 else: 

262 flash(patch_reviewer_res.json(), 'danger') 

263 if success: 

264 with requests.get( 

265 url=f'{current_app.config.get("DARC_REVIEW_URL")}/reviewer/all', 

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

267 ) as all_reviewers_res: 

268 session['reviewers'] = all_reviewers_res.json() 

269 return redirect('/image-review/external-review/reviewer-list') 

270 

271 

272# delete a reviewer 

273@external_review_bp.delete('/reviewer/<name>') 

274def delete_reviewer(name): 

275 delete_reviewer_res = requests.delete( 

276 url=f'{current_app.config.get("DARC_REVIEW_URL")}/reviewer/{name}', 

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

278 ) 

279 if delete_reviewer_res.status_code == 200: 

280 flash('Successfully deleted reviewer', 'success') 

281 with requests.get( 

282 url=f'{current_app.config.get("DARC_REVIEW_URL")}/reviewer/all', 

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

284 ) as res: 

285 session['reviewers'] = res.json() 

286 else: 

287 flash('Error deleting reviewer', 'danger') 

288 return {}, delete_reviewer_res.status_code