def get_rows_for_csv(app_name): # need to use app_name and not app_label because the app might have been # removed from SESSION_CONFIGS models_module = otree.common.get_models_module(app_name) Player = models_module.Player Group = models_module.Group Subsession = models_module.Subsession columns_for_models = { Model.__name__.lower(): get_fields_for_csv(Model) for Model in [Player, Group, Subsession, Participant, Session] } participant_ids = values_flat(dbq(Player), Player.participant_id) session_ids = values_flat(dbq(Subsession), Subsession.session_id) players = Player.values_dicts() value_dicts = dict( group={row['id']: row for row in Group.values_dicts()}, subsession={row['id']: row for row in Subsession.values_dicts()}, participant={ row['id']: row for row in Participant.values_dicts( Participant.id.in_(participant_ids)) }, session={ row['id']: row for row in Session.values_dicts(Session.id.in_(session_ids)) }, ) model_order = ['participant', 'player', 'group', 'subsession', 'session'] # header row rows = [[ f'{m}.{col}' for m in model_order for col in columns_for_models[m] ]] for player in players: tweak_player_values_dict(player) row = [] for model_name in model_order: if model_name == 'player': obj = player else: obj = value_dicts[model_name][player[f'{model_name}_id']] for colname in columns_for_models[model_name]: row.append(sanitize_for_csv(obj[colname])) rows.append(row) return rows
def export_page_times(fp): write_page_completion_buffer() batches = values_flat( dbq(PageTimeBatch).order_by('id'), PageTimeBatch.text) fp.write(','.join(TIME_SPENT_COLUMNS) + '\n') for batch in batches: fp.write(batch)
def _get_session_lookups(session_code) -> Dict[int, PageLookup]: session = dbq(Session).filter_by(code=session_code).one() pages = {} idx = 1 for app_name in session.config['app_sequence']: models = get_models_module(app_name) Subsession = models.Subsession page_sequence = get_pages_module(app_name).page_sequence subsessions = { s[0]: s[1] for s in Subsession.objects_filter(session=session).with_entities( Subsession.round_number, Subsession.id) } for rd in range(1, models.Constants.num_rounds + 1): is_first_in_round = True for PageClass in page_sequence: pages[idx] = PageLookup( app_name=app_name, page_class=PageClass, round_number=rd, subsession_id=subsessions[rd], # TODO: remove session ID, just use code everywhere session_pk=session.id, name_in_url=models.Constants.name_in_url, is_first_in_round=is_first_in_round, ) is_first_in_round = False idx += 1 return pages
def get(self, request): buf = StringIO() column_names = [ 'session_code', 'id_in_session', 'participant_code', 'channel', 'nickname', 'body', 'timestamp', ] rows = ( dbq(ChatMessage) .join(Participant) .order_by(ChatMessage.timestamp) .with_entities( Participant._session_code, Participant.id_in_session, Participant.session_id, ChatMessage.channel, ChatMessage.nickname, ChatMessage.body, ChatMessage.timestamp, ) ) writer = csv.writer(buf) writer.writerows([column_names]) writer.writerows(rows) response = get_csv_http_response(buf, 'ChatMessages') return response
def _gbat_next_group_id_in_subsession(self): # 2017-05-05: seems like this can result in id_in_subsession that # doesn't start from 1. # especially if you do group_by_arrival_time in every round # is that a problem? Group = self._GroupClass() return (dbq(func.max( Group.id_in_subsession)).filter_by(session=self.session).scalar() + 1)
def _get_participants_for_this_waitpage(self): Player = self.PlayerClass fk_field = Player.subsession_id if self.wait_for_all_groups else Player.group_id # tried select_from but my filter clause didn't work return ( dbq(Player) .join(Participant) .filter(fk_field == self._group_or_subsession.id) .with_entities(Participant) )
def _get_group_matrix(self, ids_only=False): Player = self._PlayerClass() Group = self._GroupClass() players = (dbq(Player).join(Group).filter( Player.subsession == self).order_by(Group.id_in_subsession, 'id_in_group')) d = defaultdict(list) for p in players: d[p.group.id_in_subsession].append( p.id_in_subsession if ids_only else p) return list(d.values())
def vars_for_template(self): # can't use settings.OTREE_APPS, because maybe the app # was removed from SESSION_CONFIGS. app_names_with_data = set() for session in dbq(Session): for app_name in session.config['app_sequence']: app_names_with_data.add(app_name) custom_export_apps = [] for app_name in app_names_with_data: models_module = otree.common.get_models_module(app_name) if getattr(models_module, 'custom_export', None): custom_export_apps.append(app_name) return dict( db_is_empty=not bool(dbq(Participant).first()), app_names=app_names_with_data, chat_messages_exist=bool(dbq(ChatMessage).first()), custom_export_apps=custom_export_apps, )
def is_ready(self, *, app_name, player_id, page_index, session_pk): models_module = get_models_module(app_name) Player = models_module.Player Group = models_module.Group [group_id_in_subsession] = (dbq(Player).join(Group).filter( Player.id == player_id).with_entities( Group.id_in_subsession).one()) return CompletedGBATWaitPage.objects_exists( page_index=page_index, id_in_subsession=group_id_in_subsession, session_id=session_pk, )
def get_rows_for_wide_csv(session_code): if session_code: sessions = [Session.objects_get(code=session_code)] else: sessions = dbq(Session).order_by('id').all() session_fields = get_fields_for_csv(Session) participant_fields = get_fields_for_csv(Participant) session_ids = [session.id for session in sessions] pps = (Participant.objects_filter( Participant.session_id.in_(session_ids)).order_by( Participant.id).all()) session_cache = {row.id: row for row in sessions} session_config_fields = set() for session in sessions: for field_name in SessionConfig(session.config).editable_fields(): session_config_fields.add(field_name) session_config_fields = list(session_config_fields) if not pps: # 1 empty row return [[]] header_row = [f'participant.{fname}' for fname in participant_fields] header_row += [f'session.{fname}' for fname in session_fields] header_row += [ f'session.config.{fname}' for fname in session_config_fields ] rows = [header_row] for pp in pps: session = session_cache[pp.session_id] row = [getattr(pp, fname) for fname in participant_fields] row += [getattr(session, fname) for fname in session_fields] row += [session.config.get(fname) for fname in session_config_fields] rows.append(row) order_of_apps = _get_best_app_order(sessions) rounds_per_app = OrderedDict() for app_name in order_of_apps: try: models_module = get_models_module(app_name) except ModuleNotFoundError: # this should only happen with devserver because on production server, # you would need to resetdb after renaming an app. logger.warning( f'Cannot export data for app {app_name}, which existed when the session was run ' f'but no longer exists.') continue highest_round_number = dbq( func.max(models_module.Subsession.round_number)).scalar() if highest_round_number is not None: rounds_per_app[app_name] = highest_round_number for app_name in rounds_per_app: for round_number in range(1, rounds_per_app[app_name] + 1): new_rows = get_rows_for_wide_csv_round(app_name, round_number, sessions) for i in range(len(rows)): rows[i].extend(new_rows[i]) return [[sanitize_for_csv(v) for v in row] for row in rows]
def create_session( session_config_name, *, num_participants, label='', room_name=None, is_mturk=False, is_demo=False, modified_session_config_fields=None, ) -> Session: num_subsessions = 0 try: session_config = SESSION_CONFIGS_DICT[session_config_name] except KeyError: msg = 'Session config "{}" not found in settings.SESSION_CONFIGS.' raise KeyError(msg.format(session_config_name)) from None else: # copy so that we don't mutate the original # .copy() returns a dict, so need to convert back to SessionConfig session_config = SessionConfig(session_config.copy()) modified_config = modified_session_config_fields or {} # this is for API. don't want to mislead people # to put stuff in the session config that should be in the session. bad_keys = modified_config.keys() & NON_EDITABLE_FIELDS if bad_keys: raise Exception( f'The following session config fields are not editable: {bad_keys}' ) session_config.update(modified_config) # check validity and converts serialized decimal & currency values # back to their original data type (because they were serialized # when passed through channels session_config.clean() # check that it divides evenly session_lcm = session_config.get_lcm() if num_participants is None: # most games are multiplayer, so if it's under 2, we bump it to 2 num_participants = max(session_lcm, 2) else: if num_participants % session_lcm: msg = ( 'Session Config {}: Number of participants ({}) is not a multiple ' 'of group size ({})' ).format(session_config['name'], num_participants, session_lcm) raise ValueError(msg) session = Session( config=session_config, label=label, is_demo=is_demo, num_participants=num_participants, is_mturk=is_mturk, ) db.add(session) db.commit() session_code = session.code participants = [ Participant( id_in_session=id_in_session, session=session, _session_code=session_code, ) for id_in_session in list(range(1, num_participants + 1)) ] db.add_all(participants) db.commit() # participant_values = ( # db.query(Participant) # .filter(Session.id == session.id) # .order_by('id') # .with_entities(Participant.id, Participant.code) # ).all() participant_values = ( db.query(Participant) .join(Session) .filter(Session.id == session.id) .order_by(Participant.id) .with_entities(Participant.id, Participant.code) ).all() num_pages = 0 for app_name in session_config['app_sequence']: views_module = common.get_pages_module(app_name) models_module = get_models_module(app_name) Constants: BaseConstants = models_module.Constants num_subsessions += Constants.num_rounds round_numbers = list(range(1, Constants.num_rounds + 1)) num_pages += Constants.num_rounds * len(views_module.page_sequence) Subsession = models_module.Subsession Group = models_module.Group Player = models_module.Player subsessions = [ Subsession(round_number=round_number, session=session) for round_number in round_numbers ] db.add_all(subsessions) db.commit() subsessions = ( dbq(Subsession) .filter_by(session=session) .order_by('round_number') .with_entities('id', 'round_number') ) ppg = Constants.players_per_group if ppg is None or Subsession._has_group_by_arrival_time(): ppg = num_participants num_groups_per_round = int(num_participants / ppg) groups_to_create = [] for ss_id, ss_rd in subsessions: for id_in_subsession in range(1, num_groups_per_round + 1): groups_to_create.append( Group( session=session, subsession_id=ss_id, round_number=ss_rd, id_in_subsession=id_in_subsession, ) ) db.add_all(groups_to_create) groups = ( dbq(Group).filter_by(session=session).order_by('id_in_subsession') ).all() groups_lookup = defaultdict(list) for group in groups: groups_lookup[group.subsession_id].append(group.id) players_to_create = [] for ss_id, ss_rd in subsessions: roles = get_roles(Constants) participant_index = 0 for group_id in groups_lookup[ss_id]: for id_in_group in range(1, ppg + 1): participant = participant_values[participant_index] players_to_create.append( Player( session=session, subsession_id=ss_id, round_number=ss_rd, participant_id=participant[0], group_id=group_id, id_in_group=id_in_group, _role=get_role(roles, id_in_group), ) ) participant_index += 1 # Create players db.add_all(players_to_create) dbq(Participant).filter_by(session=session).update( {Participant._max_page_index: num_pages} ) # make creating_session use the current session, # so that session.save() below doesn't overwrite everything # set earlier for subsession in session.get_subsessions(): subsession.creating_session() # 2017-09-27: moving this inside the transaction session._set_admin_report_app_names() if room_name is not None: from otree.room import ROOM_DICT room = ROOM_DICT[room_name] room.set_session(session) db.commit() return session
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 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 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 RedirectResponse(pp._start_url(), status_code=302)