Skip to content

Commit

Permalink
Version 1.0 (Prolific) (#41)
Browse files Browse the repository at this point in the history
* version 1.0 - initial push

* Version 1.0 potpourri (#40)

* track ip address

* collate nivturk plugins

* update error page

* fix bot detection bug

* error catching on missing session info

* per user completion code + asynchronous completion bug

* cleaned up config file / changed default completion code behavior

* redirects from all pages on previous completion

* intelligent incognito handling

* add monitor script

* update error codes

* add additional catch for repeat visits

* Update 1004 error text

Co-authored-by: Daniel Bennett <danielbrianbennett@gmail.com>
  • Loading branch information
szorowi1 and danielbrianbennett authored Aug 11, 2020
1 parent ed0981e commit a75dc35
Show file tree
Hide file tree
Showing 14 changed files with 434 additions and 244 deletions.
83 changes: 57 additions & 26 deletions app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from app import consent, alert, experiment, complete, error
from .io import write_metadata
from .utils import gen_code
__version__ = '0.9.9.3'
__version__ = '1.0'

## Define root directory.
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))
Expand All @@ -21,19 +21,18 @@
if not os.path.isdir(reject_dir): os.makedirs(reject_dir)

## Check Flask mode; if debug mode, clear session variable.
debug = cfg['FLASK']['DEBUG'] != "FALSE"
debug = cfg['FLASK'].getboolean('DEBUG')
if debug:
msg = "WARNING: Flask currently in debug mode. This should be changed prior to production."
warnings.warn(msg)
warnings.warn("WARNING: Flask currently in debug mode. This should be changed prior to production.")

## Check Flask password.
if cfg['FLASK']['SECRET_KEY'] == "PLEASE_CHANGE_THIS":
msg = "WARNING: Flask password is currently default. This should be changed prior to production."
warnings.warn(msg)
secret_key = cfg['FLASK']['SECRET_KEY']
if secret_key == "PLEASE_CHANGE_THIS":
warnings.warn("WARNING: Flask password is currently default. This should be changed prior to production.")

## Initialize Flask application.
app = Flask(__name__)
app.secret_key = cfg['FLASK']['SECRET_KEY']
app.secret_key = secret_key

## Apply blueprints to the application.
app.register_blueprint(consent.bp)
Expand All @@ -57,50 +56,82 @@ def index():

## Record incoming metadata.
info = dict(
workerId = request.args.get('PROLIFIC_PID'), # Prolific metadata; renamed for consistency with MTurk
assignmentId = request.args.get('SESSION_ID'), # Prolific metadata; renamed for consistency with MTurk
hitId = request.args.get('STUDY_ID'), # Prolific metadata; renamed for consistency with MTurk
workerId = request.args.get('PROLIFIC_PID'), # Prolific metadata
assignmentId = request.args.get('SESSION_ID'), # Prolific metadata
hitId = request.args.get('STUDY_ID'), # Prolific metadata
subId = gen_code(24), # NivTurk metadata
address = request.remote_addr, # NivTurk metadata
browser = request.user_agent.browser, # User metadata
platform = request.user_agent.platform, # User metadata
version = request.user_agent.version, # User metadata
code_success = cfg['PROLIFIC'].get('CODE_SUCCESS', gen_code(8).upper()),
code_reject = cfg['PROLIFIC'].get('CODE_REJECT', gen_code(8).upper()),
)

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

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

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

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

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

## Update metadata.
session['workerId'] = info['workerId']
session['ERROR'] = '1004: suspected incognito user.'
write_metadata(session, ['ERROR'], 'a')
## Consult log file.
with open(os.path.join(session['metadata'], info['workerId']),'r') as f:
logs = f.read()

## Redirect participant to error (unusual activity).
return redirect(url_for('error.error', errornum=1004))
## 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')

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

## Case 4: repeat visit, manually changed workerId.
elif 'workerId' in session and session['workerId'] != info['workerId']:

## Update metadata.
session['ERROR'] = '1002: workerId tampering detected.'
write_metadata(session, ['ERROR'], 'a')
session['ERROR'] = '1005: workerId tampering detected.'
session['complete'] = 'error'
write_metadata(session, ['ERROR','complete'], 'a')

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

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

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

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

## Case 5: repeat visit, preexisting activity.
## Case 6: repeat visit, preexisting activity.
elif 'workerId' in session:

## Update metadata.
Expand All @@ -110,12 +141,12 @@ def index():
## Redirect participant to consent form.
return redirect(url_for('consent.consent'))

## Case 6: first visit, workerId present.
## Case 7: first visit, workerId present.
else:

## Update metadata.
for k, v in info.items(): session[k] = v
write_metadata(session, ['workerId','hitId','assignmentId','subId','browser','platform','version'], 'w')
write_metadata(session, ['workerId','hitId','assignmentId','subId','address','browser','platform','version'], 'w')

## Redirect participant to consent form.
return redirect(url_for('consent.consent'))
32 changes: 24 additions & 8 deletions app/alert.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,24 @@
def alert():
"""Present alert to participant."""

## Case 1: first visit.
if not 'alert' in session:
## Error-catching: screen for missing session.
if not 'workerId' in session:

## Update participant metadata.
session['alert'] = True
write_metadata(session, ['alert'], 'a')
## Redirect participant to error (missing workerId).
return redirect(url_for('error.error', errornum=1000))

## Present alert page.
return render_template('alert.html')
## 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.
else:
elif 'alert' in session:

## Update participant metadata.
session['WARNING'] = "Revisited alert page."
Expand All @@ -28,6 +34,16 @@ def alert():
## Redirect participant to error (previous participation).
return redirect(url_for('experiment.experiment'))

## Case 3: first visit.
else:

## Update participant metadata.
session['alert'] = True
write_metadata(session, ['alert'], 'a')

## Present alert page.
return render_template('alert.html')

@bp.route('/alert', methods=['POST'])
def alert_post():
"""Process participant repsonse to alert page."""
Expand Down
28 changes: 24 additions & 4 deletions app/app.ini
Original file line number Diff line number Diff line change
@@ -1,10 +1,30 @@
[FLASK]

# Flask secret key for encrypting session objects
# Suggested: get key from https://randomkeygen.com
SECRET_KEY = PLEASE_CHANGE_THIS
COMPLETION_CODE = PROLIFIC_CODE_GOES_HERE
DECOY_CODE = D60F2155
DEBUG = TRUE

# Toggle debug mode (allow repeat visits from same session)
# Accepts true or false
DEBUG = true

[PROLIFIC]

# Participant completion code on success
# Accepts string, or comment out for per-user codes
CODE_SUCCESS = PROLIFIC_CODE_GOES_HERE

# Participant completion code on reject
# Accepts string, or comment out for per-user codes
CODE_REJECT = D60F2155

[IO]
DATA = ../data

# Path to metadata folder [default: ../metadata]
METADATA = ../metadata

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

# Path to reject folder [default: ../reject]
REJECT = ../reject
68 changes: 45 additions & 23 deletions app/complete.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,59 @@
import os, configparser
from flask import (Blueprint, redirect, render_template, request, session, url_for)
from .io import write_metadata

## Initialize blueprint.
bp = Blueprint('complete', __name__)

## Define root directory.
ROOT_DIR = os.path.dirname(os.path.realpath(__file__))

## Load and parse configuration file.
cfg = configparser.ConfigParser()
cfg.read(os.path.join(ROOT_DIR, 'app.ini'))

## Specify completion URLs (real and decoy).
url_stem = "https://app.prolific.co/submissions/complete?cc="
complete_url = url_stem + cfg['FLASK']['COMPLETION_CODE']
decoy_url = url_stem + cfg['FLASK']['DECOY_CODE']

@bp.route('/complete')
def complete():
"""Present completion screen to participant."""

## Case 1: navigation to completion page without completion flag
if 'complete' not in session or session['complete'] == False:
## Error-catching: screen for missing session.
if not 'workerId' in session:

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

## Case 1: visit complete page without previous completion.
elif 'complete' not in session:

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

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

## Update participant metadata.
session['ERROR'] = "1012: Visited completion page without valid completion flag."
write_metadata(session, ['ERROR'], 'a')
return redirect(decoy_url)
## Case 2: visit complete page with previous rejection.
elif session['complete'] == 'success':

## Case 2: data_pass
## Update metadata.
session['WARNING'] = "Revisited complete."
write_metadata(session, ['WARNING'], 'a')

## Redirect participant with completion code.
url = "https://app.prolific.co/submissions/complete?cc=" + session['code_success']
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')

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

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

## Update participant metadata.
write_metadata(session, ['complete'], 'a')
return redirect(complete_url)
## Update metadata.
session['WARNING'] = "Revisited complete."
write_metadata(session, ['WARNING'], 'a')

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

0 comments on commit a75dc35

Please sign in to comment.