import time import django.utils.timezone import vanilla from django.http import ( HttpResponse, HttpResponseRedirect, HttpResponseNotFound, ) from django.shortcuts import get_object_or_404, render from django.template.response import TemplateResponse from django.utils.translation import ugettext as _ 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.mturk from otree.common import make_hash, BotError from otree.db import idmap from otree.models import Participant, Session from otree.models_concrete import ( ParticipantRoomVisit, BrowserBotsLauncherSessionCode, ParticipantVarsFromREST, ) from otree.room import ROOM_DICT 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 ''' return HttpResponseNotFound(_("Session is full.")) class OutOfRangeNotification(vanilla.View): name_in_url = 'shared' url_pattern = r'^OutOfRangeNotification/(?P[a-z0-9]+)/$' def dispatch(self, request, participant_code): participant = get_object_or_404(Participant, code=participant_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=participant.code ) return TemplateResponse(request, 'otree/OutOfRangeNotification.html') class InitializeParticipant(vanilla.UpdateView): url_pattern = r'^InitializeParticipant/(?P[a-z0-9]+)/$' def get(self, request, participant_code): participant = get_object_or_404(Participant, code=participant_code) if participant._index_in_pages == 0: participant._index_in_pages = 1 participant.visited = True # participant.label might already have been set participant.label = participant.label or self.request.GET.get( otree.constants.participant_label ) now = django.utils.timezone.now() participant.time_started = now participant._last_page_timestamp = time.time() participant.save() with idmap.use_cache(): player = participant._get_current_player() player.start() first_url = participant._url_i_should_be_on() return HttpResponseRedirect(first_url) class MTurkStart(vanilla.View): url_pattern = r"^MTurkStart/(?P[a-z0-9]+)/$" def dispatch(self, request, session_code): self.session = get_object_or_404(otree.models.Session, code=session_code) return super().dispatch(request) def get(self, request): GET = request.GET try: assignment_id = GET['assignmentId'] worker_id = GET['workerId'] except Exception: return HttpResponseNotFound( 'URL is missing assignmentId or workerId parameter' ) qual_id = self.session.config['mturk_hit_settings'].get( 'grant_qualification_id' ) use_sandbox = self.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 = Participant.objects.exclude( session=self.session ).filter(mturk_worker_id=worker_id, session__mturk_qual_id=qual_id) if previous_participation.exists(): return HttpResponse('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 mturk_client = otree.views.mturk.get_mturk_client(use_sandbox=use_sandbox) # seems OK to assign this multiple times mturk_client.associate_qualification_with_worker( QualificationTypeId=qual_id, WorkerId=worker_id, # Mturk complains if I omit IntegerValue IntegerValue=1, ) 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 participant = self.session.participant_set.get(mturk_worker_id=worker_id) except Participant.DoesNotExist: try: participant = self.session.participant_set.filter( visited=False ).order_by('id')[0] except IndexError: 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 participant.visited = True participant.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 participant.mturk_assignment_id = assignment_id participant.save() return HttpResponseRedirect(participant._start_url()) def get_existing_or_new_participant(session, label): if label: try: return session.participant_set.get(label=label) except Participant.DoesNotExist: pass return session.participant_set.filter(visited=False).order_by('id').first() def get_participant_with_cookie_check(session, cookies): cookie_name = 'session_{}_participant'.format(session.code) participant_code = cookies.get(cookie_name) # this could return None if participant_code: return Participant.objects.filter(code=participant_code).first() participant = session.participant_set.filter(visited=False).order_by('id').first() if participant: cookies[cookie_name] = participant.code 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_existing_or_new_participant(session, label) else: participant = get_participant_with_cookie_check(session, cookies) 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.label = label participant.save() return participant class JoinSessionAnonymously(vanilla.View): url_pattern = r'^join/(?P[a-z0-9]+)/$' def get(self, request, anonymous_code): session = get_object_or_404( otree.models.Session, _anonymous_code=anonymous_code ) label = self.request.GET.get('participant_label') participant = participant_or_none_if_exceeded(session, label=label) if not participant: return no_participants_left_http_response() return HttpResponseRedirect(participant._start_url()) class AssignVisitorToRoom(GenericWaitPageMixin, vanilla.View): url_pattern = r'^room/(?P\w+)/$' def dispatch(self, request, room): self.room_name = room try: room = ROOM_DICT[self.room_name] except KeyError: return HttpResponseNotFound('Invalid room specified in url') label = self.request.GET.get('participant_label', '') if room.has_participant_labels(): if label: missing_label = False invalid_label = label not in room.get_participant_labels() else: missing_label = True invalid_label = False # needs to be easy to re-enter label, in case we are in kiosk # mode if missing_label or invalid_label and not room.use_secure_urls: return render( request, "otree/RoomInputLabel.html", {'invalid_label': invalid_label}, ) if room.use_secure_urls: hash = self.request.GET.get('hash') if hash != make_hash(label): return HttpResponseNotFound( 'Invalid hash parameter. use_secure_urls is True, ' 'so you must use the participant-specific URL.' ) session = room.get_session() if session is None: self.tab_unique_id = otree.common.random_chars_10() self._socket_url = channel_utils.room_participant_path( room_name=self.room_name, participant_label=label, # random chars in case the participant has multiple tabs open tab_unique_id=self.tab_unique_id, ) return render( request, "otree/WaitPageRoom.html", { 'view': self, 'title_text': _('Please wait'), 'body_text': _('Waiting for your session to begin'), }, ) if label: cookies = None else: cookies = request.session # 2017-08-02: changing the behavior so that even in a room without # participant_label_file, 2 requests for the same start URL with same label # will return the same participant. Not sure if the previous behavior # (assigning to 2 different participants) was intentional or bug. participant = participant_or_none_if_exceeded( session, label=label, cookies=cookies ) if not participant: return no_participants_left_http_response() if label: # whether the room has participant labels or not passed_vars = ParticipantVarsFromREST.objects.filter( room_name=self.room_name, participant_label=label ).first() if passed_vars: participant.vars.update(passed_vars.vars) participant.save() passed_vars.delete() return HttpResponseRedirect(participant._start_url()) def get_context_data(self, **kwargs): return {'room': self.room_name} def socket_url(self): return self._socket_url def redirect_url(self): return self.request.get_full_path() class ParticipantRoomHeartbeat(vanilla.View): url_pattern = r'^ParticipantRoomHeartbeat/(?P\w+)/$' def get(self, request, tab_unique_id): # better not to return 404, because in practice, on Firefox, # this was still being requested after the session started. ParticipantRoomVisit.objects.filter(tab_unique_id=tab_unique_id).update( last_updated=time.time() ) return HttpResponse('') class ParticipantHeartbeatGBAT(vanilla.View): url_pattern = r'^ParticipantHeartbeatGBAT/(?P\w+)/$' def get(self, request, participant_code): Participant.objects.filter(code=participant_code).update( _last_request_timestamp=time.time() ) return HttpResponse('') class BrowserBotStartLink(GenericWaitPageMixin, vanilla.View): '''should i move this to another module? because the rest of these views are accessible without password login. ''' url_pattern = r'^browser_bot_start/(?P\w+)/$' def dispatch(self, request, pre_create_id): session_info = BrowserBotsLauncherSessionCode.objects.first() if session_info: if pre_create_id != session_info.pre_create_id: return HttpResponseNotFound('Incorrect pre_create_id') session = Session.objects.get(code=session_info.code) participant = ( session.participant_set.filter(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 participant.save() return HttpResponseRedirect(participant._start_url()) else: ctx = { 'view': self, 'title_text': 'Please wait', 'body_text': 'Waiting for browser bots session to begin', } return render(request, "otree/WaitPage.html", ctx) def socket_url(self): return '/browser_bot_wait/' def redirect_url(self): return self.request.get_full_path()