def test_json_field(self): for value in self.test_values: field = models._JSONField(default=value) serialized = field.get_prep_value(value) if value is None: self.assertIsNone(serialized) else: self.assertJSONEqual(json.dumps(value), serialized) restored = field.to_python(serialized) self.assertEquals(value, restored)
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 JSONFieldModel(ModelWithVars): vars = models._JSONField(default=dict) integer = models.IntegerField(default=0) json_field = models._JSONField()
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) -> typing.Dict: ''' Code is more complicated because of a performance optimization ''' 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 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_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: 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)