from django.db.models import Prefetch import otree.common from otree.db import models from otree.common import get_models_module, in_round, in_rounds import copy from otree.common import has_group_by_arrival_time, add_field_tracker from django.apps import apps class GroupMatrixError(ValueError): pass class RoundMismatchError(GroupMatrixError): pass class BaseSubsession(models.Model): """Base class for all Subsessions. """ class Meta: abstract = True ordering = ['pk'] index_together = ['session', 'round_number'] session = models.ForeignKey( 'otree.Session', related_name='%(app_label)s_%(class)s', null=True, on_delete=models.CASCADE, ) round_number = models.PositiveIntegerField( db_index=True, doc='''If this subsession is repeated (i.e. has multiple rounds), this field stores the position of this subsession, among subsessions in the same app. ''', ) 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.pk) 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('pk')) def get_group_matrix(self): players_prefetch = Prefetch( 'player_set', queryset=self._PlayerClass().objects.order_by('id_in_group'), to_attr='_ordered_players', ) return [ group._ordered_players for group in self.group_set.order_by('id_in_subsession').prefetch_related( players_prefetch ) ] def set_group_matrix(self, matrix): """ warning: this deletes the groups and any data stored on them TODO: we should indicate this in docs """ 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.pk 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 = list( self.player_set.values_list('pk', flat=True).order_by('pk') ) 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) # Before deleting groups, Need to set the foreignkeys to None # 2016-11-8: does this need to be in a transaction? # because what if a player refreshes their page while this is going # on? self.player_set.update(group=None) self.group_set.all().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 = self.in_round(round_number) group_matrix = [ group._ordered_players for group in previous_round.group_set.order_by( 'id_in_subsession' ).prefetch_related( Prefetch( 'player_set', queryset=self._PlayerClass().objects.order_by('id_in_group'), to_attr='_ordered_players', ) ) ] for i, group_list in enumerate(group_matrix): for j, player in enumerate(group_list): # for every entry (i,j) in the matrix, follow the pointer # to the same person in the next round group_matrix[i][j] = player.in_round(self.round_number) self.set_group_matrix(group_matrix) @property def _Constants(self): return get_models_module(self._meta.app_config.name).Constants def _GroupClass(self): return apps.get_model(self._meta.app_config.label, 'Group') def _PlayerClass(self): return apps.get_model(self._meta.app_config.label, 'Player') @classmethod def _has_group_by_arrival_time(cls): return has_group_by_arrival_time(cls._meta.app_config.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 before_session_starts(self): '''Deprecated and renamed to creating_session''' pass def creating_session(self): pass def vars_for_admin_report(self): return {} @classmethod def _ensure_required_fields(cls): add_field_tracker(cls)