from starlette.endpoints import HTTPEndpoint from starlette.responses import HTMLResponse, Response, RedirectResponse, JSONResponse from starlette.requests import Request from starlette.responses import Response, RedirectResponse import otree.bots.browser as browser_bots import otree.channels.utils as channel_utils import otree.common import otree.constants import otree.models import otree.views.admin import otree.views.cbv import otree.views.mturk from otree.common import ( make_hash, BotError, GlobalState, ) from otree.database import NoResultFound, db, dbq from otree.i18n import core_gettext as _ from otree.models import Participant, Session from otree.models_concrete import ParticipantVarsFromREST from otree.mturk_client import TurkClient from otree.room import ROOM_DICT from otree.templating import render from otree.views.abstract import GenericWaitPageMixin def no_participants_left_http_response(): ''' this function exists because i'm not sure if Http response objects can be reused better to return 404 so browser bot client & tests can recognize it ''' # Translators: for example this is shown if you create a session for 10 # participants. The 11th person to click will get this message # It means there is no space for you. return Response(_("Session is full."), status_code=404) class OutOfRangeNotification(HTTPEndpoint): url_pattern = '/OutOfRangeNotification/{code}' def get(self, request): code = request.path_params['code'] participant = db.get_or_404(Participant, code=code) if participant.is_browser_bot: session = participant.session has_next_submission = browser_bots.enqueue_next_post_data( participant_code=participant.code ) if has_next_submission: msg = ( 'Finished the last page, ' 'but the bot is still trying ' 'to submit more pages.' ) raise BotError(msg) browser_bots.send_completion_message( session_code=session.code, participant_code=code ) return render('otree/OutOfRangeNotification.html', {}) class GBATInactive(HTTPEndpoint): url_pattern = '/GBATInactive' def get(self, request): return render('otree/GBATInactive.html', {}) class InitializeParticipant(HTTPEndpoint): url_pattern = '/InitializeParticipant/{code}' def get(self, request: Request): """anything essential should be done in""" code = request.path_params['code'] pp = db.get_or_404(Participant, code=code) query_params = request.query_params label = query_params.get(otree.constants.participant_label) pp.initialize(label) first_url = pp._url_i_should_be_on() return RedirectResponse(first_url) class MTurkStart(HTTPEndpoint): url_pattern = r"/MTurkStart/{code}" def get(self, request: Request): code = request.path_params['code'] session = self.session = db.get_or_404(Session, code=code) GET = request.query_params try: assignment_id = GET['assignmentId'] worker_id = GET['workerId'] except KeyError: return Response( 'URL is missing assignmentId or workerId parameter', status_code=404 ) qual_id = session.config['mturk_hit_settings'].get('grant_qualification_id') use_sandbox = session.mturk_use_sandbox if qual_id and not use_sandbox: # this is necessary because MTurk's qualification requirements # don't prevent 100% of duplicate participation. See: # https://groups.google.com/forum/#!topic/otree/B66HhbFE9ck previous_participation = ( dbq(Participant) .join(Session) .filter( Participant.session != session, Session.mturk_qual_id == qual_id, Participant.mturk_worker_id == worker_id, ) .scalar() is not None ) if previous_participation: return Response('You have already accepted a related HIT') # if using sandbox, there is no point in granting quals. # https://groups.google.com/forum/#!topic/otree/aAmqTUF-b60 # don't pass request arg, because we don't want to show a message. # using the fully qualified name because that seems to make mock.patch work # seems OK to assign this multiple times TurkClient.assign_qualification( dict( QualificationTypeId=qual_id, WorkerId=worker_id, # Mturk complains if I omit IntegerValue IntegerValue=1, ), use_sandbox=use_sandbox, ) try: # just check if this worker already game, but # don't filter for assignment, because maybe they already started # and returned the previous assignment # in this case, we should assign back to the same participant # so that we don't get duplicates in the DB, and so people # can't snoop and try the HIT first, then re-try to get a bigger bonus pp = self.session.pp_set.filter_by(mturk_worker_id=worker_id).one() except NoResultFound: pp = self.session.pp_set.filter_by(visited=False).order_by('id').first() if not pp: return no_participants_left_http_response() # 2014-10-17: needs to be here even if it's also set in # the next view to prevent race conditions # this needs to be inside the lock pp.visited = True pp.mturk_worker_id = worker_id # reassign assignment_id, even if they are returning, because maybe they accepted # and then returned, then re-accepted with a different assignment ID # if it's their second time pp.mturk_assignment_id = assignment_id return _redirect_to_start_url(pp) def _redirect_to_start_url(participant): start_url = participant._start_url() return RedirectResponse(start_url, status_code=302) def get_participant_by_label(session, label): q = session.pp_set if label: try: return q.filter_by(label=label).one() except NoResultFound: pass return q.filter_by(visited=False).order_by('id').first() def get_participant_by_cookies(session, cookies): cookie_name = 'session_{}_participant'.format(session.code) code = cookies.get(cookie_name) # this could return None if code: return Participant.objects_filter(code=code).first() participant = session.pp_set.filter_by(visited=False).order_by('id').first() if participant: cookies[cookie_name] = participant.code return participant def mark_visited_and_record_label(participant, label): if not participant: return # needs to be here even if it's also set in # the next view to prevent race conditions participant.visited = True if label: participant.set_label(label) return participant def participant_or_none_if_exceeded(session, *, label, cookies=None): '''pass request.session as an arg if you want to get/set a cookie''' if cookies is None: participant = get_participant_by_label(session, label) else: participant = get_participant_by_cookies(session, cookies) if not participant: return return mark_visited_and_record_label(participant, label) class SessionWideLink(HTTPEndpoint): """ Better to have just 1 endpoint. In demo mode, we don't use cookies. With a real session, we do use cookies. We never need to have both. And don't have to think about what if the participant changes the URL. """ url_pattern = '/join/{anonymous_code}' use_cookies: bool def get_participant(self, request, label): raise NotImplementedError def get(self, request: Request): anonymous_code = request.path_params['anonymous_code'] session = db.get_or_404(Session, _anonymous_code=anonymous_code) label = request.query_params.get('participant_label') # is this priority right? maybe cookies should override label? # but maybe a participant first clicks the wrong link, # they shouldn't be stuck and forbidden from participating. if label: participant = get_participant_by_label(session, label) elif not session.is_demo: participant = get_participant_by_cookies(session, request.session) else: participant = get_participant_by_label(session, None) if not participant: return no_participants_left_http_response() mark_visited_and_record_label(participant, label) return _redirect_to_start_url(participant) def validate_room_visitor(room, query_params: dict) -> dict: errors = {} if room.has_participant_labels: label = query_params.get('participant_label', '') if not label: errors['participant_label'] = 'Please enter a participant label' elif label not in room.get_participant_labels(): errors['participant_label'] = 'This participant label was not found' elif room.use_secure_urls: hash = query_params.get('hash') if not hash or hash != make_hash(label): errors['hash'] = ( 'Invalid hash parameter. use_secure_urls is True, ' 'so you must use the participant-specific URL.' ) # later, validate custom params here. return errors class AssignVisitorToRoom(GenericWaitPageMixin, HTTPEndpoint): url_pattern = '/room/{room_name}' def get(self, request: Request): room_name = request.path_params['room_name'] try: room = ROOM_DICT[room_name] except KeyError: return Response('Invalid room specified in url', status_code=404) if not request.query_params.get('welcome_page_ok'): return render( room.welcome_page or "otree/RoomWelcomePage.html", dict(has_participant_label_file=room.has_participant_labels), ) errors = validate_room_visitor(room, request.query_params) if errors: return HTMLResponse(f"
{errors}
", status_code=404) # labels are still allowed even if the room has no participant_labels_file label = request.query_params.get('participant_label', '') session = room.get_session() if session is None: self.tab_unique_id = otree.common.random_chars_join_code() self._socket_url = channel_utils.room_participant_path( room_name=room_name, participant_label=label, # random chars in case the participant has multiple tabs open tab_unique_id=self.tab_unique_id, ) return render( "otree/WaitPageGeneric.html", dict( view=self, title_text=_('Please wait'), body_text=_('Waiting for your session to begin'), ), ) if label: participant = get_participant_by_label(session, label) else: participant = get_participant_by_cookies(session, request.session) if not participant: return no_participants_left_http_response() mark_visited_and_record_label(participant, label) if label: # whether the room has participant labels or not passed_vars = ParticipantVarsFromREST.objects_filter( room_name=room_name, participant_label=label ).first() if passed_vars: participant.vars.update(passed_vars.vars) db.delete(passed_vars) return _redirect_to_start_url(participant) def socket_url(self): return self._socket_url async def post(self, request: Request): room_name = request.path_params['room_name'] try: room = ROOM_DICT[room_name] except KeyError: return Response('Invalid room specified in url', status_code=404) # Parse JSON data from the request try: json_data = await request.json() except: return Response('Invalid JSON data', status_code=400) # Merge query params with JSON data for validation query_params = dict(request.query_params) query_params.update(json_data) # Validate the data errors = validate_room_visitor(room, query_params) if errors: return JSONResponse({'errors': errors}) # If validation passes, return success return JSONResponse({'status': 'ok'}) class BrowserBotStartLink(GenericWaitPageMixin, HTTPEndpoint): '''should i move this to another module? because the rest of these views are accessible without password login. ''' # remote CLI browser bots won't work if this takes an admin_secret_code param because # SECRET_KEY might be different on the server. url_pattern = '/browser_bot_start' def get(self, request): session_code = GlobalState.browser_bots_launcher_session_code if session_code: try: session = Session.objects_get(code=session_code) except NoResultFound: # maybe it's an old session pass else: participant = ( session.pp_set.filter_by(visited=False).order_by('id').first() ) if not participant: return no_participants_left_http_response() # 2014-10-17: needs to be here even if it's also set in # the next view to prevent race conditions participant.visited = True return _redirect_to_start_url(participant) ctx = dict( view=self, title_text='Please wait', body_text='Waiting for browser bots session to begin', ) return render("otree/WaitPageGeneric.html", ctx) def socket_url(self): return '/browser_bot_wait/'