class FormFieldModel(otree.models.BaseGroup): null_boolean = models.BooleanField() big_integer = models.BigIntegerField() boolean = models.BooleanField(default=False) char = models.CharField() comma_separated_integer = models.CommaSeparatedIntegerField(max_length=100) date = models.DateField() date_time = models.DateTimeField() alt_date_time = models.DateTimeField( widget=otree.forms.SplitDateTimeWidget) decimal = models.DecimalField(max_digits=5, decimal_places=2) email = models.EmailField() file = models.FileField(upload_to='_tmp/uploads') file_path = models.FilePathField() float = models.FloatField() integer = models.IntegerField() generic_ip_address = models.GenericIPAddressField() positive_integer = models.PositiveIntegerField() positive_small_integer = models.PositiveSmallIntegerField() slug = models.SlugField() small_integer = models.SmallIntegerField() text = models.TextField() alt_text = models.TextField(widget=otree.forms.TextInput) time = models.TimeField() url = models.URLField() many_to_many = models.ManyToManyField('SimpleModel', related_name='+') one_to_one = models.OneToOneField('SimpleModel', related_name='+') currency = models.CurrencyField() currency_choice = models.CurrencyField(choices=[('0.01', '0.01'), ('1.20', '1.20')]) sent_amount = models.CurrencyField(choices=currency_range(0, 0.75, 0.05)) slider_widget = models.IntegerField(widget=widgets.SliderInput())
class RedwoodEvent(models.Model): class Meta: app_label = "otree" # If I don't set this, it could be in an unpredictable order ordering = ['-timestamp'] timestamp = models.DateTimeField(null=False) component = models.CharField(max_length=100, null=False) session = models.ForeignKey( 'otree.Session', null=False, related_name='+') subsession = models.IntegerField(null=True) round = models.IntegerField(null=False) group = models.IntegerField(null=False) value = models._JSONField() def save(self, *args, **kwargs): if self.timestamp is None: self.timestamp = timezone.now() super().save(*args, **kwargs)
class Participant(ModelWithVars): class Meta: ordering = ['pk'] app_label = "otree" index_together = ['session', 'mturk_worker_id', 'mturk_assignment_id'] session = models.ForeignKey('otree.Session', on_delete=models.CASCADE) label = models.CharField( max_length=50, null=True, doc=( "Label assigned by the experimenter. Can be assigned by passing a " "GET param called 'participant_label' to the participant's start " "URL")) id_in_session = models.PositiveIntegerField(null=True) payoff = models.CurrencyField(default=0) time_started = models.DateTimeField(null=True) user_type_in_url = constants_internal.user_type_participant mturk_assignment_id = models.CharField(max_length=50, null=True) mturk_worker_id = models.CharField(max_length=50, null=True) _index_in_subsessions = models.PositiveIntegerField(default=0, null=True) _index_in_pages = models.PositiveIntegerField(default=0, db_index=True) def _id_in_session(self): """the human-readable version.""" return 'P{}'.format(self.id_in_session) _waiting_for_ids = models.CharField(null=True, max_length=300) code = models.CharField( default=random_chars_8, max_length=16, # set non-nullable, until we make our CharField non-nullable null=False, # unique implies DB index unique=True, doc=( "Randomly generated unique identifier for the participant. If you " "would like to merge this dataset with those from another " "subsession in the same session, you should join on this field, " "which will be the same across subsessions.")) visited = models.BooleanField( default=False, db_index=True, doc="""Whether this user's start URL was opened""") ip_address = models.GenericIPAddressField(null=True) # stores when the page was first visited _last_page_timestamp = models.PositiveIntegerField(null=True) _last_request_timestamp = models.PositiveIntegerField(null=True) is_on_wait_page = models.BooleanField(default=False) # these are both for the admin # In the changelist, simply call these "page" and "app" _current_page_name = models.CharField(max_length=200, null=True, verbose_name='page') _current_app_name = models.CharField(max_length=200, null=True, verbose_name='app') # only to be displayed in the admin participants changelist _round_number = models.PositiveIntegerField(null=True) _current_form_page_url = models.URLField() _max_page_index = models.PositiveIntegerField() _browser_bot_finished = models.BooleanField(default=False) _is_bot = models.BooleanField(default=False) # can't start with an underscore because used in template # can't end with underscore because it's a django field (fields.E001) is_browser_bot = models.BooleanField(default=False) _player_lookups = None def player_lookup(self): ''' Code is more complicated because of a performance optimization ''' index = self._index_in_pages if self._player_lookups is None: self._player_lookups = {} if index not in self._player_lookups: # kind of a binary search type logic. limit the number of queries # to log2(n). similar to the way arraylists grow. num_extra_lookups = len(self._player_lookups) + 1 qs = ParticipantToPlayerLookup.objects.filter( participant=self, page_index__range=(index, index + num_extra_lookups)).values() for player_lookup in qs: self._player_lookups[ player_lookup['page_index']] = player_lookup return self._player_lookups[index] def _current_page(self): return '{}/{} pages'.format(self._index_in_pages, self._max_page_index) # because variables used in templates can't start with an underscore def current_page_(self): return self._current_page() def get_players(self): """Used to calculate payoffs""" lst = [] app_sequence = self.session.config['app_sequence'] for app in app_sequence: models_module = otree.common_internal.get_models_module(app) players = models_module.Player.objects.filter( participant=self).order_by('round_number') lst.extend(list(players)) return lst def status(self): # TODO: status could be a field that gets set imperatively if not self.visited: return 'Not started' if self.is_on_wait_page: if self._waiting_for_ids: return 'Waiting for {}'.format(self._waiting_for_ids) return 'Waiting' return 'Playing' def _url_i_should_be_on(self): if not self.visited: return self._start_url() if self._index_in_pages <= self._max_page_index: return self.player_lookup()['url'] if self.session.mturk_HITId: assignment_id = self.mturk_assignment_id if self.session.mturk_use_sandbox: url = 'https://workersandbox.mturk.com/mturk/externalSubmit' else: url = "https://www.mturk.com/mturk/externalSubmit" url = otree.common_internal.add_params_to_url( url, { 'assignmentId': assignment_id, 'extra_param': '1' # required extra param? }) return url return reverse('OutOfRangeNotification') def _start_url(self): return otree.common_internal.participant_start_url(self.code) def payoff_in_real_world_currency(self): return self.payoff.to_real_world_currency(self.session) def money_to_pay(self): '''deprecated''' return self.payoff_plus_participation_fee() def payoff_plus_participation_fee(self): return self.session._get_payoff_plus_participation_fee(self.payoff)
class Participant(ModelWithVars): class Meta: ordering = ['pk'] app_label = "otree" index_together = ['session', 'mturk_worker_id', 'mturk_assignment_id'] exclude_from_data_analysis = models.BooleanField( default=False, doc=("if set to 1, the experimenter indicated that this participant's " "data points should be excluded from the data analysis (e.g. a " "problem took place during the experiment)")) session = models.ForeignKey(Session) time_started = models.DateTimeField(null=True) user_type_in_url = constants_internal.user_type_participant mturk_assignment_id = models.CharField(max_length=50, null=True) mturk_worker_id = models.CharField(max_length=50, null=True) start_order = models.PositiveIntegerField(db_index=True) # unique=True can't be set, because the same external ID could be reused # in multiple sequences. however, it should be unique within the sequence. label = models.CharField( max_length=50, null=True, doc=( "Label assigned by the experimenter. Can be assigned by passing a " "GET param called 'participant_label' to the participant's start " "URL")) _index_in_subsessions = models.PositiveIntegerField(default=0, null=True) _index_in_pages = models.PositiveIntegerField(default=0, db_index=True) id_in_session = models.PositiveIntegerField(null=True) def _id_in_session(self): """the human-readable version.""" return 'P{}'.format(self.id_in_session) _waiting_for_ids = models.CharField(null=True, max_length=300) code = models.CharField( default=random_chars_8, max_length=16, null=False, db_index=True, unique=True, doc=( "Randomly generated unique identifier for the participant. If you " "would like to merge this dataset with those from another " "subsession in the same session, you should join on this field, " "which will be the same across subsessions.")) last_request_succeeded = models.BooleanField( verbose_name='Health of last server request') visited = models.BooleanField( default=False, db_index=True, doc="""Whether this user's start URL was opened""") ip_address = models.GenericIPAddressField(null=True) # stores when the page was first visited _last_page_timestamp = models.PositiveIntegerField(null=True) _last_request_timestamp = models.PositiveIntegerField(null=True) is_on_wait_page = models.BooleanField(default=False) # these are both for the admin # In the changelist, simply call these "page" and "app" _current_page_name = models.CharField(max_length=200, null=True, verbose_name='page') _current_app_name = models.CharField(max_length=200, null=True, verbose_name='app') # only to be displayed in the admin participants changelist _round_number = models.PositiveIntegerField(null=True) _current_form_page_url = models.URLField() _max_page_index = models.PositiveIntegerField() _is_auto_playing = models.BooleanField(default=False) def _start_auto_play(self): self._is_auto_playing = True self.save() client = django.test.Client() if not self.visited: client.get(self._start_url(), follow=True) def _stop_auto_play(self): self._is_auto_playing = False self.save() def player_lookup(self): # this is the most reliable way to get the app name, # because of WaitUntilAssigned... # 2016-04-07: WaitUntilAssigned removed try: return ParticipantToPlayerLookup.objects.get( participant_pk=self.pk, page_index=self._index_in_pages) except: pass def _current_page(self): return '{}/{} pages'.format(self._index_in_pages, self._max_page_index) def get_players(self): """Used to calculate payoffs""" lst = [] app_sequence = self.session.config['app_sequence'] for app in app_sequence: models_module = otree.common_internal.get_models_module(app) players = models_module.Player.objects.filter( participant=self).order_by('round_number') lst.extend(list(players)) return lst def status(self): # TODO: status could be a field that gets set imperatively if not self.visited: return 'Not visited yet' if self.is_on_wait_page: if self._waiting_for_ids: return 'Waiting for {}'.format(self._waiting_for_ids) return 'Waiting' return 'Playing' def _url_i_should_be_on(self): if self._index_in_pages <= self._max_page_index: return self.player_lookup().url else: if self.session.mturk_HITId: assignment_id = self.mturk_assignment_id if self.session.mturk_sandbox: url = ( 'https://workersandbox.mturk.com/mturk/externalSubmit') else: url = "https://www.mturk.com/mturk/externalSubmit" url = otree.common_internal.add_params_to_url( url, { 'assignmentId': assignment_id, 'extra_param': '1' # required extra param? }) return url from otree.views.concrete import OutOfRangeNotification return OutOfRangeNotification.url(self) def __unicode__(self): return self.name() def _start_url(self): return '/InitializeParticipant/{}'.format(self.code) @property def payoff(self): return sum(player.payoff or c(0) for player in self.get_players()) def payoff_in_real_world_currency(self): return self.payoff.to_real_world_currency(self.session) def payoff_from_subsessions(self): """Deprecated on 2015-05-07. Remove at some point. """ return self.payoff def money_to_pay(self): return (self.session.config['participation_fee'] + self.payoff.to_real_world_currency(self.session)) def total_pay(self): return self.money_to_pay() def payoff_is_complete(self): return all(p.payoff is not None for p in self.get_players()) def name(self): return id_label_name(self.pk, self.label)
class Player(otree.models.BasePlayer): # <built-in> group = models.ForeignKey(Group, null=True) subsession = models.ForeignKey(Subsession) # </built-in> player_re_type = models.IntegerField(min=min(Constants.player_types), max=max(Constants.player_types)) transcription = models.TextField() share = models.BooleanField(default=False, widget=widgets.HiddenInput()) training_skip = models.BooleanField(default=False, widget=widgets.HiddenInput()) training_start_time = models.DateTimeField() training_idx = models.PositiveIntegerField(default=0) training_intents = models.JSONField() round_1_start_time = models.DateTimeField() round_1_idx = models.PositiveIntegerField(default=0) round_1_transcription_texts = models.JSONField() round_1_intents = models.JSONField() round_1_a_payoff = models.CurrencyField() round_2_start_time = models.DateTimeField() round_2_idx = models.PositiveIntegerField(default=0) round_2_transcription_texts = models.JSONField() round_2_intents = models.JSONField() round_2_shared = models.PositiveIntegerField(default=0) round_2_a_payoff = models.CurrencyField() round_2_b_payoff = models.CurrencyField() round_3_start_time = models.DateTimeField() round_3_idx = models.PositiveIntegerField(default=0) round_3_transcription_texts = models.JSONField() round_3_intents = models.JSONField() round_3_shared = models.PositiveIntegerField(default=0) round_3_a_payoff = models.CurrencyField() round_3_b_payoff = models.CurrencyField() def set_payoff(self): self.payoff = 0 @property def training_transcription_texts(self): return Constants.reference_texts def training_png(self, idx): return Constants.reference_pngs[idx] def round_1_png(self, idx): if not hasattr(self, "__round_1_png"): self.__round_1_png = {} if idx not in self.__round_1_png: text = self.round_1_transcription_texts[idx] self.__round_1_png[idx] = txt2png.render( text, encoding=Constants.png_encoding) return self.__round_1_png[idx] def set_round_1_payoff(self): self.round_1_a_payoff = Constants.a_payoff * self.round_1_idx def round_1_time_left(self): start = self.round_1_start_time now = timezone.now() time_left = Constants.round_1_seconds - (now - start).seconds return time_left if time_left > 0 else 0 def round_2_png(self, idx): if not hasattr(self, "__round_2_png"): self.__round_2_png = {} if idx not in self.__round_2_png: text = self.round_2_transcription_texts[idx] self.__round_2_png[idx] = txt2png.render( text, encoding=Constants.png_encoding) return self.__round_2_png[idx] def set_round_2_payoff(self): count_b = self.round_2_shared if count_b > 0: self.round_2_b_payoff = Constants.b_payoff * count_b else: self.round_2_b_payoff = 0 count_a = self.round_2_idx - count_b if count_a > 0: self.round_2_a_payoff = Constants.a_payoff * count_a else: self.round_2_a_payoff = 0 def round_2_time_left(self): start = self.round_2_start_time now = timezone.now() time_left = Constants.round_2_seconds - (now - start).seconds return time_left if time_left > 0 else 0 def round_3_png(self, idx): if not hasattr(self, "__round_3_png"): self.__round_3_png = {} if idx not in self.__round_3_png: text = self.round_3_transcription_texts[idx] self.__round_3_png[idx] = txt2png.render( text, encoding=Constants.png_encoding) return self.__round_3_png[idx] def set_round_3_payoff(self): count_b = self.round_3_shared if count_b > 0: self.round_3_b_payoff = Constants.b_payoff * count_b else: self.round_3_b_payoff = 0 count_a = self.round_3_idx - count_b if count_a > 0: self.round_3_a_payoff = Constants.a_payoff * count_a else: self.round_3_a_payoff = 0 def round_3_time_left(self): start = self.round_3_start_time now = timezone.now() time_left = Constants.round_3_seconds - (now - start).seconds return time_left if time_left > 0 else 0
class Session(ModelWithVars): class Meta: # if i don't set this, it could be in an unpredictable order ordering = ['pk'] app_label = "otree" config = models.JSONField( default=dict, null=True, doc=("the session config dict, as defined in the " "programmer's settings.py.")) # label of this session instance label = models.CharField(max_length=300, null=True, blank=True, help_text='For internal record-keeping') experimenter_name = models.CharField( max_length=300, null=True, blank=True, help_text='For internal record-keeping') code = models.RandomCharField( db_index=True, length=8, doc="Randomly generated unique identifier for the session.") time_scheduled = models.DateTimeField( null=True, doc="The time at which the session is scheduled", help_text='For internal record-keeping', blank=True) time_started = models.DateTimeField( null=True, doc="The time at which the experimenter started the session") mturk_HITId = models.CharField( max_length=300, null=True, blank=True, help_text='Hit id for this session on MTurk') mturk_HITGroupId = models.CharField( max_length=300, null=True, blank=True, help_text='Hit id for this session on MTurk') mturk_qualification_type_id = models.CharField( max_length=300, null=True, blank=True, help_text='Qualification type that is ' 'assigned to each worker taking hit') # since workers can drop out number of participants on server should be # greater than number of participants on mturk # value -1 indicates that this session it not intended to run on mturk mturk_num_participants = models.IntegerField( default=-1, help_text="Number of participants on MTurk") mturk_sandbox = models.BooleanField( default=True, help_text="Should this session be created in mturk sandbox?") archived = models.BooleanField( default=False, db_index=True, doc=("If set to True the session won't be visible on the " "main ViewList for sessions")) git_commit_timestamp = models.CharField( max_length=200, null=True, doc=( "Indicates the version of the code (as recorded by Git) that was " "used to run the session, so that the session can be replicated " "later.\n Search through the Git commit log to find a commit that " "was made at this time.")) comment = models.TextField(blank=True) _ready_to_play = models.BooleanField(default=False) _anonymous_code = models.RandomCharField(length=10) special_category = models.CharField( db_index=True, max_length=20, null=True, doc="whether it's a test session, demo session, etc.") # whether someone already viewed this session's demo links demo_already_used = models.BooleanField(default=False, db_index=True) # indicates whether a session has been fully created (not only has the # model itself been created, but also the other models in the hierarchy) ready = models.BooleanField(default=False) _pre_create_id = models.CharField(max_length=300, db_index=True, null=True) def __unicode__(self): return self.code @property def participation_fee(self): '''This method is deprecated from public API, but still useful internally (like data export)''' return self.config['participation_fee'] @property def real_world_currency_per_point(self): '''This method is deprecated from public API, but still useful internally (like data export)''' return self.config['real_world_currency_per_point'] @property def session_type(self): '''2015-07-10: session_type is deprecated this shim method will be removed eventually''' return self.config def is_open(self): return GlobalSingleton.objects.get().default_session == self def is_for_mturk(self): return (not self.is_demo()) and (self.mturk_num_participants > 0) def is_demo(self): return (self.special_category == constants_internal.session_special_category_demo) def get_subsessions(self): lst = [] app_sequence = self.config['app_sequence'] for app in app_sequence: models_module = otree.common_internal.get_models_module(app) subsessions = models_module.Subsession.objects.filter( session=self).order_by('round_number') lst.extend(list(subsessions)) return lst def delete(self, using=None): for subsession in self.get_subsessions(): subsession.delete() super(Session, self).delete(using) def get_participants(self): return self.participant_set.all() def _create_groups_and_initialize(self): # group_by_arrival_time code used to be here for subsession in self.get_subsessions(): subsession._create_groups() subsession._initialize() subsession.save() self._ready_to_play = True # assert self is subsession.session self.save() def mturk_requester_url(self): if self.mturk_sandbox: requester_url = ( "https://requestersandbox.mturk.com/mturk/manageHITs") else: requester_url = "https://requester.mturk.com/mturk/manageHITs" return requester_url def mturk_worker_url(self): if self.mturk_sandbox: worker_url = ( "https://workersandbox.mturk.com/mturk/preview?groupId={}" ).format(self.mturk_HITGroupId) else: worker_url = ( "https://www.mturk.com/mturk/preview?groupId={}").format( self.mturk_HITGroupId) return worker_url def advance_last_place_participants(self): participants = self.get_participants() c = django.test.Client() # in case some participants haven't started unvisited_participants = [] for p in participants: if not p._current_form_page_url: unvisited_participants.append(p) c.get(p._start_url(), follow=True) if unvisited_participants: from otree.models import Participant for p in unvisited_participants: p.save() Participant.flush_cached_instance(p) # that's it -- just visit the start URL, advancing # by 1 return last_place_page_index = min([p._index_in_pages for p in participants]) last_place_participants = [ p for p in participants if p._index_in_pages == last_place_page_index ] for p in last_place_participants: if not p._current_form_page_url: # what if first page is wait page? raise resp = c.post(p._current_form_page_url, data={constants_internal.auto_submit: True}, follow=True) assert resp.status_code < 400 def build_participant_to_player_lookups(self): subsession_app_names = self.config['app_sequence'] num_pages_in_each_app = {} for app_name in subsession_app_names: views_module = otree.common_internal.get_views_module(app_name) num_pages = len(views_module.page_sequence) num_pages_in_each_app[app_name] = num_pages for participant in self.get_participants(): participant.build_participant_to_player_lookups( num_pages_in_each_app)
class Session(ModelWithVars): class Meta: app_label = "otree" # if i don't set this, it could be in an unpredictable order ordering = ['pk'] config = models.JSONField(default=dict, null=True) # type: dict vars = models.JSONField(default=dict) # type: dict # label of this session instance label = models.CharField(max_length=300, null=True, blank=True, help_text='For internal record-keeping') experimenter_name = models.CharField( max_length=300, null=True, blank=True, help_text='For internal record-keeping') ready = models.BooleanField(default=False) code = models.CharField( default=random_chars_8, max_length=16, # set non-nullable, until we make our CharField non-nullable null=False, unique=True, doc="Randomly generated unique identifier for the session.") time_scheduled = models.DateTimeField( null=True, doc="The time at which the session is scheduled", help_text='For internal record-keeping', blank=True) time_started = models.DateTimeField( null=True, doc="The time at which the experimenter started the session") mturk_HITId = models.CharField( max_length=300, null=True, blank=True, help_text='Hit id for this session on MTurk') mturk_HITGroupId = models.CharField( max_length=300, null=True, blank=True, help_text='Hit id for this session on MTurk') mturk_qualification_type_id = models.CharField( max_length=300, null=True, blank=True, help_text='Qualification type that is ' 'assigned to each worker taking hit') # since workers can drop out number of participants on server should be # greater than number of participants on mturk # value -1 indicates that this session it not intended to run on mturk mturk_num_participants = models.IntegerField( default=-1, help_text="Number of participants on MTurk") mturk_sandbox = models.BooleanField( default=True, help_text="Should this session be created in mturk sandbox?") archived = models.BooleanField( default=False, db_index=True, doc=("If set to True the session won't be visible on the " "main ViewList for sessions")) comment = models.TextField(blank=True) _anonymous_code = models.CharField(default=random_chars_10, max_length=10, null=False, db_index=True) _pre_create_id = models.CharField(max_length=300, db_index=True, null=True) use_browser_bots = models.BooleanField(default=False) # if the user clicks 'start bots' twice, this will prevent the bots # from being run twice. _cannot_restart_bots = models.BooleanField(default=False) _bots_finished = models.BooleanField(default=False) _bots_errored = models.BooleanField(default=False) _bot_case_number = models.PositiveIntegerField() is_demo = models.BooleanField(default=False) # whether SOME players are bots has_bots = models.BooleanField(default=False) def __unicode__(self): return self.code @property def participation_fee(self): '''This method is deprecated from public API, but still useful internally (like data export)''' return self.config['participation_fee'] @property def real_world_currency_per_point(self): '''This method is deprecated from public API, but still useful internally (like data export)''' return self.config['real_world_currency_per_point'] def is_for_mturk(self): return (not self.is_demo) and (self.mturk_num_participants > 0) def get_subsessions(self): lst = [] app_sequence = self.config['app_sequence'] for app in app_sequence: models_module = otree.common_internal.get_models_module(app) subsessions = models_module.Subsession.objects.filter( session=self).order_by('round_number') lst.extend(list(subsessions)) return lst def delete(self, using=None): for subsession in self.get_subsessions(): subsession.delete() super(Session, self).delete(using) def get_participants(self): return self.participant_set.all() def get_human_participants(self): return self.participant_set.filter(_is_bot=False) def _create_groups_and_initialize(self): # group_by_arrival_time code used to be here for subsession in self.get_subsessions(): subsession._create_groups() subsession.before_session_starts() subsession.save() def mturk_requester_url(self): if self.mturk_sandbox: requester_url = ( "https://requestersandbox.mturk.com/mturk/manageHITs") else: requester_url = "https://requester.mturk.com/mturk/manageHITs" return requester_url def mturk_worker_url(self): if self.mturk_sandbox: return ("https://workersandbox.mturk.com/mturk/preview?groupId={}" ).format(self.mturk_HITGroupId) return ("https://www.mturk.com/mturk/preview?groupId={}").format( self.mturk_HITGroupId) def advance_last_place_participants(self): # can't auto-advance bots, because that could break their # pre-determined logic # consider pros/cons of doing this participants = self.get_human_participants() # in case some participants haven't started unvisited_participants = [] for p in participants: if p._index_in_pages == 0: unvisited_participants.append(p) client.get(p._start_url(), follow=True) if unvisited_participants: # that's it -- just visit the start URL, advancing by 1 return last_place_page_index = min([p._index_in_pages for p in participants]) last_place_participants = [ p for p in participants if p._index_in_pages == last_place_page_index ] for p in last_place_participants: # what if first page is wait page? # that shouldn't happen, because then they must be # waiting for some other players who are even further back assert p._current_form_page_url try: resp = client.post(p._current_form_page_url, data={constants_internal.auto_submit: True}, follow=True) except: logging.exception("Failed to advance participants.") raise assert resp.status_code < 400 def build_participant_to_player_lookups(self): subsession_app_names = self.config['app_sequence'] views_modules = {} for app_name in subsession_app_names: views_modules[app_name] = ( otree.common_internal.get_views_module(app_name)) def views_module_for_player(player): return views_modules[player._meta.app_config.name] records_to_create = [] for participant in self.get_participants(): page_index = 0 for player in participant.get_players(): for View in views_module_for_player(player).page_sequence: page_index += 1 records_to_create.append( ParticipantToPlayerLookup( participant=participant, page_index=page_index, app_name=player._meta.app_config.name, player_pk=player.pk, url=reverse(View.url_name(), args=[participant.code, page_index]))) # technically could be stored at the session level participant._max_page_index = page_index participant.save() ParticipantToPlayerLookup.objects.bulk_create(records_to_create) def get_room(self): from otree.room import ROOM_DICT try: room_name = RoomToSession.objects.get(session=self).room_name return ROOM_DICT[room_name] except RoomToSession.DoesNotExist: return None
class Session(ModelWithVars): class Meta: app_label = "otree" # if i don't set this, it could be in an unpredictable order ordering = ['pk'] config = models._JSONField(default=dict, null=True) # type: dict vars = models._JSONField(default=dict) # type: dict # label of this session instance label = models.CharField(max_length=300, null=True, blank=True, help_text='For internal record-keeping') experimenter_name = models.CharField( max_length=300, null=True, blank=True, help_text='For internal record-keeping') ready = models.BooleanField(default=False) code = models.CharField( default=random_chars_8, max_length=16, # set non-nullable, until we make our CharField non-nullable null=False, unique=True, doc="Randomly generated unique identifier for the session.") time_scheduled = models.DateTimeField( null=True, doc="The time at which the session is scheduled", help_text='For internal record-keeping', blank=True) time_started = models.DateTimeField( null=True, doc="The time at which the experimenter started the session") mturk_HITId = models.CharField( max_length=300, null=True, blank=True, help_text='Hit id for this session on MTurk') mturk_HITGroupId = models.CharField( max_length=300, null=True, blank=True, help_text='Hit id for this session on MTurk') mturk_qualification_type_id = models.CharField( max_length=300, null=True, blank=True, help_text='Qualification type that is ' 'assigned to each worker taking hit') # since workers can drop out number of participants on server should be # greater than number of participants on mturk # value -1 indicates that this session it not intended to run on mturk mturk_num_participants = models.IntegerField( default=-1, help_text="Number of participants on MTurk") mturk_sandbox = models.BooleanField( default=True, help_text="Should this session be created in mturk sandbox?") archived = models.BooleanField( default=False, db_index=True, doc=("If set to True the session won't be visible on the " "main ViewList for sessions")) comment = models.TextField(blank=True) _anonymous_code = models.CharField(default=random_chars_10, max_length=10, null=False, db_index=True) _pre_create_id = models.CharField(max_length=255, db_index=True, null=True) use_browser_bots = models.BooleanField(default=False) # if the user clicks 'start bots' twice, this will prevent the bots # from being run twice. _cannot_restart_bots = models.BooleanField(default=False) _bots_finished = models.BooleanField(default=False) _bots_errored = models.BooleanField(default=False) _bot_case_number = models.PositiveIntegerField() is_demo = models.BooleanField(default=False) # whether SOME players are bots has_bots = models.BooleanField(default=False) _admin_report_app_names = models.TextField(default='') _admin_report_num_rounds = models.CharField(default='', max_length=255) def __unicode__(self): return self.code @property def participation_fee(self): '''This method is deprecated from public API, but still useful internally (like data export)''' return self.config['participation_fee'] @property def real_world_currency_per_point(self): '''This method is deprecated from public API, but still useful internally (like data export)''' return self.config['real_world_currency_per_point'] def is_for_mturk(self): return (not self.is_demo) and (self.mturk_num_participants > 0) def get_subsessions(self): lst = [] app_sequence = self.config['app_sequence'] for app in app_sequence: models_module = otree.common_internal.get_models_module(app) subsessions = models_module.Subsession.objects.filter( session=self).order_by('round_number') lst.extend(list(subsessions)) return lst def delete(self, using=None): for subsession in self.get_subsessions(): subsession.delete() super(Session, self).delete(using) def get_participants(self): return self.participant_set.all() def _create_groups_and_initialize(self): # group_by_arrival_time_time code used to be here for subsession in self.get_subsessions(): subsession._create_groups() subsession.before_session_starts() subsession.save() def mturk_requester_url(self): if self.mturk_sandbox: requester_url = ( "https://requestersandbox.mturk.com/mturk/manageHITs") else: requester_url = "https://requester.mturk.com/mturk/manageHITs" return requester_url def mturk_worker_url(self): if self.mturk_sandbox: return ("https://workersandbox.mturk.com/mturk/preview?groupId={}" ).format(self.mturk_HITGroupId) return ("https://www.mturk.com/mturk/preview?groupId={}").format( self.mturk_HITGroupId) def advance_last_place_participants(self): participants = self.get_participants() # in case some participants haven't started unvisited_participants = [] for p in participants: if p._index_in_pages == 0: unvisited_participants.append(p) client.get(p._start_url(), follow=True) if unvisited_participants: # that's it -- just visit the start URL, advancing by 1 return last_place_page_index = min([p._index_in_pages for p in participants]) last_place_participants = [ p for p in participants if p._index_in_pages == last_place_page_index ] for p in last_place_participants: try: if p._current_form_page_url: resp = client.post( p._current_form_page_url, data={ constants_internal.auto_submit: True, constants_internal.admin_secret_code: ADMIN_SECRET_CODE }, follow=True) else: # it's possible that the slowest user is on a wait page, # especially if their browser is closed. # because they were waiting for another user who then # advanced past the wait page, but they were never # advanced themselves. resp = client.get(p._start_url(), follow=True) except: logging.exception("Failed to advance participants.") raise assert resp.status_code < 400 # do the auto-advancing here, # rather than in increment_index_in_pages, # because it's only needed here. channels.Group('auto-advance-{}'.format(p.code)).send( {'text': json.dumps({'auto_advanced': True})}) def pages_auto_reload_when_advanced(self): # keep it enable until I determine # (a) the usefulness of the feature # (b) the impact on performance return True # return settings.DEBUG or self.is_demo def build_participant_to_player_lookups(self): subsession_app_names = self.config['app_sequence'] views_modules = {} for app_name in subsession_app_names: views_modules[app_name] = ( otree.common_internal.get_views_module(app_name)) def views_module_for_player(player): return views_modules[player._meta.app_config.name] records_to_create = [] for participant in self.get_participants(): page_index = 0 for player in participant.get_players(): for View in views_module_for_player(player).page_sequence: page_index += 1 records_to_create.append( ParticipantToPlayerLookup( participant=participant, page_index=page_index, app_name=player._meta.app_config.name, player_pk=player.pk, url=reverse(View.url_name(), args=[participant.code, page_index]))) # technically could be stored at the session level participant._max_page_index = page_index participant.save() ParticipantToPlayerLookup.objects.bulk_create(records_to_create) def get_room(self): from otree.room import ROOM_DICT try: room_name = RoomToSession.objects.get(session=self).room_name return ROOM_DICT[room_name] except RoomToSession.DoesNotExist: return None def _get_payoff_plus_participation_fee(self, payoff): '''For a participant who has the given payoff, return their payoff_plus_participation_fee Useful to define it here, for data export ''' return (self.config['participation_fee'] + payoff.to_real_world_currency(self)) def _set_admin_report_app_names(self): admin_report_app_names = [] num_rounds_list = [] for app_name in self.config['app_sequence']: models_module = otree.common_internal.get_models_module(app_name) app_label = get_app_label_from_name(app_name) try: get_template('{}/AdminReport.html'.format(app_label)) admin_report_app_names.append(app_name) num_rounds_list.append(models_module.Constants.num_rounds) except TemplateDoesNotExist: pass self._admin_report_app_names = ';'.join(admin_report_app_names) self._admin_report_num_rounds = ';'.join( str(n) for n in num_rounds_list) def _admin_report_apps(self): return self._admin_report_app_names.split(';') def _admin_report_num_rounds_list(self): return [int(num) for num in self._admin_report_num_rounds.split(';')] def has_admin_report(self): return bool(self._admin_report_app_names)
class Participant(ModelWithVars): class Meta: ordering = ['pk'] app_label = "otree" index_together = ['session', 'mturk_worker_id', 'mturk_assignment_id'] session = models.ForeignKey('otree.Session') vars = models._JSONField(default=dict) label = models.CharField( max_length=50, null=True, doc=( "Label assigned by the experimenter. Can be assigned by passing a " "GET param called 'participant_label' to the participant's start " "URL")) id_in_session = models.PositiveIntegerField(null=True) exclude_from_data_analysis = models.BooleanField( default=False, doc=("if set to 1, the experimenter indicated that this participant's " "data points should be excluded from the data analysis (e.g. a " "problem took place during the experiment)")) time_started = models.DateTimeField(null=True) user_type_in_url = constants_internal.user_type_participant mturk_assignment_id = models.CharField(max_length=50, null=True) mturk_worker_id = models.CharField(max_length=50, null=True) start_order = models.PositiveIntegerField(db_index=True) _index_in_subsessions = models.PositiveIntegerField(default=0, null=True) _index_in_pages = models.PositiveIntegerField(default=0, db_index=True) def _id_in_session(self): """the human-readable version.""" return 'P{}'.format(self.id_in_session) _waiting_for_ids = models.CharField(null=True, max_length=300) code = models.CharField( default=random_chars_8, max_length=16, # set non-nullable, until we make our CharField non-nullable null=False, # unique implies DB index unique=True, doc=( "Randomly generated unique identifier for the participant. If you " "would like to merge this dataset with those from another " "subsession in the same session, you should join on this field, " "which will be the same across subsessions.")) last_request_succeeded = models.BooleanField( verbose_name='Health of last server request') visited = models.BooleanField( default=False, db_index=True, doc="""Whether this user's start URL was opened""") ip_address = models.GenericIPAddressField(null=True) # stores when the page was first visited _last_page_timestamp = models.PositiveIntegerField(null=True) _last_request_timestamp = models.PositiveIntegerField(null=True) is_on_wait_page = models.BooleanField(default=False) # these are both for the admin # In the changelist, simply call these "page" and "app" _current_page_name = models.CharField(max_length=200, null=True, verbose_name='page') _current_app_name = models.CharField(max_length=200, null=True, verbose_name='app') # only to be displayed in the admin participants changelist _round_number = models.PositiveIntegerField(null=True) _current_form_page_url = models.URLField() _max_page_index = models.PositiveIntegerField() _browser_bot_finished = models.BooleanField(default=False) _is_bot = models.BooleanField(default=False) _player_lookups = None def player_lookup(self): # this is the most reliable way to get the app name, # because of WaitUntilAssigned... # 2016-04-07: WaitUntilAssigned removed index = self._index_in_pages if not self._player_lookups or index not in self._player_lookups: self._player_lookups = self._player_lookups or {} # kind of a binary search type logic. limit the number of queries # to log2(n). similar to the way arraylists grow. num_extra_lookups = len(self._player_lookups) + 1 qs = ParticipantToPlayerLookup.objects.filter( participant=self.pk, page_index__range=(index, index + num_extra_lookups)).values() for player_lookup in qs: self._player_lookups[ player_lookup['page_index']] = player_lookup return self._player_lookups[index] def future_player_lookup(self, pages_ahead): try: return ParticipantToPlayerLookup.objects.get( participant=self.pk, page_index=self._index_in_pages + pages_ahead) except ParticipantToPlayerLookup.DoesNotExist: return def _current_page(self): return '{}/{} pages'.format(self._index_in_pages, self._max_page_index) def get_players(self): """Used to calculate payoffs""" lst = [] app_sequence = self.session.config['app_sequence'] for app in app_sequence: models_module = otree.common_internal.get_models_module(app) players = models_module.Player.objects.filter( participant=self).order_by('round_number') lst.extend(list(players)) return lst def status(self): # TODO: status could be a field that gets set imperatively if not self.visited: return 'Not started' if self.is_on_wait_page: if self._waiting_for_ids: return 'Waiting for {}'.format(self._waiting_for_ids) return 'Waiting' return 'Playing' def _url_i_should_be_on(self): if self._index_in_pages <= self._max_page_index: return self.player_lookup()['url'] if self.session.mturk_HITId: assignment_id = self.mturk_assignment_id if self.session.mturk_sandbox: url = 'https://workersandbox.mturk.com/mturk/externalSubmit' else: url = "https://www.mturk.com/mturk/externalSubmit" url = otree.common_internal.add_params_to_url( url, { 'assignmentId': assignment_id, 'extra_param': '1' # required extra param? }) return url return reverse('OutOfRangeNotification') def __unicode__(self): return self.name() @permalink def _start_url(self): return 'InitializeParticipant', (self.code, ) @property def payoff(self): app_sequence = self.session.config['app_sequence'] total_payoff = 0 for app in app_sequence: models_module = otree.common_internal.get_models_module(app) app_payoff = models_module.Player.objects.filter( participant=self).aggregate(Sum('payoff'))['payoff__sum'] total_payoff += (app_payoff or 0) return c(total_payoff) def payoff_in_real_world_currency(self): return self.payoff.to_real_world_currency(self.session) def money_to_pay(self): return self.payoff_plus_participation_fee() def payoff_plus_participation_fee(self): return self.session._get_payoff_plus_participation_fee(self.payoff) def name(self): return id_label_name(self.pk, self.label)
class Session(ModelWithVars): class Meta: # if i don't set this, it could be in an unpredictable order ordering = ['pk'] app_label = "otree" config = models.JSONField( default=dict, null=True, doc=("the session config dict, as defined in the " "programmer's settings.py.")) # label of this session instance label = models.CharField(max_length=300, null=True, blank=True, help_text='For internal record-keeping') experimenter_name = models.CharField( max_length=300, null=True, blank=True, help_text='For internal record-keeping') code = models.RandomCharField( length=8, doc="Randomly generated unique identifier for the session.") time_scheduled = models.DateTimeField( null=True, doc="The time at which the session is scheduled", help_text='For internal record-keeping', blank=True) time_started = models.DateTimeField( null=True, doc="The time at which the experimenter started the session") mturk_HITId = models.CharField( max_length=300, null=True, blank=True, help_text='Hit id for this session on MTurk') mturk_HITGroupId = models.CharField( max_length=300, null=True, blank=True, help_text='Hit id for this session on MTurk') mturk_qualification_type_id = models.CharField( max_length=300, null=True, blank=True, help_text='Qualification type that is ' 'assigned to each worker taking hit') # since workers can drop out number of participants on server should be # greater than number of participants on mturk # value -1 indicates that this session it not intended to run on mturk mturk_num_participants = models.IntegerField( default=-1, help_text="Number of participants on MTurk") mturk_sandbox = models.BooleanField( default=True, help_text="Should this session be created in mturk sandbox?") archived = models.BooleanField( default=False, doc=("If set to True the session won't be visible on the " "main ViewList for sessions")) git_commit_timestamp = models.CharField( max_length=200, null=True, doc=( "Indicates the version of the code (as recorded by Git) that was " "used to run the session, so that the session can be replicated " "later.\n Search through the Git commit log to find a commit that " "was made at this time.")) comment = models.TextField(blank=True) _ready_to_play = models.BooleanField(default=False) _anonymous_code = models.RandomCharField(length=10) special_category = models.CharField( max_length=20, null=True, doc="whether it's a test session, demo session, etc.") # whether someone already viewed this session's demo links demo_already_used = models.BooleanField(default=False) # indicates whether a session has been fully created (not only has the # model itself been created, but also the other models in the hierarchy) ready = models.BooleanField(default=False) _pre_create_id = models.CharField(max_length=300, null=True) def __unicode__(self): return self.code @property def participation_fee(self): '''This method is deprecated from public API, but still useful internally (like data export)''' return self.config['participation_fee'] @property def real_world_currency_per_point(self): '''This method is deprecated from public API, but still useful internally (like data export)''' return self.config['real_world_currency_per_point'] @property def session_type(self): '''2015-07-10: session_type is deprecated this shim method will be removed eventually''' return self.config def is_open(self): return GlobalSingleton.objects.get().default_session == self def is_for_mturk(self): return (not self.is_demo()) and (self.mturk_num_participants > 0) def is_demo(self): return (self.special_category == constants_internal.session_special_category_demo) def subsession_names(self): names = [] for subsession in self.get_subsessions(): app_name = subsession._meta.app_config.name name = '{} {}'.format( otree.common_internal.app_name_format(app_name), subsession.name()) names.append(name) if names: return ', '.join(names) else: return '[empty sequence]' def get_subsessions(self): lst = [] app_sequence = self.config['app_sequence'] for app in app_sequence: models_module = otree.common_internal.get_models_module(app) subsessions = models_module.Subsession.objects.filter( session=self).order_by('round_number') lst.extend(list(subsessions)) return lst def delete(self, using=None): for subsession in self.get_subsessions(): subsession.delete() super(Session, self).delete(using) def get_participants(self): return self.participant_set.all() def payments_ready(self): for participants in self.get_participants(): if not participants.payoff_is_complete(): return False return True payments_ready.boolean = True def _create_groups_and_initialize(self): # if ppg is None, then the arrival time doesn't matter because # everyone is assigned to one big group. # otherwise, even in single-player games, you would have to wait # for other players to arrive # the drawback of this approach is that id_in_group is # predetermined, rather than by arrival time. # alternative design: # instead of checking ppg, we could also check if the game # contains a wait page # another alternative: # allow players to start even if the rest of the group hasn't arrived # but this might break some assumptions such as len(grp.get_players()) # also, what happens if you get to the next round before # another player has started the first? you can't clone the # previous round's groups for subsession in self.get_subsessions(): cond = (self.config.get('group_by_arrival_time') and subsession._Constants.players_per_group is not None) if cond: if subsession.round_number == 1: subsession._set_players_per_group_list() subsession._create_empty_groups() else: subsession._create_groups() subsession._initialize() subsession.save() self._ready_to_play = True # assert self is subsession.session self.save() def mturk_requester_url(self): if self.mturk_sandbox: requester_url = ( "https://requestersandbox.mturk.com/mturk/manageHITs") else: requester_url = "https://requester.mturk.com/mturk/manageHITs" return requester_url def mturk_worker_url(self): if self.mturk_sandbox: worker_url = ( "https://workersandbox.mturk.com/mturk/preview?groupId={}" ).format(self.mturk_HITGroupId) else: worker_url = ( "https://www.mturk.com/mturk/preview?groupId={}").format( self.mturk_HITGroupId) return worker_url def advance_last_place_participants(self): participants = self.get_participants() c = django.test.Client() # in case some participants haven't started some_participants_not_visited = False for p in participants: if not p.visited: some_participants_not_visited = True c.get(p._start_url(), follow=True) if some_participants_not_visited: # refresh from DB so that _current_form_page_url gets set participants = self.participant_set.all() last_place_page_index = min([p._index_in_pages for p in participants]) last_place_participants = [ p for p in participants if p._index_in_pages == last_place_page_index ] for p in last_place_participants: # what if current_form_page_url hasn't been set yet? resp = c.post(p._current_form_page_url, data={constants_internal.auto_submit: True}, follow=True) assert resp.status_code < 400 def build_session_user_to_user_lookups(self): subsession_app_names = self.config['app_sequence'] num_pages_in_each_app = {} for app_name in subsession_app_names: views_module = otree.common_internal.get_views_module(app_name) num_pages = len(views_module.page_sequence) num_pages_in_each_app[app_name] = num_pages for participant in self.get_participants(): participant.build_session_user_to_user_lookups( num_pages_in_each_app)
class Participant(SessionUser): class Meta: ordering = ['pk'] app_label = "otree" exclude_from_data_analysis = models.BooleanField( default=False, doc=("if set to 1, the experimenter indicated that this participant's " "data points should be excluded from the data analysis (e.g. a " "problem took place during the experiment)")) session = models.ForeignKey(Session) time_started = models.DateTimeField(null=True) user_type_in_url = constants_internal.user_type_participant mturk_assignment_id = models.CharField(max_length=50, null=True) mturk_worker_id = models.CharField(max_length=50, null=True) mturk_reward_paid = models.BooleanField(default=False) mturk_bonus_paid = models.BooleanField(default=False) start_order = models.PositiveIntegerField() # unique=True can't be set, because the same external ID could be reused # in multiple sequences. however, it should be unique within the sequence. label = models.CharField( max_length=50, null=True, doc=( "Label assigned by the experimenter. Can be assigned by passing a " "GET param called 'participant_label' to the participant's start " "URL")) def __unicode__(self): return self.name() def _start_url(self): return '/InitializeParticipant/{}'.format(self.code) def get_players(self): return self.get_users() @property def payoff(self): return sum(player.payoff or c(0) for player in self.get_players()) def payoff_in_real_world_currency(self): return self.payoff.to_real_world_currency(self.session) def payoff_from_subsessions(self): """Deprecated on 2015-05-07. Remove at some point. """ return self.payoff def money_to_pay(self): return (self.session.config['participation_fee'] + self.payoff.to_real_world_currency(self.session)) def total_pay(self): return self.money_to_pay() def payoff_is_complete(self): return all(p.payoff is not None for p in self.get_players()) def money_to_pay_display(self): complete = self.payoff_is_complete() money_to_pay = self.money_to_pay() if complete: return money_to_pay return u'{} (incomplete)'.format(money_to_pay) def name(self): return id_label_name(self.pk, self.label)