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
« prev ^ index » next coverage.py v7.13.5, created at 2026-03-23 05:22 +0000
1"""
2Endpoints related to external reviewers.
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"""
11import json
12import sys
14import tator
15import requests
16from flask import current_app, flash, redirect, render_template, request, session
18from . import external_review_bp
19from application.vars.annosaurus import Annosaurus
20from application.image_review.external_review.comment_processor import CommentProcessor
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)
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
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
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'))
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')
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