Skip to content

Commit

Permalink
Nivturk v1.2 mturk (#95)
Browse files Browse the repository at this point in the history
* nivturk-v1.2-mturk (1)
- initial commit; pulled prolific branch into mturk
- integrated changes from #94

* nivturk-v1.2-mturk (2)
- fix code duplication error
- fix redirect bug
  • Loading branch information
szorowi1 authored Jun 3, 2022
1 parent ca2f60e commit 7f0e839
Show file tree
Hide file tree
Showing 121 changed files with 80,850 additions and 15,645 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,5 @@ build/
# project specific
data/**
metadata/**
incomplete/**
reject/**
24 changes: 10 additions & 14 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,27 @@
.. image:: https://img.shields.io/github/license/mashape/apistatus.svg
:target: https://github.com/nivlab/NivLink/blob/master/LICENSE

.. image:: https://zenodo.org/badge/182183266.svg
:target: https://zenodo.org/badge/latestdoi/182183266

NivTurk
=======

Niv lab tools for securely serving and storing data from online computational psychiatry experiments.

Quickstart
^^^^^^^^^^

The following is the minimal set of commands needed to get started with NivTurk (assuming you have already a virtual machine with python 3.6+ installed):

.. code-block:: bash
ssh <user-name>@<server-name>.princeton.edu
git clone https://github.com/nivlab/nivturk.git
cd nivturk
pip install -r requirements.txt
gunicorn -b 0.0.0.0:9000 -w 4 app:app
Documentation
^^^^^^^^^^^^^

For details on how to serve your experiment, how the code is organized, and how data is stored, please see the
`Documentation <https://nivlab.github.io/nivturk>`_.

Citation
^^^^^^^^

If you use this library in academic work, please cite the following:

| Samuel Zorowitz & Daniel Bennett. (2022). NivTurk (v1.2-prolific). Zenodo. https://doi.org/10.5281/zenodo.6609218
Acknowledgements
^^^^^^^^^^^^^^^^
NivTurk was developed with support from the National Center for Advancing Translational Sciences (NCATS), a component of the National Institute of Health (NIH), under award number UL1TR003017.
94 changes: 49 additions & 45 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import os, sys, configparser, warnings
import os, sys, re, configparser, warnings
from flask import (Flask, redirect, render_template, request, session, url_for)
from app import consent, alert, experiment, complete, error
from .io import write_metadata
from .utils import gen_code
__version__ = '1.1'
__version__ = '1.2'

## Define root directory.
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
Expand All @@ -17,6 +17,8 @@
if not os.path.isdir(data_dir): os.makedirs(data_dir)
meta_dir = os.path.join(ROOT_DIR, cfg['IO']['METADATA'])
if not os.path.isdir(meta_dir): os.makedirs(meta_dir)
incomplete_dir = os.path.join(ROOT_DIR, cfg['IO']['INCOMPLETE'])
if not os.path.isdir(incomplete_dir): os.makedirs(incomplete_dir)
reject_dir = os.path.join(ROOT_DIR, cfg['IO']['REJECT'])
if not os.path.isdir(reject_dir): os.makedirs(reject_dir)

Expand All @@ -30,6 +32,9 @@
if secret_key == "PLEASE_CHANGE_THIS":
warnings.warn("WARNING: Flask password is currently default. This should be changed prior to production.")

## Check restart mode; if true, participants can restart experiment.
allow_restart = cfg['FLASK'].getboolean('ALLOW_RESTART')

## Initialize Flask application.
app = Flask(__name__)
app.secret_key = secret_key
Expand All @@ -52,7 +57,9 @@ def index():
## Store directories in session object.
session['data'] = data_dir
session['metadata'] = meta_dir
session['incomplete'] = incomplete_dir
session['reject'] = reject_dir
session['allow_restart'] = allow_restart

## Record incoming metadata.
info = dict(
Expand All @@ -72,47 +79,23 @@ def index():
version = request.user_agent.version, # User metadata
)

## Case 1: workerId absent.
## Case 1: workerId absent form URL.
if info['workerId'] is None:

## Redirect participant to error (missing workerId).
return redirect(url_for('error.error', errornum=1000))

## Case 2: mobile user.
## Case 2: mobile / tablet user.
elif info['platform'] in ['android','iphone','ipad','wii']:

## Redirect participant to error (platform error).
return redirect(url_for('error.error', errornum=1001))

## Case 3: repeat visit, preexisting log but no session data.
elif not 'workerId' in session and info['workerId'] in os.listdir(meta_dir):

## Consult log file.
with open(os.path.join(session['metadata'], info['workerId']),'r') as f:
logs = f.read()

## Case 3a: previously started experiment.
if 'experiment' in logs:

## Update metadata.
session['workerId'] = info['workerId']
session['ERROR'] = '1004: Suspected incognito user.'
session['complete'] = 'error'
write_metadata(session, ['ERROR','complete'], 'a')

## Redirect participant to error (previous participation).
return redirect(url_for('error.error', errornum=1004))

## Case 3b: no previous experiment starts.
else:

## Update metadata.
for k, v in info.items(): session[k] = v
session['WARNING'] = "Assigned new subId."
write_metadata(session, ['subId','WARNING'], 'a')
## Case 3: previous complete.
elif 'complete' in session:

## Redirect participant to consent form.
return redirect(url_for('consent.consent'))
## Redirect participant to complete page.
return redirect(url_for('complete.complete'))

## Case 4: repeat visit, manually changed workerId.
elif 'workerId' in session and session['workerId'] != info['workerId']:
Expand All @@ -125,25 +108,46 @@ def index():
## Redirect participant to error (unusual activity).
return redirect(url_for('error.error', errornum=1005))

## Case 5: repeat visit, previously completed experiment.
elif 'complete' in session:
## Case 5: repeat visit, preexisting activity.
elif 'workerId' in session:

## Update metadata.
session['WARNING'] = "Revisited home."
write_metadata(session, ['WARNING'], 'a')
## Redirect participant to consent form.
return redirect(url_for('consent.consent'))

## Redirect participant to complete page.
return redirect(url_for('complete.complete'))
## Case 6: repeat visit, preexisting log but no session data.
elif not 'workerId' in session and info['workerId'] in os.listdir(meta_dir):

## Case 6: repeat visit, preexisting activity.
elif 'workerId' in session:
## Parse log file.
with open(os.path.join(session['metadata'], info['workerId']), 'r') as f:
logs = f.read()

## Extract subject ID.
info['subId'] = re.search('subId\t(.*)\n', logs).group(1)

## Check for previous consent.
consent = re.search('consent\t(.*)\n', logs)
if consent and consent.group(1) == 'True': info['consent'] = True # consent = true
elif consent and consent.group(1) == 'False': info['consent'] = False # consent = false
elif consent: info['consent'] = consent.group(1) # consent = bot

## Check for previous experiment.
experiment = re.search('experiment\t(.*)\n', logs)
if experiment: info['experiment'] = experiment.group(1)

## Check for previous complete.
complete = re.search('complete\t(.*)\n', logs)
if complete: info['complete'] = complete.group(1)

## Update metadata.
session['WARNING'] = "Revisited home."
write_metadata(session, ['WARNING'], 'a')
for k, v in info.items(): session[k] = v

## Redirect participant to consent form.
return redirect(url_for('consent.consent'))
## Redirect participant as appropriate.
if 'complete' in session:
return redirect(url_for('complete.complete'))
elif 'experiment' in session:
return redirect(url_for('experiment.experiment'))
else:
return redirect(url_for('consent.consent'))

## Case 7: first visit, workerId present.
else:
Expand Down
10 changes: 1 addition & 9 deletions app/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,13 @@ def alert():
## Case 1: previously completed experiment.
elif 'complete' in session:

## Update metadata.
session['WARNING'] = "Revisited alert page."
write_metadata(session, ['WARNING'], 'a')

## Redirect participant to complete page.
return redirect(url_for('complete.complete'))

## Case 2: repeat visit.
elif 'alert' in session:

## Update participant metadata.
session['WARNING'] = "Revisited alert page."
write_metadata(session, ['WARNING'], 'a')

## Redirect participant to error (previous participation).
## Redirect participant to experiment.
return redirect(url_for('experiment.experiment'))

## Case 3: first visit.
Expand Down
11 changes: 9 additions & 2 deletions app/app.ini
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
[FLASK]

# Flask secret key for encrypting session objects
# Suggested: get key from https://randomkeygen.com
# Recommended: get key from https://randomkeygen.com
SECRET_KEY = PLEASE_CHANGE_THIS

# Toggle debug mode (allow repeat visits from same session)
# Allow participants to restart experiments
# Accepts true or false
ALLOW_RESTART = false

# Toggle debug mode (session cookies cleared on start)
# Accepts true or false
DEBUG = true

Expand All @@ -16,5 +20,8 @@ METADATA = ../metadata
# Path to data folder [default: ../data]
DATA = ../data

# Path to incomplete data folder [default: ../incomplete]
INCOMPLETE = ../incomplete

# Path to reject folder [default: ../reject]
REJECT = ../reject
38 changes: 20 additions & 18 deletions app/complete.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def complete():
## Access query string.
query_info = request.args

## Confirm all TurkPrime metadata present.
## Confirm all CloudResearch metadata present.
fields = ['workerId','assignmentId','hitId','a','tp_a','b','tp_b','c','tp_c']
all_fields = all([f in query_info for f in fields])

Expand All @@ -21,38 +21,40 @@ def complete():
## Redirect participant to error (missing workerId).
return redirect(url_for('error.error', errornum=1000))

## Case 1: visit complete page with previous rejection.
elif session.get('complete') == 'reject':
## Case 1: visit complete page without previous completion.
elif 'complete' not in session:

## Update metadata.
session['WARNING'] = "Revisited complete."
write_metadata(session, ['WARNING'], 'a')
## Flag experiment as complete.
session['ERROR'] = "1005: Visited complete page before completion."
session['complete'] = 'reject'
write_metadata(session, ['ERROR','complete'], 'a')

## Redirect participant to error (unusual activity).
return redirect(url_for('error.error', errornum=1005))

## Case 2: visit complete page with previous error.
elif session.get('complete') == 'error':

## Update metadata.
session['WARNING'] = "Revisited complete."
write_metadata(session, ['WARNING'], 'a')
## Case 2: visit complete page with previous rejection.
elif session['complete'] == 'reject':

## Redirect participant to error (unusual activity).
return redirect(url_for('error.error', errornum=1005))

## Case 3: visit complete page but missing metadata.
elif session.get('complete') == 'success' and not all_fields:
## Case 3: visit complete page with previous error.
elif session['complete'] == 'error':

## Determine error code.
errornum = 1002 if not session['consent'] else 1005

## Redirect participant to error (unusual activity).
return redirect(url_for('error.error', errornum=errornum))

## Update metadata.
session['WARNING'] = "Revisited complete."
write_metadata(session, ['WARNING'], 'a')
## Case 4: visit complete page with previous success.
elif session['complete'] == 'success' and not all_fields:

## Redirect participant with complete metadata.
url = "/complete?workerId=%s&assignmentId=%s&hitId=%s&a=%s&tp_a=%s&b=%s&tp_b=%s&c=%s&tp_c=%s" %(session['workerId'], session['assignmentId'], session['hitId'], session['a'], session['tp_a'], session['b'], session['tp_b'], session['c'], session['tp_c'])
return redirect(url)

## Case 4: all else.
## Case 5: all else.
else:

## Redirect participant with completion code.
Expand Down
20 changes: 2 additions & 18 deletions app/consent.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,6 @@ def consent():
## Case 1: previously completed experiment.
elif 'complete' in session:

## Update metadata.
session['WARNING'] = "Revisited consent page."
write_metadata(session, ['WARNING'], 'a')

## Redirect participant to complete page.
return redirect(url_for('complete.complete'))

Expand All @@ -33,30 +29,18 @@ def consent():
## Case 3: repeat visit, previous bot-detection.
elif session['consent'] == 'BOT':

## Update participant metadata.
session['WARNING'] = "Revisited consent form."
write_metadata(session, ['WARNING'], 'a')

## Redirect participant to error (unusual activity).
return redirect(url_for('error.error', errornum=1005))

## Case 4: repeat visit, previous non-consent.
elif session['consent'] == False:

## Update participant metadata.
session['WARNING'] = "Revisited consent form."
write_metadata(session, ['WARNING'], 'a')

## Redirect participant to error (decline consent).
return redirect(url_for('error.error', errornum=1002))

## Case 5: repeat visit, previous consent.
else:

## Update participant metadata.
session['WARNING'] = "Revisited consent form."
write_metadata(session, ['WARNING'], 'a')

## Redirect participant to alert page.
return redirect(url_for('alert.alert'))

Expand All @@ -73,9 +57,8 @@ def consent_post():

## Update participant metadata.
session['consent'] = 'BOT'
session['experiment'] = False # Prevents incognito users
session['complete'] = 'error'
write_metadata(session, ['consent','experiment','complete'], 'a')
write_metadata(session, ['consent','complete'], 'a')

## Redirect participant to error (unusual activity).
return redirect(url_for('error.error', errornum=1005))
Expand All @@ -94,6 +77,7 @@ def consent_post():

## Update participant metadata.
session['consent'] = False
session['complete'] = 'error'
write_metadata(session, ['consent'], 'a')

## Redirect participant to error (decline consent).
Expand Down
Loading

0 comments on commit 7f0e839

Please sign in to comment.