예제 #1
0
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())
예제 #2
0
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)
예제 #3
0
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)
예제 #5
0
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