Skip to content

Commit

Permalink
nivturk v1.2 (prolific branch) (#94)
Browse files Browse the repository at this point in the history
* nivturk-v1.2-prolific (1)
- upgrade to jspsych v7.2.1 (#80)

* nivturk-v1.2-prolific (2)
- streamline participant redirects
- remove unnecessary warnings
- improve log file parsing using regex

* nivturk-v1.2-prolific (3)
- update nivturk functions to redirect participants even when data fails 
to save

* nivturk-v1.2-prolific (4)
- add an ALLOW_RESTART mode that enables participants to restart an 
experiment

* nivturk-v1.2-prolific (5)
- update alert page language to reflect new restart mode

* nivturk-v1.2-prolific (6)
- add incomplete data saving (#93)

* nivturk-v1.2-prolific (7)
- add incomplete folder
  • Loading branch information
szorowi1 authored Jun 2, 2022
1 parent 0acc865 commit 304ad26
Show file tree
Hide file tree
Showing 120 changed files with 80,829 additions and 15,631 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/**
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 @@ -68,47 +75,23 @@ def index():
code_reject = cfg['PROLIFIC'].get('CODE_REJECT', gen_code(8).upper()),
)

## 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 @@ -121,25 +104,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 @@ -26,5 +30,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
27 changes: 9 additions & 18 deletions app/complete.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,33 +27,24 @@ def complete():
return redirect(url)

## Case 2: visit complete page with previous rejection.
elif session['complete'] == 'success':

## Update metadata.
session['WARNING'] = "Revisited complete."
write_metadata(session, ['WARNING'], 'a')
elif session['complete'] == 'reject':

## Redirect participant with completion code.
url = "https://app.prolific.co/submissions/complete?cc=" + session['code_success']
## Redirect participant with decoy code.
url = "https://app.prolific.co/submissions/complete?cc=" + session['code_reject']
return redirect(url)

## Case 3: visit complete page with previous rejection.
elif session['complete'] == 'reject':

## Update metadata.
session['WARNING'] = "Revisited complete."
write_metadata(session, ['WARNING'], 'a')
elif session['complete'] == 'success':

## Redirect participant with decoy code.
url = "https://app.prolific.co/submissions/complete?cc=" + session['code_reject']
## Redirect participant with completion code.
url = "https://app.prolific.co/submissions/complete?cc=" + session['code_success']
return redirect(url)

## Case 4: visit complete page with previous error.
else:

## Update metadata.
session['WARNING'] = "Revisited complete."
write_metadata(session, ['WARNING'], 'a')
## Determine error code.
errornum = 1002 if not session['consent'] else 1005

## Redirect participant to error (unusual activity).
return redirect(url_for('error.error', errornum=1005))
return redirect(url_for('error.error', errornum=errornum))
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
29 changes: 24 additions & 5 deletions app/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,11 @@ def experiment():
## Case 1: previously completed experiment.
elif 'complete' in session:

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

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

## Case 2: repeat visit.
elif 'experiment' in session:
elif not session['allow_restart'] and 'experiment' in session:

## Update participant metadata.
session['ERROR'] = "1004: Revisited experiment."
Expand Down Expand Up @@ -65,6 +61,29 @@ def pass_message():
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
return ('', 200)

@bp.route('/incomplete_save', methods=['POST'])
def incomplete_save():
"""Save incomplete jsPsych dataset to disk."""

if request.is_json:

## Retrieve jsPsych data.
JSON = request.get_json()

## Save jsPsch data to disk.
write_data(session, JSON, method='incomplete')

## Flag partial data saving.
session['MESSAGE'] = 'incomplete dataset saved'
write_metadata(session, ['MESSAGE'], 'a')

## DEV NOTE:
## This function returns the HTTP response status code: 200
## Code 200 signifies the POST request has succeeded.
## For a full list of status codes, see:
## https://developer.mozilla.org/en-US/docs/Web/HTTP/Status
return ('', 200)

@bp.route('/redirect_success', methods = ['POST'])
def redirect_success():
"""Save complete jsPsych dataset to disk."""
Expand Down
2 changes: 2 additions & 0 deletions app/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -41,5 +41,7 @@ def write_data(session, json, method='pass'):
fout = os.path.join(session['data'], '%s.json' %session['subId'])
elif method == 'reject':
fout = os.path.join(session['reject'], '%s.json' %session['subId'])
elif method == 'incomplete':
fout = os.path.join(session['incomplete'], '%s.json' %session['subId'])

with open(fout, 'w') as f: f.write(json)
Loading

0 comments on commit 304ad26

Please sign in to comment.