Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

nivturk v1.2 (prolific branch) #94

Merged
merged 7 commits into from
Jun 2, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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