import csv import datetime import os from otree.api import * doc = """ Consent page for the experiment. PURPOSE ------- Displays the participant information sheet, collects electronic consent via a required checkbox, and writes a dedicated consent log entry that satisfies the following electronic-consent storage requirement: "The log must contain at least: date, time, IP address, and user ID." HOW IT WORKS ------------ 1. The page renders the information sheet and a mandatory checkbox. 2. When the participant submits: - oTree validates that the checkbox is ticked (error if not). - `before_next_page` stamps the timestamp on the Player record and calls `_write_consent_log`, which appends one row to consent_log.csv in the project root. 3. The log file is created automatically on the first consent submission. HOW TO USE ---------- Add 'Consent' as the FIRST entry in the `app_sequence` of every SESSION_CONFIG in settings.py: 'app_sequence': ['Consent', 'Combined_2lights', 'Pay'], """ # ───────────────────────────────────────────────────────────────────────────── # CONSTANTS / MODELS # ───────────────────────────────────────────────────────────────────────────── class C(BaseConstants): NAME_IN_URL = 'Consent' PLAYERS_PER_GROUP = None NUM_ROUNDS = 1 class Subsession(BaseSubsession): pass class Group(BaseGroup): pass class Player(BasePlayer): # ── Prolific ID (collected before consent, stored separately from Code.html) ─ ID_consent = models.StringField(label="Please enter your Prolific ID") # ── Consent checkbox ────────────────────────────────────────────────────── # Set to True when the participant ticks the box and submits. consent_given = models.BooleanField( label="I have read the information above and agree to participate in this study.", widget=widgets.CheckboxInput, initial=False, ) # ── Timestamp ───────────────────────────────────────────────────────────── # Stored as "YYYY-MM-DD HH:MM:SS" (set in before_next_page). consent_timestamp = models.StringField(blank=True) # ── IP address ──────────────────────────────────────────────────────────── # Submitted as a hidden form field pre-filled by vars_for_template. # Listed in form_fields so oTree stores it automatically on submit. consent_ip = models.StringField(blank=True) # ───────────────────────────────────────────────────────────────────────────── # CONSENT LOG # Writes/appends rows to /consent_log.csv # Each row records: date, time, participant_code (user ID), ip_address, # consent_given, session_code. # # This file is the dedicated consent storage required by the protocol. # Keep it stored securely and back it up regularly. # ───────────────────────────────────────────────────────────────────────────── def _write_consent_log(player): """Append one consent record to consent_log.csv in the project root.""" now = datetime.datetime.now() # Resolve the project root: two levels up from this file # (Consent/__init__.py → Consent/ → project root) project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) log_path = os.path.join(project_root, 'consent_log.csv') file_exists = os.path.isfile(log_path) with open(log_path, 'a', newline='', encoding='utf-8') as f: writer = csv.writer(f) # Write header on the very first run if not file_exists: writer.writerow([ 'date', 'time', 'participant_code', # oTree internal user ID 'prolific_id', # ID_consent from this page 'ip_address', 'consent_given', 'session_code', ]) writer.writerow([ now.strftime('%Y-%m-%d'), now.strftime('%H:%M:%S'), player.participant.code, player.ID_consent or '', player.consent_ip or 'unknown', player.consent_given, player.session.code, ]) # ───────────────────────────────────────────────────────────────────────────── # PAGES # ───────────────────────────────────────────────────────────────────────────── class ConsentPage(Page): form_model = 'player' # consent_ip is included here so oTree saves the hidden field on submit. form_fields = ['ID_consent', 'consent_given', 'consent_ip'] # NOTE: get_context_data is a standard Django CBV method that oTree does # NOT intercept with its own player-passing logic, so `self` here is the # real page view instance with access to self.request. # (oTree's own methods like vars_for_template receive the *player* as # their first argument, which is why we cannot use those to access # self.request.) def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) # oTree >= 5.10 uses Starlette (ASGI), so the request is a # starlette.requests.Request — not a Django WSGIRequest. # Headers are in request.headers (lowercase), and the direct client # IP is in request.client.host (not request.META['REMOTE_ADDR']). x_forwarded_for = self.request.headers.get('x-forwarded-for', '') if x_forwarded_for: ip = x_forwarded_for.split(',')[0].strip() else: ip = self.request.client.host if self.request.client else 'unknown' ctx['ip_address'] = ip return ctx @staticmethod def error_message(player, values): """Block form submission if the participant did not tick the box.""" if not values.get('consent_given'): return 'You must check the box to confirm your consent before continuing.' @staticmethod def before_next_page(player, timeout_happened): # 1. Record the consent timestamp on the Player model player.consent_timestamp = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 2. Write the dedicated consent log entry _write_consent_log(player) page_sequence = [ConsentPage]