def test_pickle(self): self.test_values = [ 1, 1.33, None, [1, 2, 3., {}, ["coso", None, {}]], { "foo": [], "algo": None, "spam": [1, 2, 3.] }, [], {}, { 1.5: 2 }, { Currency(1): Currency(2) }, {1, 2, 3} ] for value in self.test_values: field = models._PickleField(default=value) serialized = field.get_prep_value(value) restored = field.to_python(serialized) self.assertEquals(value, restored)
def test_pickle(self): self.test_values = [ 1, 1.33, None, [1, 2, 3., {}, ["coso", None, {}]], {"foo": [], "algo": None, "spam": [1, 2, 3.]}, [], {}, {1.5: 2}, {Currency(1): Currency(2)}, {1,2,3} ] for value in self.test_values: field = models._PickleField(default=value) serialized = field.get_prep_value(value) restored = field.to_python(serialized) self.assertEquals(value, restored)
class Session(ModelWithVars): class Meta: app_label = "otree" # if i don't set this, it could be in an unpredictable order ordering = ['pk'] _pickle_fields = ['vars', 'config'] config = models._PickleField(default=dict, null=True) # 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.") 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') # 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_use_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) num_participants = models.PositiveIntegerField() 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 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.creating_session() subsession.save() def mturk_requester_url(self): if self.mturk_use_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_use_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: current_form_page_url = p._current_form_page_url if current_form_page_url: resp = client.post( current_form_page_url, data={ constants_internal.timeout_happened: True, constants_internal.admin_secret_code: ADMIN_SECRET_CODE }, follow=True) # not sure why, but many users are getting HttpResponseNotFound if resp.status_code >= 400: msg = ('Submitting page {} failed, ' 'returned HTTP status code {}.'.format( current_form_page_url, resp.status_code)) content = resp.content if len(content) < 600: msg += ' response content: {}'.format(content) raise AssertionError(msg) 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. start_url = p._start_url() resp = client.get(start_url, follow=True) except: logging.exception("Failed to advance participants.") raise # 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 Session(models.OTreeModel): class Meta: app_label = "otree" # if i don't set this, it could be in an unpredictable order ordering = ['pk'] _ft = FieldTrackerWithVarsSupport() vars: dict = models._PickleField(default=dict) config: dict = models._PickleField(default=dict, null=True) # label of this session instance label = models.CharField(max_length=300, null=True, blank=True, help_text='For internal record-keeping') code = models.CharField( default=random_chars_8, max_length=16, null=False, unique=True, doc="Randomly generated unique identifier for 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', ) is_mturk = models.BooleanField(default=False) def mturk_num_workers(self): assert self.is_mturk return int(self.num_participants / settings.MTURK_NUM_PARTICIPANTS_MULTIPLE) mturk_use_sandbox = models.BooleanField( default=True, help_text="Should this session be created in mturk sandbox?") # use Float instead of DateTime because DateTime # is a pain to work with (e.g. naive vs aware datetime objects) # and there is no need here for DateTime mturk_expiration = models.FloatField(null=True) mturk_qual_id = models.CharField(default='', max_length=50) 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) is_demo = models.BooleanField(default=False) _admin_report_app_names = models.TextField(default='') _admin_report_num_rounds = models.CharField(default='', max_length=255) num_participants = models.PositiveIntegerField() 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 use_browser_bots(self): return self.config.get('use_browser_bots', False) def mock_exogenous_data(self): ''' It's for any exogenous data: - participant labels (which are not passed in through REST API) - participant vars - session vars (if we enable that) ''' if self.config.get('mock_exogenous_data'): import shared_out as user_utils with otree.db.idmap.use_cache(): user_utils.mock_exogenous_data(self) otree.db.idmap.save_objects() # need to save self because it's not in the idmap cache self.save() def get_subsessions(self): lst = [] app_sequence = self.config['app_sequence'] for app in app_sequence: models_module = otree.common.get_models_module(app) subsessions = models_module.Subsession.objects.filter( session=self).order_by('round_number') lst.extend(list(subsessions)) return lst def get_participants(self): return list(self.participant_set.order_by('id_in_session')) def mturk_worker_url(self): # different HITs # get the same preview page, because they are lumped into the same # "hit group". This is not documented, but it seems HITs are lumped # if a certain subset of properties are the same: # https://forums.aws.amazon.com/message.jspa?messageID=597622#597622 # this seems like the correct design; the only case where this will # not work is if the HIT was deleted from the server, but in that case, # the HIT itself should be canceled. # 2018-06-04: # the format seems to have changed to this: # https://worker.mturk.com/projects/{group_id}/tasks?ref=w_pl_prvw # but the old format still works. # it seems I can't replace groupId by hitID, which i would like to do # because it's more precise. subdomain = "workersandbox" if self.mturk_use_sandbox else 'www' return "https://{}.mturk.com/mturk/preview?groupId={}".format( subdomain, self.mturk_HITGroupId) def mturk_is_expired(self): # self.mturk_expiration is offset-aware, so therefore we must compare # it against an offset-aware value. return self.mturk_expiration and self.mturk_expiration < time.time() def mturk_is_active(self): return self.mturk_HITId and not self.mturk_is_expired() def advance_last_place_participants(self): # django.test takes 0.5 sec to import, # if this is done globally then it adds to each startup # it's only used here, and often isn't used at all. # so best to do it only here # it gets cached import django.test client = django.test.Client() 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: current_form_page_url = p._current_form_page_url if current_form_page_url: resp = client.post( current_form_page_url, data={ otree.constants.timeout_happened: True, otree.constants.admin_secret_code: ADMIN_SECRET_CODE, }, follow=True, ) # not sure why, but many users are getting HttpResponseNotFound if resp.status_code >= 400: msg = ('Submitting page {} failed, ' 'returned HTTP status code {}.'.format( current_form_page_url, resp.status_code)) content = resp.content if len(content) < 600: msg += ' response content: {}'.format(content) raise AssertionError(msg) 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. start_url = p._start_url() resp = client.get(start_url, follow=True) except: logging.exception("Failed to advance participants.") raise # do the auto-advancing here, # rather than in increment_index_in_pages, # because it's only needed here. otree.channels.utils.sync_group_send_wrapper( type='auto_advanced', group=auto_advance_group(p.code), event={}) 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.get_models_module(app_name) app_label = get_app_label_from_name(app_name) try: select_template([ f'{app_label}/admin_report.html', f'{app_label}/AdminReport.html' ]) except TemplateDoesNotExist: pass else: admin_report_app_names.append(app_name) num_rounds_list.append(models_module.Constants.num_rounds) 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 ModelWithVars(models.Model): class Meta: abstract = True vars = models._PickleField(default=dict) # type: dict
class Participant(models.OTreeModel, models.VarsMixin, ParticipantIDMapMixin): class Meta: ordering = ['pk'] app_label = "otree" index_together = ['session', 'mturk_worker_id', 'mturk_assignment_id'] session = djmodels.ForeignKey('otree.Session', on_delete=models.CASCADE) vars: dict = models._PickleField(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) payoff = models.CurrencyField(default=0) time_started = djmodels.DateTimeField(null=True) mturk_assignment_id = models.CharField(max_length=50, null=True) mturk_worker_id = models.CharField(max_length=50, null=True) _index_in_pages = models.PositiveIntegerField(default=0, db_index=True) def _numeric_label(self): """the human-readable version.""" return 'P{}'.format(self.id_in_session) _monitor_note = 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, ) # useful when we don't want to load the whole session just to get the code _session_code = djmodels.CharField(max_length=16) visited = models.BooleanField( default=False, db_index=True, doc="""Whether this user's start URL was opened""" ) # 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 = djmodels.URLField() _max_page_index = models.PositiveIntegerField() _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) _timeout_expiration_time = models.FloatField() _timeout_page_index = models.PositiveIntegerField() _gbat_is_waiting = models.BooleanField(default=False) _gbat_page_index = models.PositiveIntegerField() _gbat_grouped = models.BooleanField() def _current_page(self): # don't put 'pages' because that causes wrapping which takes more space # since it's longer than the header return f'{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.get_models_module(app) players = models_module.Player.objects.filter(participant=self).order_by( 'round_number' ) lst.extend(list(players)) return lst 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 url_i_should_be_on( self.code, self._session_code, self._index_in_pages ) return reverse('OutOfRangeNotification', args=[self.code]) def _start_url(self): return otree.common.participant_start_url(self.code) def payoff_in_real_world_currency(self): return self.payoff.to_real_world_currency(self.session) def payoff_plus_participation_fee(self): return self.session._get_payoff_plus_participation_fee(self.payoff) def _get_current_player(self): lookup = get_page_lookup(self._session_code, self._index_in_pages) models_module = otree.common.get_models_module(lookup.app_name) PlayerClass = getattr(models_module, 'Player') return PlayerClass.objects.get( participant=self, round_number=lookup.round_number )
class ModelWithVars(_SaveTheChangeWithCustomFieldSupport, models.Model): class Meta: abstract = True vars = models._PickleField(default=dict) # type: dict
class Session(ModelWithVars): class Meta: app_label = "otree" # if i don't set this, it could be in an unpredictable order ordering = ['pk'] _pickle_fields = ['vars', 'config'] config = models._PickleField(default=dict, null=True) # 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_for_browser = 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.") 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') # 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_use_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) def use_browser_bots(self): return self.participant_set.filter(is_browser_bot=True).exists() is_demo = models.BooleanField(default=False) _admin_report_app_names = models.TextField(default='') _admin_report_num_rounds = models.CharField(default='', max_length=255) num_participants = models.PositiveIntegerField() 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 get_participants(self): return list(self.participant_set.order_by('id_in_session')) def mturk_requester_url(self): subdomain = 'requestersandbox' if self.mturk_use_sandbox else 'requester' return "https://{}.mturk.com/mturk/manageHITs".format(subdomain) def mturk_worker_url(self): # different HITs # get the same preview page, because they are lumped into the same # "hit group". This is not documented, but it seems HITs are lumped # if a certain subset of properties are the same: # https://forums.aws.amazon.com/message.jspa?messageID=597622#597622 # this seems like the correct design; the only case where this will # not work is if the HIT was deleted from the server, but in that case, # the HIT itself should be canceled. # 2018-06-04: # the format seems to have changed to this: # https://worker.mturk.com/projects/{group_id}/tasks?ref=w_pl_prvw # but the old format still works. # it seems I can't replace groupId by hitID, which i would like to do # because it's more precise. subdomain = "workersandbox" if self.mturk_use_sandbox else 'www' return "https://{}.mturk.com/mturk/preview?groupId={}".format( subdomain, self.mturk_HITGroupId) def advance_last_place_participants(self): # django.test takes 0.5 sec to import, # if this is done globally then it adds to each startup # it's only used here, and often isn't used at all. # so best to do it only here # it gets cached import django.test client = django.test.Client() 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: current_form_page_url = p._current_form_page_url if current_form_page_url: resp = client.post( current_form_page_url, data={ constants_internal.timeout_happened: True, constants_internal.admin_secret_code: ADMIN_SECRET_CODE }, follow=True) # not sure why, but many users are getting HttpResponseNotFound if resp.status_code >= 400: msg = ('Submitting page {} failed, ' 'returned HTTP status code {}.'.format( current_form_page_url, resp.status_code)) content = resp.content if len(content) < 600: msg += ' response content: {}'.format(content) raise AssertionError(msg) 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. start_url = p._start_url() resp = client.get(start_url, follow=True) except: logging.exception("Failed to advance participants.") raise # 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 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(models.Model): class Meta: ordering = ['pk'] app_label = "otree" index_together = ['session', 'mturk_worker_id', 'mturk_assignment_id'] _ft = FieldTrackerWithVarsSupport() vars: dict = models._PickleField(default=dict) 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 = dj_models.DateTimeField(null=True) 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""" ) # deprecated on 2019-10-16. eventually get rid of this @property def ip_address(self): return 'deprecated' @ip_address.setter def ip_address(self, value): if value: raise ValueError('Do not store anything into participant.ip_address') # 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 = dj_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.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'] return reverse('OutOfRangeNotification') def _start_url(self): return otree.common.participant_start_url(self.code) def payoff_in_real_world_currency(self): return self.payoff.to_real_world_currency(self.session) def payoff_plus_participation_fee(self): return self.session._get_payoff_plus_participation_fee(self.payoff)