예제 #1
0
    class Flags(Variable):
        __table_args__ = ({"keep_existing": True}, )
        __tablename__ = "openfred_flags"
        id = C(BI, FK(Variable.id), primary_key=True)
        flag_ks = C(ARRAY(Int), nullable=False)
        flag_vs = C(ARRAY(Str(37)), nullable=False)
        __mapper_args_ = {"polymorphic_identity": "flags"}

        @property
        def flag(self, key):
            flags = dict(zip(self.flag_ks, self.flag_vs))
            return flags[key]
예제 #2
0
 class Variable(Base):
     __table_args__ = ({"keep_existing": True}, )
     __tablename__ = "openfred_variables"
     id = C(BI, primary_key=True)
     name = C(Str(255), nullable=False, unique=True)
     # TODO: Figure out whether and where this is in the '.nc' files.
     type = C(Str(37))
     netcdf_attributes = C(JSON)
     description = C(Text)
     standard_name = C(Str(255))
     __mapper_args_ = {
         "polymorphic_identity": "variable",
         "polymorphic_on": type,
     }
예제 #3
0
 class Series(Base):
     __tablename__ = "openfred_series"
     __table_args__ = (
         UC("height", "location_id", "timespan_id", "variable_id"),
         {
             "keep_existing": True
         },
     )
     id = C(BI, primary_key=True)
     values = C(ARRAY(Float), nullable=False)
     height = C(Float)
     timespan_id = C(BI, FK(classes["Timespan"].id), nullable=False)
     location_id = C(BI, FK(classes["Location"].id), nullable=False)
     variable_id = C(BI, FK(classes["Variable"].id), nullable=False)
     timespan = relationship(classes["Timespan"], backref="series")
     location = relationship(classes["Location"], backref="series")
     variable = relationship(classes["Variable"], backref="series")
예제 #4
0
class OktmoObj(Base):
    __tablename__ = 'oktmo'

    CLS_ENUM = (
        u'сф',  # субъект
        u'мр',  # муниципальный район
        u'го',  # городской округ
        u'гп',  # городское поселение
        u'сп',  # сельское поселение
        u'мс',  # межселенная территория
        u'тгфз'  # внутригородская территория города федерального значения
    )

    id = C(BigInteger, primary_key=True)
    code = C(Unicode(8))  # код ОКТМО
    raw = C(Unicode(255))  # строка из ОКТМО
    parent = C(Unicode(8))  # родитель с учетом группировки
    parent_obj = C(Unicode(8))  # родетель без учета группировок
    is_group = C(Boolean())  # это группировка ?

    lvl = C(SmallInteger())  # уровень без учета группировок
    cls = C(Enum(*CLS_ENUM, **{'native_enum': False}))  # класс

    is_subject = C(Boolean())  # это субъект?
    simple_name = C(Unicode(255))  # упрощенное название

    def parse(self, lookup=None):
        self.id = int(self.code)

        # убираем сноску с конца строки
        if self.raw[-1] == '*':
            self.raw = self.raw[:-1]

        # код заканчивается на n нулей
        zeroes = lambda n: self.code.endswith('0' * n)

        # первые n cимволов кода
        first = lambda n: self.code[:n]

        # дополненные нулями
        first_fill = lambda n: fill(self.code[:n])

        p1 = int(self.code[2])  # признак 1 - 3-й разряд
        p2 = int(self.code[5])  # признак 2 - 6-й разряд

        g1 = int(self.code[3:5])
        g2 = int(self.code[6:8])

        if self.raw[-1] == '/':
            self.is_group = True

            if zeroes(5):
                # субъекты закодированные на втором уровне
                if self.code[:3] in SUBJ_AD:
                    self.is_group = False
                    self.is_subject = True
                    self.cls = 'сф'
                self.parent = first_fill(2)
            elif zeroes(4):
                # группировка МР и ГО внутри авт. округов
                self.parent = first_fill(3)
            elif zeroes(2):
                self.parent = first_fill(5)
        else:
            self.is_group = False

            if zeroes(6):
                self.lvl = 1
                self.is_subject = True
                self.cls = 'сф'

            elif zeroes(3):
                self.lvl = 2
                self.parent_obj = fill(first(2))
                if p1 == 8:
                    if g1 in range(10, 50):
                        self.cls = 'мр'
                    elif g1 in range(50, 99):
                        self.cls = 'го'
                elif p1 == 6:
                    self.cls = 'мр'
                elif p1 == 7:
                    self.cls = 'го'
                elif p1 == 3:
                    self.cls = 'тгфз'
                elif p1 == 9:
                    if lookup(OktmoObj, OktmoObj.code == fill(first(2) + '3')):
                        self.cls = 'тгфз'
                    elif g1 in range(11, 50):
                        self.cls = 'мр'
                    elif g1 in range(50, 100):
                        self.cls = 'го'

                if not self.cls:
                    stderr.write(u'Неопознаный признак P1 %d в %s %s\n' %
                                 (p1, self.code, self.raw))

            else:
                self.lvl = 3
                if p2 == 1:
                    self.cls = 'гп'
                elif p2 == 4:
                    self.cls = 'сп'
                elif p2 == 7:
                    self.cls = 'мс'

                if not self.cls:
                    stderr.write(u'Неопознаный признак P2 %d в %s %s\n' %
                                 (p2, self.code, self.raw))

            if self.lvl > 1 and not self.parent:
                self.parent = fill(self.code[:{2: 3, 3: 6}[self.lvl]])

        # parent_obj
        if self.parent:
            op = lookup(OktmoObj, OktmoObj.code == self.parent)
            if not op:
                stderr.write(u'Неверный родитель для %s %s\n' %
                             (self.code, self.raw))
            elif not op.is_group:
                self.parent_obj = op.code
            elif op.is_group and op.parent_obj:
                self.parent_obj = op.parent_obj
            else:
                stderr.write(u"Двойная группировка: %s %s\n" %
                             (self.code, self.raw))

        if not self.is_group:
            self.simple_name = self.raw
            for r in SIMPLE_NAME_REs:
                if self.cls in r[0]:
                    for p in r[1]:
                        exp = re.compile(p[0], re.UNICODE + re.IGNORECASE)
                        self.simple_name = exp.sub(p[1], self.simple_name)
예제 #5
0
class OktmoOkatoObj(Base):
    __tablename__ = 'oktmo_okato'
    oktmo = C(Unicode(8), primary_key=True)
    okato = C(Unicode(11), primary_key=True)
    settlement_name = C(Unicode(100))
예제 #6
0
def mapped_classes(metadata):
    """ Returns classes mapped to the openFRED database via SQLAlchemy.

    The classes are dynamically created and stored in a dictionary keyed by
    class names. The dictionary also contains the special entry `__Base__`,
    which an SQLAlchemy `declarative_base` instance used as the base class from
    which all mapped classes inherit.
    """

    Base = declarative_base(metadata=metadata)
    classes = {"__Base__": Base}

    def map(name, registry, namespace):
        namespace["__tablename__"] = "openfred_" + name.lower()
        namespace["__table_args__"] = namespace.get("__table_args__", ()) + ({
            "keep_existing":
            True
        }, )
        if namespace["__tablename__"][-1] != "s":
            namespace["__tablename__"] += "s"
        registry[name] = type(name, (registry["__Base__"], ), namespace)

    map(
        "Timespan",
        classes,
        {
            "id": C(BI, primary_key=True),
            "start": C(DT),
            "stop": C(DT),
            "resolution": C(Interval),
            "segments": C(ARRAY(DT, dimensions=2)),
            "__table_args__": (UC("start", "stop", "resolution"), ),
        },
    )
    map(
        "Location",
        classes,
        {
            "id":
            C(BI, primary_key=True),
            "point":
            C(
                geotypes.Geometry(geometry_type="POINT", srid=4326),
                unique=True,
            ),
        },
    )

    # TODO: Handle units.
    class Variable(Base):
        __table_args__ = ({"keep_existing": True}, )
        __tablename__ = "openfred_variables"
        id = C(BI, primary_key=True)
        name = C(Str(255), nullable=False, unique=True)
        # TODO: Figure out whether and where this is in the '.nc' files.
        type = C(Str(37))
        netcdf_attributes = C(JSON)
        description = C(Text)
        standard_name = C(Str(255))
        __mapper_args_ = {
            "polymorphic_identity": "variable",
            "polymorphic_on": type,
        }

    classes["Variable"] = Variable

    class Flags(Variable):
        __table_args__ = ({"keep_existing": True}, )
        __tablename__ = "openfred_flags"
        id = C(BI, FK(Variable.id), primary_key=True)
        flag_ks = C(ARRAY(Int), nullable=False)
        flag_vs = C(ARRAY(Str(37)), nullable=False)
        __mapper_args_ = {"polymorphic_identity": "flags"}

        @property
        def flag(self, key):
            flags = dict(zip(self.flag_ks, self.flag_vs))
            return flags[key]

    classes["Flags"] = Flags

    class Series(Base):
        __tablename__ = "openfred_series"
        __table_args__ = (
            UC("height", "location_id", "timespan_id", "variable_id"),
            {
                "keep_existing": True
            },
        )
        id = C(BI, primary_key=True)
        values = C(ARRAY(Float), nullable=False)
        height = C(Float)
        timespan_id = C(BI, FK(classes["Timespan"].id), nullable=False)
        location_id = C(BI, FK(classes["Location"].id), nullable=False)
        variable_id = C(BI, FK(classes["Variable"].id), nullable=False)
        timespan = relationship(classes["Timespan"], backref="series")
        location = relationship(classes["Location"], backref="series")
        variable = relationship(classes["Variable"], backref="series")

    classes["Series"] = Series

    return classes
예제 #7
0
class BaseGroup(SSPPGModel, MixinSessionFK):
    __abstract__ = True

    id_in_subsession = C(st.Integer, index=True)

    round_number = C(st.Integer, index=True)

    @property
    def _Constants(self) -> BaseConstants:
        return get_models_module(self.get_folder_name()).Constants

    def __unicode__(self):
        return str(self.id)

    def get_players(self):
        return list(self.player_set.order_by('id_in_group'))

    def get_player_by_id(self, id_in_group):
        try:
            return self.player_set.filter_by(id_in_group=id_in_group).one()
        except NoResultFound:
            msg = 'No player with id_in_group {}'.format(id_in_group)
            raise ValueError(msg) from None

    def get_player_by_role(self, role):
        if get_roles(self._Constants):
            try:
                return self.player_set.filter_by(_role=role).one()
            except NoResultFound:
                pass
        else:
            for p in self.get_players():
                if p.role() == role:
                    return p
        msg = f'No player with role "{role}"'
        raise ValueError(msg)

    def set_players(self, players_list):
        Constants = self._Constants
        roles = get_roles(Constants)
        for i, player in enumerate(players_list, start=1):
            player.group = self
            player.id_in_group = i
            player._role = get_role(roles, i)
        db.commit()

    def in_round(self, round_number):
        try:
            return in_round(
                type(self),
                round_number,
                session=self.session,
                id_in_subsession=self.id_in_subsession,
            )
        except InvalidRoundError as exc:
            msg = (str(exc) + '; ' +
                   ('Hint: you should not use this '
                    'method if you are rearranging groups between rounds.'))
            ExceptionClass = type(exc)
            raise ExceptionClass(msg) from None

    def in_rounds(self, first, last):
        try:
            return in_rounds(
                type(self),
                first,
                last,
                session=self.session,
                id_in_subsession=self.id_in_subsession,
            )
        except InvalidRoundError as exc:
            msg = (str(exc) + '; ' +
                   ('Hint: you should not use this '
                    'method if you are rearranging groups between rounds.'))
            ExceptionClass = type(exc)
            raise ExceptionClass(msg) from None

    def in_previous_rounds(self):
        return self.in_rounds(1, self.round_number - 1)

    def in_all_rounds(self):
        return self.in_previous_rounds() + [self]

    @declared_attr
    def subsession_id(cls):
        app_name = cls.get_folder_name()
        return C(st.Integer, ForeignKey(f'{app_name}_subsession.id'))

    @declared_attr
    def subsession(cls):
        return relationship(f'{cls.__module__}.Subsession',
                            back_populates='group_set')

    @declared_attr
    def player_set(cls):
        return relationship(f'{cls.__module__}.Player',
                            back_populates="group",
                            lazy='dynamic')
예제 #8
0
 def subsession_id(cls):
     app_name = cls.get_folder_name()
     return C(st.Integer, ForeignKey(f'{app_name}_subsession.id'))
예제 #9
0
class OkatoObj(Base):
    __tablename__ = 'okato'

    CLS_ENUM = (
        u'адм_район',  # районы субъекта
        u'город',  # города
        u'пгт',  # поселки городского типа
        u'город|пгт',
        u'гфз_1',  # первый уровень деления ГФЗ: округа Москвы, районы Спб
        u'гфз_2',  # второй уровень деления ГФЗ: районы Москвы, округа Спб
        u'нп',
        u'сельсовет',
        u'unknown',
        u'гор_район'  # район города, или городского округа
    )

    id = C(BigInteger, primary_key=True)
    code = C(Unicode(11), unique=True, nullable=False)  # код ОКАТО
    raw = C(Unicode(255))  # строка из ОКАТО as-is

    is_group = C(Boolean())  # это группировка ?
    parent = C(Unicode(11))  # родитель с учетом группировок
    parent_obj = C(Unicode(11))  # родитель без учета группировок

    lvl = C(SmallInteger())  # уровень
    cls = C(Unicode(10))  # класс

    is_settlement = C(Boolean())  # это населенный пункт
    is_subject = C(Boolean())  # это субъект
    name = C(Unicode(100))  # имя без статусной части
    status = C(Unicode(100))  # статусная часть

    cl_class = None
    cl_level = None
    manager = None

    def parse(self, lookup=None):
        code = self.code
        raw = self.raw
        self.manager = None

        # код заканчивается на n нулей
        zeroes = lambda n: self.code.endswith('0' * n)

        # все группировки заканчиваются на '/'
        self.is_group = raw[-1] == '/'

        p1 = int(code[2])  # признак 1 - разряд 3
        p2 = int(code[5])  # признак 2 - разряд 6
        v1 = int(code[3:5])  # разряды 4-5
        v2 = int(code[6:8])  # разряды 7-8

        level = None

        if self.is_group:
            if len(code) == 8:
                if zeroes(5):
                    self.parent = fill(code[:2])
                elif zeroes(4):
                    pst = int(code[3])
                    while True:
                        p = fill(code[:3] + str(pst))
                        print p
                        po = lookup(OkatoObj, OkatoObj.code == p)
                        if po:
                            self.parent = po.code
                            stderr.write(
                                "[%s] %s > [%s] %s\n" %
                                (self.code, self.raw, po.code, po.raw))
                            break
                        else:
                            pst = pst - 1
                            if pst < 0:
                                stderr.write(
                                    u"Не удалось определить родителя для %s %s\n"
                                    % (self.code, self.raw))
                                break

                elif code.endswith(('00', '50')):
                    self.parent = code[:5] + '000'

            if len(code) == 11:
                assert code[-3:] == '000', 'Ошибка в группировке'
                self.parent = fill(code[:8])
                self.parent_obj = fill(code[:8])

        elif len(code) == 8 and not self.is_group:
            if zeroes(6):
                self.cl_level = 1
                self.parent = None
                self.parent_obj = None
            if zeroes(5):
                # это автономный округ
                self.cls = 'ао'
                self.parent = fill(code[:2])
                self.parent_obj = fill(code[:2])
            elif zeroes(3):
                self.cl_level = 2
                self.parent = code[:3] + '00000'
                self.parent_obj = code[:2] + '000000'
                if p1 == 1:
                    pst = int(code[3])
                    while True:
                        p = fill(code[:3] + str(pst))
                        po = lookup(OkatoObj, OkatoObj.code == p)
                        if po and po.is_group:
                            self.parent = po.code
                            stderr.write(
                                "[%s] %s > [%s] %s\n" %
                                (self.code, self.raw, po.code, po.raw))
                            break
                        else:
                            pst = pst - 1

                elif p1 == 2:
                    self.parent_obj = code[:2] + '000000'
                    if v1 in range(1, 60):
                        self.cl_class = 'адм_район'
                        self.parent = code[:3] + '00000'
                    elif v1 in range(60, 100):
                        self.cl_class = 'гфз_1'
                        self.parent = code[:3] + '60000'

                elif p1 == 4:
                    # по описанию статуc должен зависеть от v1,
                    # но на московской области это не работает,
                    # поэтому город или пгт

                    # попробуем посмотреть группировку верхнего уровня
                    p_code = code[:3] + '00000'
                    parent_group = lookup(
                        OkatoObj, OkatoObj.code == p_code.encode('utf-8'))
                    pr = parent_group.raw

                    if pr.startswith(u'Города'):
                        self.cl_class = 'город'
                    elif pr.startswith(u'Поселки городского типа'):
                        self.cl_class = 'пгт'

                elif p1 == 5:

                    # это значения признака в классификаторе не описано,
                    # на московской области вроде бы работает
                    if v1 in range(1, 60):
                        self.cl_class = 'город'
                    elif v1 in range(60, 100):
                        self.cl_class = 'пгт'

            else:
                self.cl_level = 3
                self.parent = code[:7] + '0'
                self.parent_obj = code[:5] + '000'
                if p2 == 3:
                    self.cl_class = 'гор_район'
                elif p2 == 5:
                    if v2 in range(1, 50):
                        self.cl_class = 'город'
                    elif v2 in range(50, 100):
                        if v1 in range(60, 100):
                            self.cl_class = 'гфз_2'
                        elif v1 in range(1, 60):
                            self.cl_class = 'пгт'
                elif p2 == 6:
                    # в самарской области сюда попадают устраненные НП в Тольяти
                    self.cl_class = 'unknown'
                elif p2 == 8:
                    self.cl_class = 'сельсовет'
        elif len(code) == 11 and not self.is_group:
            self.cl_level = 4
            self.cl_class = 'нп'
            self.parent = fill(code[:8], to_len=11)
            self.parent_obj = fill(code[:8])

        # на первом уровне все субъекты, на втором только то, что еще не успели упразднить
        self.is_subject = self.cl_level == 1 or (code in SUBJ_AD)

        self.is_district = len(code) == 8 and code[2] == '2' and code[
            -3:] == '000' and code[3:5] <> '00'
        self.is_city = len(code) == 8 and code[2] == '4' and code[
            -3:] == '000' and code[3:5] <> '00'

        if self.cl_class in ('город', 'пгт', 'город|пгт'):
            self.is_settlement = True
            if self.cl_class in ('город'):
                self.name = raw
                self.status = self.cl_class
            elif self.cl_class in ('пгт'):
                self.name = raw
                self.status = "поселок городского типа"
        if len(code) == 11 and not self.is_group:
            # сельские НП
            self.is_settlement = True

        self.lvl = self.cl_level
        self.cls = self.cl_class

        # определяем статус
        for ss in _STATUS_SEARCH:
            m = ss[0].match(raw)
            if m:
                (self.name, self.status) = (m.group(1), ss[1])

        if self.is_settlement and not self.status:
            stderr.write(u"Не удалось определить статус НП %s [%s]\n" %
                         (self.code, self.raw))

        if self.code in SUBJ_AD:
            self.parent = fill(self.code[:2])
예제 #10
0
class BaseSubsession(SSPPGModel, MixinSessionFK):
    __abstract__ = True

    round_number = C(
        st.Integer,
        index=True,
    )

    def in_round(self, round_number):
        return in_round(type(self), round_number, session=self.session)

    def in_rounds(self, first, last):
        return in_rounds(type(self), first, last, session=self.session)

    def in_previous_rounds(self):
        return self.in_rounds(1, self.round_number - 1)

    def in_all_rounds(self):
        return self.in_previous_rounds() + [self]

    def __unicode__(self):
        return str(self.id)

    def get_groups(self):
        return list(self.group_set.order_by('id_in_subsession'))

    def get_players(self):
        return list(self.player_set.order_by('id'))

    def _get_group_matrix(self, ids_only=False):
        Player = self._PlayerClass()
        Group = self._GroupClass()
        players = (dbq(Player).join(Group).filter(
            Player.subsession == self).order_by(Group.id_in_subsession,
                                                'id_in_group'))
        d = defaultdict(list)
        for p in players:
            d[p.group.id_in_subsession].append(
                p.id_in_subsession if ids_only else p)
        return list(d.values())

    def get_group_matrix(self):
        return self._get_group_matrix()

    def get_group_matrix_ids(self):
        return self._get_group_matrix(ids_only=True)

    def set_group_matrix(self, matrix):
        """
        warning: this deletes the groups and any data stored on them
        """

        try:
            players_flat = [p for g in matrix for p in g]
        except TypeError:
            raise GroupMatrixError(
                'Group matrix must be a list of lists.') from None
        try:
            matrix_pks = sorted(p.id for p in players_flat)
        except AttributeError:
            # if integers, it's OK
            if isinstance(players_flat[0], int):
                # deep copy so that we don't modify the input arg
                matrix = copy.deepcopy(matrix)
                players_flat = sorted(players_flat)
                players_from_db = self.get_players()
                if players_flat == list(range(1, len(players_from_db) + 1)):
                    for i, row in enumerate(matrix):
                        for j, val in enumerate(row):
                            matrix[i][j] = players_from_db[val - 1]
                else:
                    msg = (
                        'If you pass a matrix of integers to this function, '
                        'It must contain all integers from 1 to '
                        'the number of players in the subsession.')
                    raise GroupMatrixError(msg) from None
            else:
                msg = ('The elements of the group matrix '
                       'must either be Player objects, or integers.')
                raise GroupMatrixError(msg) from None
        else:
            existing_pks = values_flat(self.player_set.order_by('id'), 'id')
            if matrix_pks != existing_pks:
                wrong_round_numbers = [
                    p.round_number for p in players_flat
                    if p.round_number != self.round_number
                ]
                if wrong_round_numbers:
                    msg = ('You are setting the groups for round {}, '
                           'but the matrix contains players from round {}.'.
                           format(self.round_number, wrong_round_numbers[0]))
                    raise GroupMatrixError(msg)
                msg = ('The group matrix must contain each player '
                       'in the subsession exactly once.')
                raise GroupMatrixError(msg)

        self.player_set.update({self._PlayerClass().group_id: None})
        self.group_set.delete()

        GroupClass = self._GroupClass()
        for i, row in enumerate(matrix, start=1):
            group = GroupClass.objects_create(
                subsession=self,
                id_in_subsession=i,
                session=self.session,
                round_number=self.round_number,
            )
            group.set_players(row)

    def group_like_round(self, round_number):
        previous_round: BaseSubsession = self.in_round(round_number)
        group_matrix = previous_round._get_group_matrix(ids_only=True)
        self.set_group_matrix(group_matrix)

    @property
    def _Constants(self):
        return get_models_module(self.get_folder_name()).Constants

    def _GroupClass(self):
        return get_models_module(self.get_folder_name()).Group

    def _PlayerClass(self):
        return get_models_module(self.get_folder_name()).Player

    @classmethod
    def _has_group_by_arrival_time(cls):
        app_name = cls.get_folder_name()
        return has_group_by_arrival_time(app_name)

    def group_randomly(self, *, fixed_id_in_group=False):
        group_matrix = self.get_group_matrix()
        group_matrix = otree.common._group_randomly(group_matrix,
                                                    fixed_id_in_group)
        self.set_group_matrix(group_matrix)

    def creating_session(self):
        pass

    def vars_for_admin_report(self):
        return {}

    def _gbat_try_to_make_new_group(self, page_index):
        '''Returns the group ID of the participants who were regrouped'''
        from otree.models import Participant

        Player = self._PlayerClass()
        STALE_THRESHOLD_SECONDS = 70

        # count how many are re-grouped
        waiting_players = list(
            self.player_set.join(Participant).filter(
                Participant._gbat_is_waiting == True,
                Participant._index_in_pages == page_index,
                Participant._gbat_grouped == False,
                # this is just a failsafe
                Participant._last_request_timestamp >=
                time.time() - STALE_THRESHOLD_SECONDS,
            ))

        try:
            players_for_group = self.group_by_arrival_time_method(
                waiting_players)
        except:
            raise  #  ResponseForException

        if not players_for_group:
            return None

        participants = [p.participant for p in players_for_group]

        group_id_in_subsession = self._gbat_next_group_id_in_subsession()

        Constants = self._Constants

        this_round_new_group = None
        for round_number in range(self.round_number, Constants.num_rounds + 1):
            subsession = self.in_round(round_number)

            unordered_players = subsession.player_set.filter(
                Player.participant_id.in_([pp.id for pp in participants]))

            participant_ids_to_players = {
                player.participant: player
                for player in unordered_players
            }

            ordered_players_for_group = [
                participant_ids_to_players[participant]
                for participant in participants
            ]

            group = self._GroupClass()(
                subsession=subsession,
                id_in_subsession=group_id_in_subsession,
                session=self.session,
                round_number=round_number,
            )
            db.add(group)
            group.set_players(ordered_players_for_group)

            if round_number == self.round_number:
                this_round_new_group = group

            # prune groups without players
            # https://stackoverflow.com/a/21115972/
            for group_to_delete in subsession.group_set.outerjoin(
                    Player).filter(Player.id == None):
                db.delete(group_to_delete)

        for participant in participants:
            participant._gbat_grouped = True
            participant._gbat_is_waiting = False

        return this_round_new_group

    def _gbat_next_group_id_in_subsession(self):
        # 2017-05-05: seems like this can result in id_in_subsession that
        # doesn't start from 1.
        # especially if you do group_by_arrival_time in every round
        # is that a problem?
        Group = self._GroupClass()
        return (dbq(func.max(
            Group.id_in_subsession)).filter_by(session=self.session).scalar() +
                1)

    def group_by_arrival_time_method(self, waiting_players):
        Constants = self._Constants

        if Constants.players_per_group is None:
            msg = (
                'Page "{}": if using group_by_arrival_time, you must either set '
                'Constants.players_per_group to a value other than None, '
                'or define group_by_arrival_time_method.'.format(
                    self.__class__.__name__))
            raise AssertionError(msg)

        if len(waiting_players) >= Constants.players_per_group:
            return waiting_players[:Constants.players_per_group]

    @declared_attr
    def group_set(cls):
        return relationship(f'{cls.__module__}.Group',
                            back_populates="subsession",
                            lazy='dynamic')

    @declared_attr
    def player_set(cls):
        return relationship(f'{cls.__module__}.Player',
                            back_populates="subsession",
                            lazy='dynamic')