def _get_current_player(self): lookup = get_page_lookup(self._session_code, self._index_in_pages) models_module = otree.common.get_models_module(lookup.app_name) PlayerClass = getattr(models_module, 'Player') return PlayerClass.objects.get( participant=self, round_number=lookup.round_number )
def set_attributes(self, participant): lookup = get_page_lookup(participant._session_code, participant._index_in_pages) self._lookup = lookup app_name = lookup.app_name models_module = otree.common.get_models_module(app_name) self._Constants = models_module.Constants self.PlayerClass = getattr(models_module, 'Player') self.GroupClass = getattr(models_module, 'Group') self.SubsessionClass = getattr(models_module, 'Subsession') self.player = self.PlayerClass.objects.get( participant=participant, round_number=lookup.round_number ) self._subsession_pk = lookup.subsession_id self.round_number = lookup.round_number self._session_pk = lookup.session_pk # simpler if we set it directly so that we can do tests without idmap cache self._participant_pk = participant.pk # setting it directly makes testing easier (tests dont need to use cache) self.participant = participant # it's already validated that participant is on right page self._index_in_pages = participant._index_in_pages # for the participant changelist participant._current_app_name = app_name participant._current_page_name = self.__class__.__name__ participant._last_request_timestamp = time.time() participant._round_number = lookup.round_number self._is_frozen = True
def _increment_index_in_pages(self): # when is this not the case? participant = self.participant assert self._index_in_pages == participant._index_in_pages # we should allow a user to move beyond the last page if it's mturk # also in general maybe we should show the 'out of sequence' page # we skip any page that is a sequence page where is_displayed # evaluates to False to eliminate unnecessary redirection page_index_to_skip_to = self._get_next_page_index_if_skipping_apps() is_skipping_apps = bool(page_index_to_skip_to) for page_index in range( # go to max_page_index+2 because range() skips the last index # and it's possible to go to max_page_index + 1 (OutOfRange) self._index_in_pages + 1, participant._max_page_index + 2, ): participant._index_in_pages = page_index if page_index == participant._max_page_index + 1: # break and go to OutOfRangeNotification break if is_skipping_apps and page_index == page_index_to_skip_to: break # scope, receive, send page = get_page_lookup( participant._session_code, page_index ).page_class.instantiate_without_request() page.set_attributes(self.participant) if not is_skipping_apps: if page._lookup.is_first_in_round: # we have moved to a new round. page.player.start() if page._is_displayed(): break # if it's a wait page, record that they visited if isinstance(page, WaitPage): if page.group_by_arrival_time: # keep looping # if 1 participant can skip the page, # then all other participants should skip it also, # as described in the docs # so there is no need to mark as complete. continue # save the participant, because tally_unvisited # queries index_in_pages directly from the DB db.commit() is_last, someone_waiting = page._tally_unvisited() if is_last and someone_waiting: page._run_aapa_and_notify(page._group_or_subsession)
def live_payload_function(participant_code, page_name, payload): participant = Participant.objects.get(code=participant_code) lookup = get_page_lookup(participant._session_code, participant._index_in_pages) app_name = lookup.app_name models_module = otree.common.get_models_module(app_name) PageClass = lookup.page_class assert page_name == PageClass.__name__ method_name = PageClass.live_method with otree.db.idmap.use_cache(): player = models_module.Player.objects.get( round_number=lookup.round_number, participant=participant ) group = player.group method = getattr(group, method_name) retval = method(player.id_in_group, payload) otree.db.idmap.save_objects() if not retval: return if not isinstance(retval, dict): msg = f'{method_name} must return a dict' raise LiveMethodBadReturnValue(msg) pcodes_dict = { d['id_in_group']: d['participant__code'] for d in models_module.Player.objects.filter(group=group).values( 'participant__code', 'id_in_group' ) } if 0 in retval: if len(retval) > 1: raise LiveMethodBadReturnValue( 'If dict returned by live_method has key 0, it must not contain any other keys' ) else: for pid in retval: if pid not in pcodes_dict: msg = f'live_method has invalid return value. No player with id_in_group={repr(pid)}' raise LiveMethodBadReturnValue(msg) pcode_retval = {} for pid, pcode in pcodes_dict.items(): payload = retval.get(pid, retval.get(0)) if payload is not None: pcode_retval[pcode] = payload _live_send_back( participant._session_code, participant._index_in_pages, pcode_retval )
def post(self): r = super().post() current_app = self._lookup.app_name try: next_app = get_page_lookup(self.session.code, self.participant._index_in_pages).app_name except KeyError: # in this case it's apparently the last app in the original app_sequence next_app = None if current_app == next_app: return r seq_dict = self.participant.vars.get('_updated_seq_apps') if seq_dict: app_to_skip_to = seq_dict.get(current_app) if app_to_skip_to: where_to = get_min_idx_for_app(self.participant._session_code, app_to_skip_to) else: where_to = self.participant._max_page_index + 1 self.participant._index_in_pages = where_to self._is_frozen = False self._index_in_pages = where_to return self._redirect_to_page_the_user_should_be_on() else: return r
async def live_payload_function(participant_code, page_name, payload): participant = Participant.objects_get(code=participant_code) lookup = get_page_lookup(participant._session_code, participant._index_in_pages) app_name = lookup.app_name models_module = otree.common.get_models_module(app_name) PageClass = lookup.page_class # this could be incorrect if the player advances right after liveSend is executed. # maybe just return if it doesn't match. (but leave it in for now and see how much that occurs, # don't want silent failures.) if page_name != PageClass.__name__: logger.warning( f'Ignoring liveSend message from {participant_code} because ' f'they are on page {PageClass.__name__}, not {page_name}.') return live_method_name = PageClass.live_method player = models_module.Player.objects_get(round_number=lookup.round_number, participant=participant) # it makes sense to check the group first because # if the player forgot to define it on the Player, # we shouldn't fall back to checking the group. you could get an error like # 'Group' has no attribute 'live_auction' which would be confusing. # also, we need this 'group' object anyway. # and this is a good place to show the deprecation warning. group = player.group if hasattr(group, live_method_name): method = getattr(group, live_method_name) retval = method(player.id_in_group, payload) else: method = getattr(player, live_method_name) retval = method(payload) if not retval: return if not isinstance(retval, dict): msg = f'{live_method_name} must return a dict' raise LiveMethodBadReturnValue(msg) Player: BasePlayer = models_module.Player pcodes_dict = { d[0]: d[1] for d in Player.objects_filter( group=group).join(Participant).with_entities( Player.id_in_group, Participant.code, ) } if 0 in retval: if len(retval) > 1: raise LiveMethodBadReturnValue( 'If dict returned by live_method has key 0, it must not contain any other keys' ) else: for pid in retval: if pid not in pcodes_dict: msg = f'live_method has invalid return value. No player with id_in_group={repr(pid)}' raise LiveMethodBadReturnValue(msg) pcode_retval = {} for pid, pcode in pcodes_dict.items(): payload = retval.get(pid, retval.get(0)) if payload is not None: pcode_retval[pcode] = payload await _live_send_back(participant._session_code, participant._index_in_pages, pcode_retval)
def get_current_page_name(self): lookup = get_page_lookup(self.participant._session_code, self.participant._index_in_pages) return lookup.page_class.__name__
def advance_last_place_participants(self): """the problem with using the test client to make get/post requests is (1) this request already has the global asyncio.lock (2) there are apparently some issues with async/await and event loops. """ from otree.lookup import get_page_lookup from otree.api import WaitPage, Page participants = self.get_participants() # in case some participants haven't started unvisited_participants = False for p in participants: if p._index_in_pages == 0: p.initialize(None) if unvisited_participants: # that's it -- just visit the start URL, advancing by 1 return last_place_page_index = min([p._index_in_pages for p in participants]) last_place_participants = [ p for p in participants if p._index_in_pages == last_place_page_index ] for p in last_place_participants: page_index = p._index_in_pages if page_index >= p._max_page_index: return page = get_page_lookup( self.code, page_index).page_class.instantiate_without_request() page.set_attributes(p) if isinstance(page, Page): from starlette.datastructures import FormData page._is_frozen = False page._form_data = FormData({ otree.constants.admin_secret_code: ADMIN_SECRET_CODE, otree.constants.timeout_happened: '1', }) # TODO: should we also call .get() so that _update_monitor_table will also get run? resp = page.post() if resp.status_code >= 400: msg = ( f'Submitting page {p._current_form_page_url} failed, ' f'returned HTTP status code {resp.status_code}. ' 'Check the logs') raise AssertionError(msg) else: # it's possible that the slowest user is on a wait page, # especially if their browser is closed. # because they were waiting for another user who then # advanced past the wait page, but they were never # advanced themselves. resp = page.inner_dispatch(request=None) # do the auto-advancing here, # rather than in increment_index_in_pages, # because it's only needed here. otree.channels.utils.sync_group_send(group=auto_advance_group( p.code), data={'auto_advanced': True})