예제 #1
0
def pages_function(helper: AppCheckHelper, app_name):
    pages_module = common.get_pages_module(app_name)
    try:
        page_list = pages_module.page_sequence
    except:
        helper.add_error('pages.py is missing the variable page_sequence.',
                         numeric_id=21)
        return
    else:
        for i, ViewCls in enumerate(page_list):
            # there is no good reason to include Page in page_sequence.
            # As for WaitPage: even though it works fine currently
            # and can save the effort of subclassing,
            # we should restrict it, because:
            # - one user had "class WaitPage(Page):".
            # - if someone makes "class WaitPage(WaitPage):", they might
            #   not realize why it's inheriting the extra behavior.
            # overall, I think the small inconvenience of having to subclass
            # once per app
            # is outweighed by the unexpected behavior if someone subclasses
            # it without understanding inheritance.
            # BUT: built-in Trust game had a wait page called WaitPage.
            # that was fixed on Aug 24, 2017, need to wait a while...
            # see below in ensure_no_misspelled_attributes,
            # we can get rid of a check there also
            if ViewCls.__name__ == 'Page':
                msg = "page_sequence cannot contain a class called 'Page'."
                helper.add_error(msg, numeric_id=22)
            if ViewCls.__name__ == 'WaitPage' and app_name != 'trust':
                msg = "page_sequence cannot contain a class called 'WaitPage'."
                helper.add_error(msg, numeric_id=221)

            if issubclass(ViewCls, WaitPage):
                if ViewCls.group_by_arrival_time:
                    if i > 0:
                        helper.add_error(
                            '"{}" has group_by_arrival_time=True, so '
                            'it must be placed first in page_sequence.'.format(
                                ViewCls.__name__),
                            numeric_id=23,
                        )
                    if ViewCls.wait_for_all_groups:
                        helper.add_error(
                            'Page "{}" has group_by_arrival_time=True, so '
                            'it cannot have wait_for_all_groups=True also.'.
                            format(ViewCls.__name__),
                            numeric_id=24,
                        )
                    if hasattr(ViewCls, 'get_players_for_group'):
                        helper.add_error(
                            'Page "{}" defines get_players_for_group, which is deprecated. '
                            'You should instead define group_by_arrival_time_method on the Subsession. '
                            ''.format(ViewCls.__name__),
                            numeric_id=25,
                        )
            elif issubclass(ViewCls, Page):
                pass  # ok
            else:
                msg = '"{}" is not a valid page'.format(ViewCls)
                helper.add_error(msg, numeric_id=26)
예제 #2
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
예제 #3
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
예제 #4
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
예제 #5
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
예제 #6
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
예제 #7
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
예제 #8
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
예제 #9
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