Esempio n. 1
0
 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)
Esempio n. 2
0
 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)
Esempio n. 3
0
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)
Esempio n. 4
0
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)
Esempio n. 5
0
class ModelWithVars(models.Model):
    class Meta:
        abstract = True

    vars = models._PickleField(default=dict)  # type: dict
Esempio n. 6
0
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
        )
Esempio n. 7
0
class ModelWithVars(_SaveTheChangeWithCustomFieldSupport, models.Model):
    class Meta:
        abstract = True

    vars = models._PickleField(default=dict)  # type: dict
Esempio n. 8
0
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)
Esempio n. 9
0
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)