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 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 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 SessionUser(ModelWithVars): _index_in_subsessions = models.PositiveIntegerField(default=0, null=True) _index_in_pages = models.PositiveIntegerField(default=0) id_in_session = models.PositiveIntegerField(null=True) def _id_in_session_display(self): return 'P{}'.format(self.id_in_session) _id_in_session_display.short_description = 'Participant' _waiting_for_ids = models.CharField(null=True, max_length=300) code = models.RandomCharField( length=8, 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, 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() def _current_page(self): return '{}/{} pages'.format(self._index_in_pages, self._max_page_index) def get_users(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): if not self.visited: return 'Not visited yet' # check if they are disconnected max_seconds_since_last_request = max( constants_internal.form_page_poll_interval_seconds, constants_internal.wait_page_poll_interval_seconds, ) + 10 # for latency if self._last_request_timestamp is None: # it shouldn't be None, but sometimes is...race condition? time_since_last_request = 0 else: time_since_last_request = (time.time() - self._last_request_timestamp) if time_since_last_request > max_seconds_since_last_request: return 'Disconnected' if self.is_on_wait_page: if self._waiting_for_ids: return 'Waiting for {}'.format(self._waiting_for_ids) return 'Waiting' return 'Playing' def _pages(self): from otree.views.concrete import WaitUntilAssignedToGroup pages = [] for user in self.get_users(): app_name = user._meta.app_config.name views_module = otree.common_internal.get_views_module(app_name) subsession_pages = ([WaitUntilAssignedToGroup] + views_module.page_sequence) pages.extend(subsession_pages) return pages def _pages_as_urls(self): return [ View.url(self, index) for index, View in enumerate(self._pages()) ] def _url_i_should_be_on(self): if self._index_in_pages <= self._max_page_index: return self._pages_as_urls()[self._index_in_pages] 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 build_session_user_to_user_lookups(self, num_pages_in_each_app): def pages_for_user(user): return num_pages_in_each_app[user._meta.app_config.name] indexes = itertools.count() SessionuserToUserLookup.objects.bulk_create([ SessionuserToUserLookup( session_user_pk=self.pk, page_index=page_index, app_name=user._meta.app_config.name, user_pk=user.pk, ) for user in self.get_users() for _, page_index in zip(range(pages_for_user(user) + 1), indexes) # +1 is for WaitUntilAssigned... ]) self._max_page_index = next(indexes) - 1 self.save() class Meta: abstract = True