"""
Prompt experimenter for reason for marking session/insertion as CRITICAL
Choices are listed in the global variables. Multiple reasons can be selected.
Places info in Alyx session note in a format that is machine retrievable (text->json)
"""
# Author: Gaelle
import json
from one.api import ONE
# Global var
# Reasons for marking a session as critical
REASONS_SESS_CRIT = (
'within experiment system crash',
'synching impossible',
'dud or mock session',
'essential dataset missing',
'Other'
)
# Reasons for marking an insertion as critical
# Note: Split the reasons labelled in the GUI versus those to be seen
# when marking the insertion programmatically
REASONS_INS_CRIT_GUI = (
'Noise and artifact',
'Drift',
'Poor neural yield',
'Brain Damage',
'Other'
)
REASONS_INS_CRIT = tuple(['Histological images missing',
'Track not visible on imaging data']
+ list(REASONS_INS_CRIT_GUI))
# Util functions
def _create_note_str(ins_or_sess):
"""
:param ins_or_sess: str containing either 'insertion' or 'session'
:return:
"""
str_static = f'=== EXPERIMENTER REASON(S) FOR MARKING THE ' \
f'{ins_or_sess.upper()} AS CRITICAL ==='
return str_static
def _reason_addnumberstr(reason_list):
"""
Adding number at the beginning of the str for ease of selection by user
:param reason_list : list of str ; default: None
"""
return [f'{i}) {r}' for i, r in enumerate(reason_list)]
REASONS_SESS_WITH_NUMBERS = _reason_addnumberstr(reason_list=REASONS_SESS_CRIT)
REASONS_INS_WITH_NUMBERS = _reason_addnumberstr(reason_list=REASONS_INS_CRIT)
def _reason_question_prompt(reason_list, reasons_with_numbers, ins_or_sess):
"""
Function asking the user to enter the criteria for marking a session/insertion as CRITICAL.
:param reason_list: list of reasons (str)
:param reasons_with_numbers: list of reasons (str), with added number in front
:param ins_or_sess: str containing either 'insertion' or 'session'
"""
prompt = f'Select from this list the reason(s) why you are marking the ' \
f'{ins_or_sess} as CRITICAL:' \
f' \n {reasons_with_numbers} \n' \
f'and enter the corresponding numbers separated by commas, e.g. 1,3 -> enter: '
ans = input(prompt).strip().lower()
# turn str into numbers
string_list = ans.split(',')
try: # try-except inc ase users enters something else than a number
integer_map = map(int, string_list)
integer_list = list(integer_map)
except ValueError:
print(f'{ans} is invalid, please try again...')
return _reason_question_prompt()
if all(elem in range(0, len(reasons_with_numbers)) for elem in integer_list):
reasons_out = [reason_list[integer_n] for integer_n in integer_list]
print(f'You selected reason(s): {reasons_out}')
return reasons_out
else:
print(f'{ans} is invalid, please try again...')
return _reason_question_prompt()
def _enquire_why_other():
prompt = 'Explain why you selected "other" (free text): '
ans = input(prompt).strip().lower()
return ans
def _create_note_json(reasons_selected, reason_for_other, note_title):
note_session = {
"title": note_title,
"reasons_selected": reasons_selected,
"reason_for_other": reason_for_other
}
return json.dumps(note_session)
def _delete_note_yesno(notes):
"""
Function asking user whether notes are to be deleted.
:param notes: Alyx notes, from ONE query
:return: y / n (string)
"""
prompt = f'You are about to delete {len(notes)} existing notes; ' \
f'do you want to proceed? y/n: '
ans = input(prompt).strip().lower()
if ans not in ['y', 'n']:
print(f'{ans} is invalid, please try again...')
return _delete_note_yesno()
else:
return ans
def _upload_note_alyx(eid, note_text, content_type, str_notes_static, one=None, overwrite=False):
"""
Function to upload a note to Alyx.
It will check if notes with STR_NOTES_STATIC already exists for this session,
and ask if OK to overwrite.
:param eid: session or isnertion eid
:param note_text: text to enter within the note object
:param one: default: None -> ONE()
:param str_notes_static: string within the notes that will be searched for
:param content_type: 'session' or 'insertion'
:param overwrite: if set to False, will check whether other notes exists and ask
if deleting is OK.
If set to True, will delete any previous note without asking.
:return:
"""
if one is None:
one = ONE()
my_note = {'user': one.alyx.user,
'content_type': content_type,
'object_id': eid,
'text': f'{note_text}'}
# check if such a note already exists, ask if OK to overwrite
notes = one.alyx.rest('notes', 'list',
django=f'text__icontains,{str_notes_static},object_id,{eid}',
no_cache=True)
if len(notes) == 0:
one.alyx.rest('notes', 'create', data=my_note)
print('The selected reasons were saved on Alyx.')
else:
if overwrite:
ans = 'y'
else:
ans = _delete_note_yesno(notes=notes)
if ans == 'y':
for note in notes:
one.alyx.rest('notes', 'delete', id=note['id'])
one.alyx.rest('notes', 'create', data=my_note)
print('The selected reasons were saved on Alyx; old notes were deleted')
else:
print('The selected reasons were NOT saved on Alyx; old notes remain.')
[docs]def main_gui(eid, reasons_selected, one=None):
"""
Main function to call to input a reason for marking an insertion as
CRITICAL from the alignment GUI. It will:
- create note text, after deleting any similar notes existing already
:param: eid: insertion id
:param: reasons_selected: list of str, str are picked within REASONS_INS_CRIT_GUI
"""
# hit the database to check if eid is insertion eid
ins_list = one.alyx.rest('insertions', 'list', id=eid, no_cache=True)
if len(ins_list) != 1:
raise ValueError(f'N={len(ins_list)} insertion found, expected N=1. Check eid provided.')
# assert that reasons are all within REASONS_INS_CRIT_GUI
for item_str in reasons_selected:
assert item_str in REASONS_INS_CRIT_GUI
# create note title and text
note_title = _create_note_str('insertion')
note_text = _create_note_json(reasons_selected=reasons_selected,
reason_for_other=[],
note_title=note_title)
# upload note to Alyx
_upload_note_alyx(eid, note_text, content_type='probeinsertion',
str_notes_static=note_title, one=one, overwrite=True)
[docs]def main(eid, one=None):
"""
Main function to call to input a reason for marking a session/insertion
as CRITICAL programmatically. It will:
- ask reasons for selection of critical status
- check if 'other' reason has been selected, inquire why (free text)
- create note text, checking whether similar notes exist already
- upload note to Alyx if none exist previously or if overwrite is chosen
Q&A are prompted via the Python terminal.
Example:
# Retrieve Alyx note to test
one = ONE(base_url='https://dev.alyx.internationalbrainlab.org')
eid = '2ffd3ed5-477e-4153-9af7-7fdad3c6946b'
main(eid=eid, one=one)
# Get notes with pattern
notes = one.alyx.rest('notes', 'list',
django=f'text__icontains,{STR_NOTES_STATIC},'
f'object_id,{eid}')
test_json_read = json.loads(notes[0]['text'])
:param eid: session/insertion eid
:param one: default: None -> ONE()
:return:
"""
if one is None:
one = ONE()
# ask reasons for selection of critical status
# hit the database to know if eid is insertion or session eid
sess_list = one.alyx.get('/sessions?&django=pk,' + eid, clobber=True)
ins_list = one.alyx.get('/insertions?&django=pk,' + eid, clobber=True)
if len(sess_list) > 0 and len(ins_list) == 0: # session
reason_list = REASONS_SESS_CRIT
reasons_with_numbers = REASONS_SESS_WITH_NUMBERS
ins_or_sess = 'session'
content_type = 'session'
elif len(ins_list) > 0 and len(sess_list) == 0: # insertion
reason_list = REASONS_INS_CRIT
reasons_with_numbers = REASONS_INS_WITH_NUMBERS
content_type = 'probeinsertion'
ins_or_sess = 'insertion'
else:
raise ValueError(f'Inadequate number of session (n={len(sess_list)}) '
f'or insertion (n={len(ins_list)}) found for eid {eid}.'
f'The query output should be of length 1.')
reasons_selected = _reason_question_prompt(reason_list=reason_list,
reasons_with_numbers=reasons_with_numbers,
ins_or_sess=ins_or_sess)
# check if 'other' reason has been selected, inquire why
if 'Other' in reasons_selected:
reason_for_other = _enquire_why_other()
else:
reason_for_other = []
# create note title and text
note_title = _create_note_str(ins_or_sess)
note_text = _create_note_json(reasons_selected=reasons_selected,
reason_for_other=reason_for_other,
note_title=note_title)
# upload note to Alyx
_upload_note_alyx(eid, note_text, content_type=content_type,
str_notes_static=note_title, one=one)