diff --git a/config.py b/config.py index 5467d91..92ae205 100644 --- a/config.py +++ b/config.py @@ -6,8 +6,12 @@ CURRENT_OS: str = platform.system() API_BASE_URL: str = 'https://mlc.nimh.nih.gov/cogmood' +#API_BASE_URL='http://127.0.0.1:5000' +API_SALT: str = 'SALT' VERIFY: bool = False RUNNING_FROM_EXECUTABLE: bool = getattr(sys, 'frozen', False) and hasattr(sys, '_MEIPASS') +# WORKER_ID_SOURCE valid values = ['USER', 'EXECUTABLE'] +WORKER_ID_SOURCE = "USER" # WORKER_ID_PLACEHOLDER_VALUE is the placeholder value assigned to the WorkerID field # in the executables when we build them. It should be replaced by actual ID when # the executable is prepared for distribution. diff --git a/custom_startup.py b/custom_startup.py new file mode 100644 index 0000000..08de4e2 --- /dev/null +++ b/custom_startup.py @@ -0,0 +1,86 @@ +from smile.common import Experiment, Log, Wait, Func, UntilDone, \ + Label, Loop, If, Elif, Else, KeyPress, Ref, \ + Parallel, Slider, Serial, UpdateWidget, Debug, Meanwhile, While, Subroutine +from smile.video import Rectangle, TextInput, Button, ButtonPress +from smile.mouse import MouseCursor +from smile.startup import ( + INFO_WIDTH, + INFO_HEIGHT, + INFO_OUTLINE_COLOR, + INFO_COLOR, + INFO_FONT_SIZE, + INFO_BUTTON_HEIGHT, + INFO_BUTTON_WIDTH, + TEXT_INPUT_WIDTH, + TEXT_INPUT_HEIGHT +) +from smile.scale import scale as s +from hashlib import blake2b + +import config as CogBatt_config + +def _validate_code(exp): + worker_id = exp._subject + code = exp.get_var('_code') + expected_code = blake2b(worker_id.encode(), digest_size=4, salt=CogBatt_config.API_SALT.encode()).hexdigest()[:4] + Debug(code=code, expected_code=expected_code, invalid=code!=expected_code) + exp.set_var('code_invalid', code != expected_code) + +@Subroutine +def InputSubject(self): + with Parallel(): + with Parallel(blocking=False): + MouseCursor() + recOut = Rectangle(width=s(INFO_WIDTH) + s(20), + height=s(INFO_HEIGHT) + s(20), + color=INFO_OUTLINE_COLOR) + recin = Rectangle(width=s(INFO_WIDTH), + height=s(INFO_HEIGHT), + color=INFO_COLOR) + lbl = Label(text=CogBatt_config.EXP_NAME, center_x=recin.center_x, + top=recin.top - s(10), + font_size=s(INFO_FONT_SIZE)) + idIn = TextInput(width=s(TEXT_INPUT_WIDTH), + height=s(TEXT_INPUT_HEIGHT), + font_size=s(INFO_FONT_SIZE), + center_x=recin.center_x, + top=lbl.bottom - s(20), + multiline=False, + text="", + disabled=False, + hint_text="Prolific Worker ID", + write_tab=False) + codeIn = TextInput(width=s(TEXT_INPUT_WIDTH), + height=s(TEXT_INPUT_HEIGHT), + font_size=s(INFO_FONT_SIZE), + center_x=recin.center_x, + top=lbl.bottom - s(80), + multiline=False, + text="", + disabled=False, + hint_text="4 digit task code", + write_tab=False) + bc = Button(text="Continue", font_size=s(INFO_FONT_SIZE), + height=s(INFO_BUTTON_HEIGHT), + width=s(INFO_BUTTON_WIDTH), + right=recin.right - s(20), + bottom=recin.bottom + s(20), + name="C", + background_normal="", + background_color=INFO_OUTLINE_COLOR, + disabled=True) + with Serial(): + with While( + (Ref.object(codeIn.text).__len__() < 4) + or (Ref(str, idIn.text) == '') + ): + Wait(0.1) + bc.disabled = False + + bp = ButtonPress(buttons=[bc]) + with If( + (bp.pressed == "C") + ): + Func(self.exp._change_smile_subj, Ref.object(idIn.text).lower().strip()) + Func(self.exp.set_var, '_code', Ref.object(codeIn.text).lower().strip()) + Func(_validate_code, Ref.object(self.exp)) \ No newline at end of file diff --git a/main.py b/main.py index 36f474e..c583250 100644 --- a/main.py +++ b/main.py @@ -4,9 +4,9 @@ # Smile imports from smile.common import Experiment, Log, Wait, Func, UntilDone, \ Label, Loop, If, Elif, Else, KeyPress, Ref, \ - Parallel, Slider, Serial, UpdateWidget, Debug, Meanwhile -from smile.clock import clock + Parallel, Slider, Serial, UpdateWidget, Debug, Meanwhile, While from smile.scale import scale as s +from custom_startup import InputSubject # from android.permissions import request_permissions, Permission # request_permissions([Permission.READ_EXTERNAL_STORAGE, Permission.WRITE_EXTERNAL_STORAGE]) from kivy.resources import resource_add_path @@ -64,11 +64,6 @@ WRK_DIR = sys._MEIPASS resource_add_path(WRK_DIR) -retrieved_worker_id = retrieve_worker_id() - -tasks_from_api = get_blocks_to_run(retrieved_worker_id['content']) -number_of_tasks = 0 if tasks_from_api['status'] == 'error' else len(tasks_from_api['content']) - # Initialize the SMILE experiment. exp = Experiment(name=CogBatt_config.EXP_NAME, @@ -77,9 +72,27 @@ Touch=False, local_crashlog=True, cmd_traceback=False, data_dir=WRK_DIR, working_dir=WRK_DIR) +exp._code = '' +if CogBatt_config.WORKER_ID_SOURCE == 'EXECUTABLE': + retrieved_worker_id = retrieve_worker_id() + tasks_from_api = get_blocks_to_run(retrieved_worker_id['content']) + number_of_tasks = 0 if tasks_from_api['status'] == 'error' else len(tasks_from_api['content']) + + exp.tasks_from_api = tasks_from_api + exp.worker_id_dict = retrieved_worker_id +elif CogBatt_config.WORKER_ID_SOURCE == 'USER': + InputSubject() + tasks_from_api = Func(get_blocks_to_run, Ref.object(exp)._subject, Ref.object(exp).get_var('_code')).result + with If(tasks_from_api['status'] == 'error'): + number_of_tasks = 0 + with Else(): + number_of_tasks = tasks_from_api['content'].__len__() + exp.tasks_from_api = tasks_from_api + exp.worker_id_dict = {"status": "success", "content": Ref.object(exp)._subject} +else: + raise NotImplementedError + -exp.tasks_from_api = tasks_from_api -exp.worker_id_dict = retrieved_worker_id with Parallel(): with Serial(blocking=False): @@ -89,8 +102,13 @@ author=version.__author__, date_time=version.__date__, email=version.__email__) - Wait(.5) + with If(Ref.object(exp).get_var('code_invalid')): + error_screen(error='Invalid task code: ' + Ref(str, exp._code), + message='You entered an incorrect task code, please double check the code ' + 'listed on the website and try again. If it still does not work ' + 'please contact Dylan Nielson at Dylan.Nielson@nih.gov.' + ) with If(CogBatt_config.RUNNING_FROM_EXECUTABLE): # Handles case where retrieval of worker id fails @@ -196,7 +214,8 @@ block_name=exp.task_name + '_' + Ref(str, exp.block_number), data_directory=Ref.object( exp)._session_dir, - slog_file_name='log_'+exp.task_name+'_'+'0.slog') + slog_file_name='log_'+exp.task_name+'_'+'0.slog', + code=Ref.object(exp).get_var('_code')) Wait(3) # Error screen for failed upload diff --git a/utils.py b/utils.py index 1d5677a..262a040 100644 --- a/utils.py +++ b/utils.py @@ -4,7 +4,7 @@ import requests import logging import time -from config import API_BASE_URL, RUNNING_FROM_EXECUTABLE, CURRENT_OS, VERIFY +from config import API_BASE_URL, RUNNING_FROM_EXECUTABLE, CURRENT_OS, VERIFY, WORKER_ID_SOURCE from hashlib import blake2b from io import BytesIO from pathlib import Path @@ -17,18 +17,22 @@ format='%(asctime)s - %(levelname)s - %(message)s') -def get_blocks_to_run(worker_id: str) -> list[str] | dict[str, str]: +def get_blocks_to_run(worker_id: str, code: str | None = None) -> list[str] | dict[str, str]: """ Sends a GET request to retrieve the list of blocks that are yet to be run by the worker. Args: worker_id (str): The unique identifier for the worker whose blocks are being fetched. + code (str): Optional verifcation code if worker_id_source is user Returns: dict: A dictionary with 'status' as success or error, and 'content' containing either the blocks list or an error message. """ url = f'{API_BASE_URL}/taskcontrol' - params = {'worker_id': worker_id} + if WORKER_ID_SOURCE == 'EXECUTABLE': + params = {'worker_id': worker_id} + elif WORKER_ID_SOURCE == 'USER': + params = {'worker_id': worker_id, 'code':code} try: response = requests.get(url, params=params, verify=VERIFY) @@ -78,7 +82,7 @@ def hash_file(file_obj): return hash_blake.hexdigest() -def upload_block(worker_id: str, block_name: str, data_directory: str, slog_file_name: str) -> dict[str, str]: +def upload_block(worker_id: str, block_name: str, data_directory: str, slog_file_name: str, code: str | None = None) -> dict[str, str]: """ Sends a POST request to upload a completed block along with its checksum and the associated zipped file. Uses the config.API_BASE_URL to build the URL. @@ -88,6 +92,7 @@ def upload_block(worker_id: str, block_name: str, data_directory: str, slog_file block_name (str): The name of the block being uploaded, typically in the format "taskname_runnumber". data_directory (str): The directory where the slog file is located. slog_file_name (str): The name of the slog file. + code (str): Optional verifcation code if worker_id_source is user. Behavior: - Zips the slog file. @@ -119,7 +124,10 @@ def upload_block(worker_id: str, block_name: str, data_directory: str, slog_file current_time_ms = int(time.time() * 1000) zip_file_name_with_timestamp = f'{block_name}_{current_time_ms}.zip' - params = {'worker_id': worker_id} + if WORKER_ID_SOURCE == 'EXECUTABLE': + params = {'worker_id': worker_id} + elif WORKER_ID_SOURCE == 'USER': + params = {'worker_id': worker_id, 'code': code} data = {'block_name': block_name, 'checksum': checksum} files = {'file': (zip_file_name_with_timestamp, zip_buffer, 'application/zip')} @@ -243,6 +251,7 @@ def _read_exe_worker_id() -> dict[str, str]: return {"status": "error", "content": str(e)} + if __name__ == "__main__": upload_block(worker_id='123456', block_name='flkr_1',