Beispiel #1
0
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']
Beispiel #2
0
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
Beispiel #3
0
    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
Beispiel #4
0
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
Beispiel #5
0
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)
Beispiel #6
0
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
Beispiel #7
0
    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
Beispiel #8
0
    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
Beispiel #9
0
    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
Beispiel #10
0
    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,
        )
Beispiel #11
0
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)
Beispiel #12
0
    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()
Beispiel #13
0
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
Beispiel #14
0
    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,
        )
Beispiel #15
0
    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,
        )
Beispiel #16
0
 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
Beispiel #17
0
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
Beispiel #18
0
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
Beispiel #19
0
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
Beispiel #20
0
 def _Constants(self) -> BaseConstants:
     return get_models_module(self.get_folder_name()).Constants
Beispiel #21
0
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
Beispiel #22
0
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)
Beispiel #23
0
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
Beispiel #24
0
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]
Beispiel #25
0
 def _Constants(self) -> BaseConstants:
     return get_models_module(self._meta.app_config.name).Constants
Beispiel #26
0
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
Beispiel #27
0
                    {
                        '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))
Beispiel #28
0
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
Beispiel #29
0
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)
Beispiel #30
0
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