from sqlalchemy import Column, ForeignKey from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import relationship from sqlalchemy.sql import sqltypes as st from otree.common import in_round, in_rounds from otree.database import db, MixinSessionFK, SPGModel, CurrencyType class BasePlayer(SPGModel, MixinSessionFK): __abstract__ = True id_in_group = Column(st.Integer, nullable=True, index=True,) # don't modify this directly! Set player.payoff instead _payoff = Column(CurrencyType, default=0) round_number = Column(st.Integer, index=True) # make it non-nullable so that we don't raise an error with null. # the reason i chose to make this different from ordinary StringFields # is that it's a property. users can't just use .get('role') because # that will just access ._role. So we would need some special-casing # in __getattribute__ for role, which is not desirable. _role = Column(st.String, nullable=False, default='') # as a property, that means it's overridable @property def role(self): return self._role @property def payoff(self): return self._payoff @payoff.setter def payoff(self, value): if value is None: value = 0 delta = value - self._payoff self._payoff += delta self.participant.payoff += delta # should save it because it may not be obvious that modifying # player.payoff also changes a field on a different model db.commit() @property def id_in_subsession(self): return self.participant.id_in_session def in_round(self, round_number): return in_round(type(self), round_number, participant=self.participant) def in_rounds(self, first, last): return in_rounds(type(self), first, last, participant=self.participant) def in_previous_rounds(self): return self.in_rounds(1, self.round_number - 1) def in_all_rounds(self): '''i do it this way because it doesn't rely on idmap''' return self.in_previous_rounds() + [self] def get_others_in_group(self): return [p for p in self.group.get_players() if p != self] def get_others_in_subsession(self): return [p for p in self.subsession.get_players() if p != self] def start(self): pass @declared_attr def subsession_id(cls): app_name = cls.get_folder_name() return Column( st.Integer, ForeignKey(f'{app_name}_subsession.id', ondelete='CASCADE') ) @declared_attr def subsession(cls): return relationship(f'{cls.__module__}.Subsession', back_populates='player_set') @declared_attr def group_id(cls): app_name = cls.get_folder_name() # needs to be nullable so re-grouping can happen return Column(st.Integer, ForeignKey(f'{app_name}_group.id'), nullable=True) @declared_attr def group(cls): return relationship(f'{cls.__module__}.Group', back_populates='player_set') @declared_attr def participant_id(cls): return Column(st.Integer, ForeignKey('otree_participant.id')) @declared_attr def participant(cls): return relationship("Participant")