def get_rows_for_data_tab_app(session, app_name): """ Overridden function from `otree.export` module to provide data rows for the session data monitor for a specific app. """ models_module = get_models_module(app_name) Player = models_module.Player Group = models_module.Group Subsession = models_module.Subsession pfields, gfields, sfields = export.get_fields_for_data_tab(app_name) # find out column names for standard models std_models_colnames = dict( zip(('player', 'group', 'subsession'), (pfields, gfields, sfields))) # get custom model configuration, if there is any custom_models_conf = get_custom_models_conf(models_module, for_action='data_view') # find out column names for custom models custom_models_colnames = get_custom_models_columns(custom_models_conf, for_action='data_view') # identify links between standard and custom models links_to_custom_models = get_links_between_std_and_custom_models( custom_models_conf, for_action='data_view') # all displayed columns in their order all_colnames = combine_column_names(std_models_colnames, custom_models_colnames) # iterate through the subsessions (i.e. rounds) for subsess_id in Subsession.objects.filter(session=session).values('id'): subsess_id = subsess_id['id'] # pre-filter querysets to get only data of this subsession filter_in_subsess = dict(subsession_id__in=[subsess_id]) # define querysets for standard models and their links for merging as left index, right index # the order is important! std_models_querysets = ( (Subsession, Subsession.objects.filter(id=subsess_id), (None, None)), (Group, Group.objects.filter(**filter_in_subsess), ('subsession.id', 'group.subsession_id')), (Player, Player.objects.filter(**filter_in_subsess), ('group.id', 'player.group_id')), ) # create a dataframe for this subsession's complete data incl. custom models data df = get_dataframe_from_linked_models(std_models_querysets, links_to_custom_models, std_models_colnames, custom_models_colnames) # sanitize each value df = df.applymap(sanitize_pdvalue_for_live_update)\ .rename(columns={'group.id_in_subsession': 'player.group'})[all_colnames] yield df.to_dict(orient='split')['data']
def get_rows_for_data_tab_app(session, app_name): models_module = get_models_module(app_name) Player = models_module.Player Group = models_module.Group Subsession = models_module.Subsession pfields, gfields, sfields = get_fields_for_data_tab(app_name) players = Player.values_dicts(session=session) players_by_round = defaultdict(list) for p in players: players_by_round[p['round_number']].append(p) groups = {g['id']: g for g in Group.values_dicts(session=session)} subsessions = { s['id']: s for s in Subsession.values_dicts(session=session) } for round_number in range(1, len(subsessions) + 1): table = [] for p in players_by_round[round_number]: g = groups[p['group_id']] tweak_player_values_dict(p, g['id_in_subsession']) s = subsessions[p['subsession_id']] row = ([p[fname] for fname in pfields] + [g[fname] for fname in gfields] + [s[fname] for fname in sfields]) table.append([sanitize_for_csv(v) for v in row]) yield table
def get_context_data(self, **kwargs): form = kwargs['form'] models_module = get_models_module(form.app_name.data) subsession = models_module.Subsession.objects_get( session=self.session, round_number=form.round_number.data) vars_for_admin_report = subsession.vars_for_admin_report() or {} self.debug_tables = [ DebugTable(title='vars_for_admin_report', rows=vars_for_admin_report.items()) ] app_label = subsession.get_folder_name() user_template = get_template_name_if_exists([ f'{app_label}/admin_report.html', f'{app_label}/AdminReport.html' ]) context = super().get_context_data( subsession=subsession, Constants=models_module.Constants, user_template=user_template, **kwargs, ) # it's passed by parent class assert 'session' in context # this should take priority, in the event of a clash between # a user-defined var and a built-in one context.update(vars_for_admin_report) return context
def _get_session_lookups(session_code) -> Dict[int, PageLookup]: session = Session.objects.get(code=session_code) pages = {} idx = 1 for app_name in session.config['app_sequence']: models = get_models_module(app_name) page_sequence = get_pages_module(app_name).page_sequence subsessions = { s['round_number']: s['id'] for s in models.Subsession.objects.filter(session=session).values( 'id', 'round_number' ) } 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.pk, 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 constants(helper: AppCheckHelper, app_name): if not helper.module_exists('models'): return models = get_models_module(app_name) if not hasattr(models, 'Constants'): helper.add_error('models.py does not contain Constants class', numeric_id=11) return Constants = models.Constants attrs = ['name_in_url', 'players_per_group', 'num_rounds'] for attr_name in attrs: if not hasattr(Constants, attr_name): msg = "models.py: 'Constants' class needs to define '{}'" helper.add_error(msg.format(attr_name), numeric_id=12) ppg = Constants.players_per_group if ppg == 0 or ppg == 1: helper.add_error( "models.py: Constants.players_per_group cannot be {}. You " "should set it to None, which makes the group " "all players in the subsession.".format(ppg), numeric_id=13, ) if ' ' in Constants.name_in_url: helper.add_error( "models.py: Constants.name_in_url must not contain spaces", numeric_id=14)
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 __init__( self, case_number: int, participant_bot: ParticipantBot, lookup: ParticipantToPlayerLookup, ): app_name = lookup.app_name models_module = get_models_module(app_name) self.PlayerClass = models_module.Player self.GroupClass = models_module.Group self.SubsessionClass = models_module.Subsession self._player_pk = lookup.player_pk self._subsession_pk = lookup.subsession_pk self._session_pk = lookup.session_pk self._participant_pk = lookup.participant_id self.participant_bot = participant_bot if case_number == None: # default to case 0 case_number = 0 cases = self.cases if len(cases) >= 1: self.case = cases[case_number % len(cases)] else: self.case = None
def get_context_data(self, **kwargs): cleaned_data = kwargs['form'].cleaned_data models_module = get_models_module(cleaned_data['app_name']) subsession = models_module.Subsession.objects.get( session=self.session, round_number=cleaned_data['round_number'] ) vars_for_admin_report = subsession.vars_for_admin_report() or {} self.debug_tables = [ DebugTable( title='vars_for_admin_report', rows=vars_for_admin_report.items() ) ] # determine whether to display debug tables self.is_debug = settings.DEBUG app_label = subsession._meta.app_config.label user_template = select_template( [f'{app_label}/admin_report.html', f'{app_label}/AdminReport.html'] ) context = super().get_context_data( subsession=subsession, Constants=models_module.Constants, user_template=user_template, **kwargs, ) # it's passed by parent class assert 'session' in context # this should take priority, in the event of a clash between # a user-defined var and a built-in one context.update(vars_for_admin_report) return context
def __init__( self, case_number: int, app_name, player_pk: int, subsession_pk: int, session_pk, participant_code, ): models_module = get_models_module(app_name) self.PlayerClass = models_module.Player self.GroupClass = models_module.Group self.SubsessionClass = models_module.Subsession self._player_pk = player_pk self._subsession_pk = subsession_pk self._session_pk = session_pk self._participant_code = participant_code if case_number == None: # default to case 0 case_number = 0 cases = self.cases if len(cases) >= 1: self.case = cases[case_number % len(cases)] else: self.case = None
def vars_for_template(self): session = self.session tables = [] field_headers = {} app_names_by_subsession = [] round_numbers_by_subsession = [] for app_name in session.config['app_sequence']: models_module = get_models_module(app_name) num_rounds = models_module.Subsession.objects_filter( session=session).count() pfields, gfields, sfields = export.get_fields_for_data_tab( app_name) field_headers[app_name] = pfields + gfields + sfields for round_number in range(1, num_rounds + 1): table = dict( pfields=pfields, gfields=gfields, sfields=sfields, ) tables.append(table) app_names_by_subsession.append(app_name) round_numbers_by_subsession.append(round_number) return dict( tables=tables, field_headers_json=json.dumps(field_headers), app_names_by_subsession=app_names_by_subsession, round_numbers_by_subsession=round_numbers_by_subsession, )
def custom_export_app(app_name, fp): models_module = get_models_module(app_name) qs = models_module.Player.objects.select_related('participant', 'group', 'subsession', 'session').order_by('id') rows = models_module.custom_export(qs) # convert to strings so we don't get errors especially for Excel str_rows = [] for row in rows: str_rows.append([str(ele) for ele in row]) _export_csv(fp, str_rows)
def is_ready(self, *, app_name, player_id, page_index, session_pk): models_module = get_models_module(app_name) group_id_in_subsession = (models_module.Group.objects.filter( player__id=player_id).values_list('id_in_subsession', flat=True).get()) return CompletedGroupWaitPage.objects.filter( page_index=page_index, id_in_subsession=int(group_id_in_subsession), session_id=session_pk, ).exists()
def get_custom_models_conf_per_app(session): """ Get the custom models configuration dict for all apps in running in `session`. """ custom_models_conf_per_app = {} for app_name in session.config['app_sequence']: models_module = get_models_module(app_name) conf = get_custom_models_conf(models_module, for_action='data_view') if conf: custom_models_conf_per_app[app_name] = conf return custom_models_conf_per_app
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 vars_for_template(self): session = self.session custom_models_conf_per_app = get_custom_models_conf_per_app(session) if not custom_models_conf_per_app: # no custom models -> use default oTree method return super(SessionDataExtension, self).vars_for_template() tables = [] field_headers = {} app_names_by_subsession = [] round_numbers_by_subsession = [] for app_name in session.config['app_sequence']: models_module = get_models_module(app_name) num_rounds = models_module.Subsession.objects.filter( session=session).count() custom_models_conf = get_custom_models_conf(models_module, for_action='data_view') # find out column names for custom models custom_models_colnames = get_custom_models_columns( custom_models_conf, for_action='data_view') pfields, gfields, sfields = export.get_fields_for_data_tab( app_name) gfields = [c for c in gfields if c != 'id_in_subsession'] std_models_colnames = dict( zip(('player', 'group', 'subsession'), (pfields, gfields, sfields))) # all displayed columns in their order field_headers[app_name] = combine_column_names( std_models_colnames, custom_models_colnames) for round_number in range(1, num_rounds + 1): table = dict(pfields=pfields, cfields=custom_models_colnames, gfields=gfields, sfields=sfields) tables.append(table) app_names_by_subsession.append(app_name) round_numbers_by_subsession.append(round_number) return dict( tables=tables, field_headers_json=json.dumps(field_headers), app_names_by_subsession=app_names_by_subsession, round_numbers_by_subsession=round_numbers_by_subsession, )
def app_sequence_display(self): app_sequence = [] for app_name in self['app_sequence']: models_module = get_models_module(app_name) num_rounds = models_module.Constants.num_rounds if num_rounds > 1: formatted_app_name = '{} ({} rounds)'.format(app_name, num_rounds) else: formatted_app_name = app_name subsssn = { 'doc': getattr(models_module, 'doc', ''), 'name': formatted_app_name, } app_sequence.append(subsssn) return app_sequence
def make_bots(*, session_pk, case_number, use_browser_bots) -> List[ParticipantBot]: update_kwargs = {Participant._is_bot: True} if use_browser_bots: update_kwargs[Participant.is_browser_bot] = True Participant.objects_filter(session_id=session_pk).update(update_kwargs) bots = [] # can't use .distinct('player_pk') because it only works on Postgres # this implicitly orders by round also session = Session.objects_get(id=session_pk) participant_codes = values_flat(session.pp_set.order_by('id'), Participant.code) player_bots_dict = {pcode: [] for pcode in participant_codes} for app_name in session.config['app_sequence']: bots_module = get_bots_module(app_name) models_module = get_models_module(app_name) Player = models_module.Player players = (Player.objects_filter(session_id=session_pk).join( Participant).order_by('round_number').with_entities( Player.id, Participant.code, Player.subsession_id)) for player_id, participant_code, subsession_id in players: player_bot = bots_module.PlayerBot( case_number=case_number, app_name=app_name, player_pk=player_id, subsession_pk=subsession_id, participant_code=participant_code, session_pk=session_pk, ) player_bots_dict[participant_code].append(player_bot) executed_live_methods = set() for participant_code, player_bots in player_bots_dict.items(): bot = ParticipantBot( participant_code, player_bots=player_bots, executed_live_methods=executed_live_methods, ) bots.append(bot) return bots
def make_bots(*, session_pk, case_number, use_browser_bots) -> List[ParticipantBot]: update_kwargs = {'_is_bot': True} if use_browser_bots: update_kwargs['is_browser_bot'] = True Participant.objects.filter(session_id=session_pk).update(**update_kwargs) bots = [] # can't use .distinct('player_pk') because it only works on Postgres # this implicitly orders by round also session = Session.objects.get(pk=session_pk) participant_codes = session.participant_set.order_by('id').values_list( 'code', flat=True) player_bots_dict = {pcode: [] for pcode in participant_codes} for app_name in session.config['app_sequence']: bots_module = get_bots_module(app_name) models_module = get_models_module(app_name) players = (models_module.Player.objects.filter( session_id=session_pk).order_by('round_number').values( 'id', 'participant_id', 'participant__code', 'subsession_id')) for player in players: participant_code = player['participant__code'] player_bot = bots_module.PlayerBot( case_number=case_number, app_name=app_name, player_pk=player['id'], subsession_pk=player['subsession_id'], participant_code=participant_code, session_pk=session_pk, ) player_bots_dict[participant_code].append(player_bot) executed_live_methods = set() for participant_code, player_bots in player_bots_dict.items(): bot = ParticipantBot( participant_code, player_bots=player_bots, executed_live_methods=executed_live_methods, ) bots.append(bot) return bots
def get_urlpatterns(): routes = [] used_names_in_url = set() for app_name in settings.OTREE_APPS: models_module = common.get_models_module(app_name) name_in_url = models_module.Constants.name_in_url if name_in_url in used_names_in_url: msg = ("App {} has Constants.name_in_url='{}', " "which is already used by another app").format( app_name, name_in_url) raise ValueError(msg) used_names_in_url.add(name_in_url) views_module = common.get_pages_module(app_name) routes += url_patterns_from_app_pages(views_module.__name__, name_in_url) routes += url_patterns_from_builtin_module('otree.views.participant') routes += url_patterns_from_builtin_module('otree.views.demo') routes += url_patterns_from_builtin_module('otree.views.admin') routes += url_patterns_from_builtin_module('otree.views.room') routes += url_patterns_from_builtin_module('otree.views.mturk') routes += url_patterns_from_builtin_module('otree.views.export') routes += url_patterns_from_builtin_module('otree.views.rest') routes += websocket_routes routes += [ Mount( '/static', app=OTreeStaticFiles(directory='_static', packages=['otree'] + settings.OTREE_APPS), name="static", ), Route("/favicon.ico", endpoint=Favicon), Route('/', endpoint=HomeRedirect), ] return routes
def _Constants(self) -> BaseConstants: return get_models_module(self.get_folder_name()).Constants
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() with transaction.atomic(): # 2014-5-2: i could implement this by overriding the __init__ on the # Session model, but I don't really know how that works, and it seems # to be a bit discouraged: http://goo.gl/dEXZpv # 2014-9-22: preassign to groups for demo mode. # 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 = Session.objects.create( config=session_config, label=label, is_demo=is_demo, num_participants=num_participants, is_mturk=is_mturk, ) session_code = session.code Participant.objects.bulk_create([ Participant( id_in_session=id_in_session, session=session, _session_code=session_code, ) for id_in_session in list(range(1, num_participants + 1)) ]) participant_values = session.participant_set.order_by('id').values( 'code', 'id') 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 = 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 Subsession.objects.bulk_create([ Subsession(round_number=round_number, session=session) for round_number in round_numbers ]) subsessions = (Subsession.objects.filter( session=session).order_by('round_number').values( '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 subsession in subsessions: for id_in_subsession in range(1, num_groups_per_round + 1): groups_to_create.append( Group( session=session, subsession_id=subsession['id'], round_number=subsession['round_number'], id_in_subsession=id_in_subsession, )) Group.objects.bulk_create(groups_to_create) groups = (Group.objects.filter(session=session).values( 'id_in_subsession', 'subsession_id', 'id').order_by('id_in_subsession')) groups_lookup = defaultdict(list) for group in groups: subsession_id = group['subsession_id'] groups_lookup[subsession_id].append(group['id']) players_to_create = [] for subsession in subsessions: subsession_id = subsession['id'] round_number = subsession['round_number'] participant_index = 0 for group_id in groups_lookup[subsession_id]: for id_in_group in range(1, ppg + 1): participant = participant_values[participant_index] players_to_create.append( Player( session=session, subsession_id=subsession_id, round_number=round_number, participant_id=participant['id'], group_id=group_id, id_in_group=id_in_group, )) participant_index += 1 # Create players Player.objects.bulk_create(players_to_create) session.participant_set.update(_max_page_index=num_pages) with otree.db.idmap.use_cache(): # possible optimization: check if # Subsession.creating_session == BaseSubsession.creating_session # if so, skip it. # but this will only help people who didn't override creating_session # in that case, the session usually creates quickly, anyway. for subsession in session.get_subsessions(): subsession.creating_session() otree.db.idmap.save_objects() # 2017-09-27: moving this inside the transaction session._set_admin_report_app_names() session.save() # we don't need to mark it ready=True here...because it's in a # transaction # this should happen after session.ready = True if room_name is not None: from otree.room import ROOM_DICT room = ROOM_DICT[room_name] room.set_session(session) return session
def model_classes(helper: AppCheckHelper, app_name): models = get_models_module(app_name) for name in ['Subsession', 'Group', 'Player']: if not hasattr(models, name): helper.add_error('MissingModel: Model "%s" not defined' % name, numeric_id=110) Player = models.Player Group = models.Group Subsession = models.Subsession if hasattr(Subsession, 'before_session_starts'): msg = ('before_session_starts no longer exists. ' "You should rename it to creating_session.") helper.add_error(msg, numeric_id=119) player_columns = list(Player.__table__.columns) if any(f.name == 'payoff' for f in player_columns): msg = ('You must remove the field "payoff" from Player, ' "because it is already defined on BasePlayer.") helper.add_error(msg, numeric_id=114) if any(f.name == 'role' for f in player_columns): msg = ('You must remove the field "role" from Player, ' "because it is already defined on BasePlayer.") helper.add_error(msg, numeric_id=114) for Model in [Player, Group, Subsession]: for attr_name in dir(Model): if attr_name not in base_model_attrs[Model.__name__]: try: attr_value = getattr(Model, attr_name) _type = type(attr_value) except AttributeError: # I got "The 'q_country' attribute can only be accessed # from Player instances." # can just filter/ignore these. pass else: if _type in model_field_substitutes.keys(): msg = ( 'NonModelFieldAttr: ' '{model} has attribute "{attr}", which is not a model field, ' 'and will therefore not be saved ' 'to the database.' 'Consider changing to "{attr} = models.{FieldType}(initial={attr_value})"' ).format( model=Model.__name__, attr=attr_name, FieldType=model_field_substitutes[_type], attr_value=repr(attr_value), ) helper.add_error(msg, numeric_id=111) # if people just need an iterable of choices for a model field, # they should use a tuple, not list or dict elif _type in {list, dict, set}: warning = ( 'MutableModelClassAttr: ' '{ModelName}.{attr} is a {type_name}. ' 'Modifying it during a session (e.g. appending or setting values) ' 'will have unpredictable results; ' 'you should use ' 'session.vars or participant.vars instead. ' 'Or, if this {type_name} is read-only, ' "then it's recommended to move it outside of this class " '(e.g. put it in Constants).').format( ModelName=Model.__name__, attr=attr_name, type_name=_type.__name__, ) helper.add_error(warning, numeric_id=112)
def get_urlpatterns(): urlpatterns = [ urls.url(r'^admin/', admin.site.urls), urls.url(r'^accounts/', urls.include('otree.accounts.urls')), urls.url(r'^$', RedirectView.as_view(url='spil/', permanent=True)), urls.url(r'^spil/', GamesView.as_view(), name='games'), urls.url(r'^accounts/login/$', LoginView.as_view(), name='login'), urls.url(r'^accounts/logout/$', LogoutView.as_view(), name='logout'), urls.url(r'^delete/(?P<pk>\d+)/$', DeleteRoom.as_view(), name="delete_view_with_pk"), urls.url(r'^edit/(?P<pk>\d+)/$', UpdateRoom.as_view(), name="update_view_with_pk"), path('create_room/', CreateRoom.as_view(), name='create_room'), # Daytrader rules intro pages urls.url(r'^daytrader-introduction/$', DaytraderIntroView.as_view(), name="daytrader-intro"), urls.url(r'^daytrader-introduction/1/$', DaytraderIntro1View.as_view(), name="daytrader-intro1"), urls.url(r'^daytrader-introduction/2/$', DaytraderIntro2View.as_view(), name="daytrader-intro2"), urls.url(r'^daytrader-introduction/3/$', DaytraderIntro3View.as_view(), name="daytrader-intro3"), urls.url(r'^daytrader-introduction/4/$', DaytraderIntro4View.as_view(), name="daytrader-intro4"), urls.url(r'^daytrader-introduction/5/$', DaytraderIntro5View.as_view(), name="daytrader-intro5"), urls.url(r'^daytrader-introduction/6/$', DaytraderIntro6View.as_view(), name="daytrader-intro6"), # Bad influence rules intro pages urls.url(r'^bad-influence-introduction/$', BadInfluenceIntroView.as_view(), name="bad-influence-intro"), urls.url(r'^bad-influence-introduction/1/$', BadInfluenceIntro1View.as_view(), name="bad-influence-intro1"), ] urlpatterns += staticfiles_urlpatterns() used_names_in_url = set() for app_name in settings.INSTALLED_OTREE_APPS: models_module = common.get_models_module(app_name) name_in_url = models_module.Constants.name_in_url if name_in_url in used_names_in_url: msg = ("App {} has Constants.name_in_url='{}', " "which is already used by another app").format( app_name, name_in_url) raise ValueError(msg) used_names_in_url.add(name_in_url) views_module = common.get_pages_module(app_name) urlpatterns += url_patterns_from_app_pages(views_module.__name__, name_in_url) urlpatterns += url_patterns_from_builtin_module('otree.views.participant') urlpatterns += url_patterns_from_builtin_module('otree.views.demo') urlpatterns += url_patterns_from_builtin_module('otree.views.admin') urlpatterns += url_patterns_from_builtin_module('otree.views.room') urlpatterns += url_patterns_from_builtin_module('otree.views.mturk') urlpatterns += url_patterns_from_builtin_module('otree.views.export') urlpatterns += extensions_urlpatterns() urlpatterns += extensions_export_urlpatterns() return urlpatterns
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 _Constants(self) -> BaseConstants: return get_models_module(self._meta.app_config.name).Constants
def create_session( session_config_name, *, num_participants, label='', room_name=None, is_mturk=False, is_demo=False, edited_session_config_fields=None, ) -> Session: num_subsessions = 0 edited_session_config_fields = edited_session_config_fields or {} 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()) session_config.update(edited_session_config_fields) # 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() with transaction.atomic(): # 2014-5-2: i could implement this by overriding the __init__ on the # Session model, but I don't really know how that works, and it seems # to be a bit discouraged: http://goo.gl/dEXZpv # 2014-9-22: preassign to groups for demo mode. # 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) if is_mturk: mturk_num_participants = ( num_participants / settings.MTURK_NUM_PARTICIPANTS_MULTIPLE ) else: mturk_num_participants = -1 session = Session.objects.create( config=session_config, label=label, is_demo=is_demo, num_participants=num_participants, mturk_num_participants=mturk_num_participants, ) # type: Session Participant.objects.bulk_create( [ Participant(id_in_session=id_in_session, session=session) for id_in_session in list(range(1, num_participants + 1)) ] ) participant_values = session.participant_set.order_by('id').values('code', 'id') ParticipantLockModel.objects.bulk_create( [ ParticipantLockModel(participant_code=participant['code']) for participant in participant_values ] ) participant_to_player_lookups = [] page_index = 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 = models_module.Constants num_subsessions += Constants.num_rounds round_numbers = list(range(1, Constants.num_rounds + 1)) Subsession = models_module.Subsession Group = models_module.Group Player = models_module.Player Subsession.objects.bulk_create( [ Subsession(round_number=round_number, session=session) for round_number in round_numbers ] ) subsessions = ( Subsession.objects.filter(session=session) .order_by('round_number') .values('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 subsession in subsessions: for id_in_subsession in range(1, num_groups_per_round + 1): groups_to_create.append( Group( session=session, subsession_id=subsession['id'], round_number=subsession['round_number'], id_in_subsession=id_in_subsession, ) ) Group.objects.bulk_create(groups_to_create) groups = ( Group.objects.filter(session=session) .values('id_in_subsession', 'subsession_id', 'id') .order_by('id_in_subsession') ) groups_lookup = defaultdict(list) for group in groups: subsession_id = group['subsession_id'] groups_lookup[subsession_id].append(group['id']) players_to_create = [] for subsession in subsessions: subsession_id = subsession['id'] round_number = subsession['round_number'] participant_index = 0 for group_id in groups_lookup[subsession_id]: for id_in_group in range(1, ppg + 1): participant = participant_values[participant_index] players_to_create.append( Player( session=session, subsession_id=subsession_id, round_number=round_number, participant_id=participant['id'], group_id=group_id, id_in_group=id_in_group, ) ) participant_index += 1 # Create players Player.objects.bulk_create(players_to_create) players_flat = Player.objects.filter(session=session).values( 'id', 'participant__code', 'participant__id', 'subsession__id', 'round_number', ) players_by_round = [[] for _ in range(Constants.num_rounds)] for p in players_flat: players_by_round[p['round_number'] - 1].append(p) for round_number, round_players in enumerate(players_by_round, start=1): for View in views_module.page_sequence: page_index += 1 for p in round_players: participant_code = p['participant__code'] url = View.get_url( participant_code=participant_code, name_in_url=Constants.name_in_url, page_index=page_index, ) participant_to_player_lookups.append( ParticipantToPlayerLookup( participant_id=p['participant__id'], participant_code=participant_code, page_index=page_index, app_name=app_name, player_pk=p['id'], subsession_pk=p['subsession__id'], session_pk=session.pk, url=url, ) ) ParticipantToPlayerLookup.objects.bulk_create(participant_to_player_lookups) session.participant_set.update(_max_page_index=page_index) with otree.db.idmap.use_cache(): # possible optimization: check if # Subsession.creating_session == BaseSubsession.creating_session # if so, skip it. # but this will only help people who didn't override creating_session # in that case, the session usually creates quickly, anyway. for subsession in session.get_subsessions(): subsession.before_session_starts() subsession.creating_session() otree.db.idmap.save_objects() # 2017-09-27: moving this inside the transaction session._set_admin_report_app_names() session.save() # we don't need to mark it ready=True here...because it's in a # transaction # this should happen after session.ready = True if room_name is not None: from otree.room import ROOM_DICT room = ROOM_DICT[room_name] room.set_session(session) return session
{ 'url_name': f'markets_export_{session_config["name"]}_{generator_class.__name__}', 'link_text': generator_class.download_link_text, } for generator_class in output_generators ] } return TemplateResponse(request, 'otree_markets/MarketOutputSessionsView.html', context) return MarketOutputSessionsView markets_export_views = [] markets_export_urls = [] for session_config in SESSION_CONFIGS_DICT.values(): # if there aren't any markets apps in the app sequence, don't make an output page for them if not any(issubclass(get_models_module(app_name).Group, MarketGroup) for app_name in session_config['app_sequence']): continue # output_generators is a list of subclasses of output.BaseMarketOutputGenerator # if no output generators are provided by an app, just include the default json generator output_generators = [] for app_name in session_config['app_sequence']: try: output_module = import_module(f'{app_name}.output') output_generators.extend(output_module.output_generators) except (ImportError, AttributeError): continue if not output_generators: output_generators.append(DefaultJSONMarketOutputGenerator) markets_export_views.append(make_sessions_view(session_config, output_generators))
def get_urlpatterns(): urlpatterns = [ urls.url(r'^$', RedirectView.as_view(url='/demo', permanent=True)), urls.url(r'^accounts/login/$', LoginView.as_view(), name='login'), urls.url(r'^accounts/logout/$', LogoutView.as_view(), name='logout'), urls.url(r'^favicon\.ico$', RedirectView.as_view(url='/static/favicon.ico')), ] urlpatterns += staticfiles_urlpatterns() used_names_in_url = set() for app_name in settings.INSTALLED_OTREE_APPS: models_module = common.get_models_module(app_name) name_in_url = models_module.Constants.name_in_url if name_in_url in used_names_in_url: msg = ( "App {} has Constants.name_in_url='{}', " "which is already used by another app" ).format(app_name, name_in_url) raise ValueError(msg) used_names_in_url.add(name_in_url) views_module = common.get_pages_module(app_name) urlpatterns += url_patterns_from_app_pages(views_module.__name__, name_in_url) urlpatterns += url_patterns_from_builtin_module('otree.views.participant') urlpatterns += url_patterns_from_builtin_module('otree.views.demo') urlpatterns += url_patterns_from_builtin_module('otree.views.admin') urlpatterns += url_patterns_from_builtin_module('otree.views.room') urlpatterns += url_patterns_from_builtin_module('otree.views.mturk') urlpatterns += url_patterns_from_builtin_module('otree.views.export') urlpatterns += url_patterns_from_builtin_module('otree.views.rest') urlpatterns += extensions_urlpatterns() urlpatterns += extensions_export_urlpatterns() # serve an empty favicon? # otherwise, the logs will contain: # [WARNING] django.request > Not Found: /favicon.ico # Not Found: /favicon.ico # don't want to add a <link> in base template because even if it exists, # browsers will still request /favicon.ico. # plus it makes the HTML noisier # can't use the static() function here because maybe collectstatic # has not been run yet # and it seems an empty HttpResponse or even a 204 response makes the browser # just keep requesting the file with every page load # hmmm...now it seems that chrome is not re-requesting with every page load # but firefox does. but if i remove the favicon, there's 1 404 then FF doesn't # ask for it again. # import os # dir_path = os.path.dirname(os.path.realpath(__file__)) # with open(os.path.join(dir_path, 'favicon_invisible.ico'), 'rb') as f: # #with open('favicon.ico', 'rb') as f: # favicon_content = f.read() # # # urlpatterns.append( # urls.url( # r'^favicon\.ico$', # lambda request: HttpResponse(favicon_content, content_type="image/x-icon") # ) # ) return urlpatterns
def get_fields_for_data_tab(app_name): models_module = get_models_module(app_name) for Model in [ models_module.Player, models_module.Group, models_module.Subsession ]: yield _get_table_fields(Model, for_export=False)
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