Пример #1
0
def augment_session_config(session_config):
    new_session_config = {'doc': ''}
    new_session_config.update(settings.SESSION_CONFIG_DEFAULTS)
    new_session_config.update(session_config)

    # look up new_session_config
    # 2015-05-14: why do we strip? the doc can have line breaks in the middle
    # anyways
    new_session_config['doc'] = new_session_config['doc'].strip()

    # TODO: fixed_pay is deprecated as of 2015-05-07,
    # in favor of participation_fee. make this required at some point.
    if (('participation_fee' not in new_session_config) and
            ('fixed_pay' in new_session_config)):
        deprecate.dwarning(
            '"fixed_pay" is deprecated; '
            'you should rename it to "participation_fee".'
        )
        new_session_config['participation_fee'] = (
            new_session_config['fixed_pay'])

    new_session_config['participation_fee'] = RealWorldCurrency(
        new_session_config['participation_fee'])

    # normalize to decimal so we can do multiplications, etc
    # quantize because the original value may be a float,
    # which when converted to Decimal may have some 'decimal junk'
    # like 0.010000000000000000208166817...
    new_session_config['real_world_currency_per_point'] = Decimal(
        new_session_config['real_world_currency_per_point']
    ).quantize(Decimal('0.00001'))

    validate_session_config(new_session_config)
    return new_session_config
Пример #2
0
    def validate(self):

        config_schema = schema.Schema({
            'name': str,
            'app_sequence': list,
            'participation_fee': object,
            'real_world_currency_per_point': object,
            'num_demo_participants': int,
            'doc': str,
            object: object,
        })

        try:
            config_schema.validate(self)
        except schema.SchemaError as e:
            raise ValueError('settings.SESSION_CONFIGS: {}'.format(e))

        validate_identifier(
            self['name'],
            identifier_description='settings.SESSION_CONFIG name'
        )

        app_sequence = self['app_sequence']
        if len(app_sequence) != len(set(app_sequence)):
            msg = (
                'settings.SESSION_CONFIGS: '
                'app_sequence of "{}" '
                'must not contain duplicate elements. '
                'If you want multiple rounds, '
                'you should set Constants.num_rounds.')
            raise ValueError(msg.format(self['name']))

        if len(app_sequence) == 0:
            raise ValueError(
                'settings.SESSION_CONFIGS: Need at least one subsession.')

        self.setdefault('display_name', self['name'])
        self.setdefault('doc', '')

        # TODO: fixed_pay is deprecated as of 2015-05-07,
        # in favor of participation_fee. make this required at some point.
        if (('participation_fee' not in self) and
                ('fixed_pay' in self)):
            warn_msg = (
                "'fixed_pay' is deprecated; "
                "you should rename it to 'participation_fee'.")
            warnings.warn(warn_msg, OtreeDeprecationWarning)

            self['participation_fee'] = self['fixed_pay']

        self['participation_fee'] = RealWorldCurrency(
            self['participation_fee'])

        # normalize to decimal so we can do multiplications, etc
        # quantize because the original value may be a float,
        # which when converted to Decimal may have some 'decimal junk'
        # like 0.010000000000000000208166817...
        self['real_world_currency_per_point'] = Decimal(
            self['real_world_currency_per_point']
        ).quantize(Decimal('0.00001'))
Пример #3
0
    def clean(self):

        config_schema = schema.Schema({
            'name': str,
            'app_sequence': list,
            'participation_fee': object,
            'real_world_currency_per_point': object,
            'num_demo_participants': int,
            'doc': str,
            str: object,
        })

        try:
            config_schema.validate(self)
        except schema.SchemaError as e:
            raise ValueError(
                'settings.SESSION_CONFIGS: {}'.format(e)) from None

        # Allow non-ASCII chars in session config keys, because they are
        # configurable in the admin, so need to be readable by non-English
        # speakers. However, don't allow punctuation, spaces, etc.
        # They make it harder to reason about and could cause problems
        # later on. also could uglify the user's code.

        INVALID_IDENTIFIER_MSG = (
            'Key "{}" in settings.SESSION_CONFIGS '
            'must not contain spaces, punctuation, '
            'or other special characters. '
            'It can contain non-English characters, '
            'but it must be a valid Python variable name '
            'according to string.isidentifier().')

        for key in self:
            if not key.isidentifier():
                raise ValueError(INVALID_IDENTIFIER_MSG.format(key))

        validate_alphanumeric(
            self['name'],
            identifier_description='settings.SESSION_CONFIGS name')

        app_sequence = self['app_sequence']
        if len(app_sequence) != len(set(app_sequence)):
            msg = ('settings.SESSION_CONFIGS: '
                   'app_sequence of "{}" '
                   'must not contain duplicate elements. '
                   'If you want multiple rounds, '
                   'you should set Constants.num_rounds.')
            raise ValueError(msg.format(self['name']))

        if len(app_sequence) == 0:
            raise ValueError(
                'settings.SESSION_CONFIGS: Need at least one subsession.')

        self.setdefault('display_name', self['name'])
        self.setdefault('doc', '')

        self['participation_fee'] = RealWorldCurrency(
            self['participation_fee'])
Пример #4
0
 def form_valid(self, form):
     super(SessionEditProperties, self).form_valid(form)
     participation_fee = form.cleaned_data['participation_fee']
     real_world_currency_per_point = form.cleaned_data[
         'real_world_currency_per_point']
     config = self.session.config
     if form.cleaned_data['participation_fee'] is not None:
         config['participation_fee'] = RealWorldCurrency(participation_fee)
     if form.cleaned_data['real_world_currency_per_point'] is not None:
         config[
             'real_world_currency_per_point'] = real_world_currency_per_point
     self.session.save()
     messages.success(self.request, 'Properties have been updated')
     return HttpResponseRedirect(self.get_success_url())
Пример #5
0
 def form_valid(self, form):
     super(EditSessionProperties, self).form_valid(form)
     config = self.session.config
     participation_fee = form.cleaned_data['participation_fee']
     real_world_currency_per_point = form.cleaned_data[
         'real_world_currency_per_point']
     if form.cleaned_data['participation_fee']:
         config['participation_fee'] = RealWorldCurrency(participation_fee)
     if form.cleaned_data['real_world_currency_per_point']:
         config[
             'real_world_currency_per_point'] = real_world_currency_per_point
     # use .copy() to force marking this field as dirty/changed
     self.session.config = config.copy()
     self.session.save()
     messages.success(self.request, 'Properties have been updated')
     return HttpResponseRedirect(self.get_success_url())
Пример #6
0
 def form_valid(self, form):
     super(SessionEditProperties, self).form_valid(form)
     participation_fee = form.cleaned_data['participation_fee']
     real_world_currency_per_point = form.cleaned_data[
         'real_world_currency_per_point']
     config = self.session.config
     if form.cleaned_data['participation_fee'] is not None:
         config[
             'participation_fee'
             # need to convert back to RealWorldCurrency, because easymoney
             # MoneyFormField returns a decimal, not Money (not sure why)
         ] = RealWorldCurrency(participation_fee)
     if form.cleaned_data['real_world_currency_per_point'] is not None:
         config[
             'real_world_currency_per_point'] = real_world_currency_per_point
     self.session.save()
     messages.success(self.request, 'Properties have been updated')
     return HttpResponseRedirect(self.get_success_url())
Пример #7
0
    def post(self, request, *args, **kwargs):
        form = self.get_form(data=request.POST, files=request.FILES)
        if not form.is_valid():
            return self.form_invalid(form)
        session = self.session
        use_sandbox = 'use_sandbox' in form.data
        # session can't be created
        if (not self.in_public_domain(request, *args, **kwargs)
                and not use_sandbox):
            msg = ('<h1>Error: '
                   'oTree must run on a public domain for Mechanical Turk'
                   '</h1>')
            return HttpResponseServerError(msg)
        mturk_settings = session.config['mturk_hit_settings']
        qualification_id = mturk_settings.get('grant_qualification_id', None)
        # verify that specified qualification type
        # for preventing retakes exists on mturk server

        url_landing_page = self.request.build_absolute_uri(
            reverse('MTurkLandingPage', args=(session.code, )))

        # updating schema from http to https
        # this is compulsory for MTurk exteranlQuestion
        # TODO: validate, that the server support https
        #       (heroku does support by default)
        secured_url_landing_page = urlunparse(
            urlparse(url_landing_page)._replace(scheme='https'))

        # TODO: validate that there is enough money for the hit
        money_reward = form.cleaned_data['money_reward']

        # assign back to participation_fee, in case it was changed
        # in the form
        # need to convert back to RealWorldCurrency, because easymoney
        # MoneyFormField returns a decimal, not Money (not sure why)
        # see views.admin.EditSessionProperties
        session.config['participation_fee'] = RealWorldCurrency(money_reward)

        external_question = '''
        <ExternalQuestion xmlns="http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2006-07-14/ExternalQuestion.xsd">
          <ExternalURL>{}</ExternalURL>
          <FrameHeight>{}</FrameHeight>
        </ExternalQuestion>
        '''.format(secured_url_landing_page, mturk_settings['frame_height'])

        qualifications = mturk_settings.get('qualification_requirements')

        if qualifications and not isinstance(qualifications[0], dict):
            raise ValueError(
                'settings.py: You need to upgrade your MTurk qualification_requirements '
                'to the boto3 format. See the documentation.')

        mturk_hit_parameters = {
            'Title':
            form.cleaned_data['title'],
            'Description':
            form.cleaned_data['description'],
            'Keywords':
            form.cleaned_data['keywords'],
            'Question':
            external_question,
            'MaxAssignments':
            form.cleaned_data['assignments'],
            'Reward':
            str(float(money_reward)),
            'QualificationRequirements':
            qualifications,
            'AssignmentDurationInSeconds':
            60 * form.cleaned_data['minutes_allotted_per_assignment'],
            'LifetimeInSeconds':
            int(60 * 60 * form.cleaned_data['expiration_hours']),
            # prevent duplicate HITs
            'UniqueRequestToken':
            'otree_{}'.format(session.code),
        }

        with MTurkClient(use_sandbox=use_sandbox,
                         request=request) as mturk_client:
            if qualification_id:
                try:
                    mturk_client.get_qualification_type(
                        QualificationTypeId=qualification_id)
                # it's RequestError, but
                except Exception as exc:
                    if use_sandbox:
                        sandbox_note = (
                            'You are currently using the sandbox, so you '
                            'can only grant qualifications that were '
                            'also created in the sandbox.')
                    else:
                        sandbox_note = (
                            'You are using the MTurk live site, so you '
                            'can only grant qualifications that were '
                            'also created on the live site, and not the '
                            'MTurk sandbox.')
                    msg = (
                        "In settings.py you specified qualification ID '{qualification_id}' "
                        "MTurk returned the following error: [{exc}] "
                        "Note: {sandbox_note}".format(
                            qualification_id=qualification_id,
                            exc=exc,
                            sandbox_note=sandbox_note))
                    messages.error(request, msg)
                    return HttpResponseRedirect(
                        reverse('MTurkCreateHIT', args=(session.code, )))

            hit = mturk_client.create_hit(**mturk_hit_parameters)['HIT']

            session.mturk_HITId = hit['HITId']
            session.mturk_HITGroupId = hit['HITGroupId']
            session.mturk_use_sandbox = use_sandbox
            session.save()

        return HttpResponseRedirect(
            reverse('MTurkCreateHIT', args=(session.code, )))
Пример #8
0
    def clean(self):

        required_keys = [
            'name',
            'app_sequence',
            'num_demo_participants',
            'participation_fee',
            'real_world_currency_per_point',
        ]

        for key in required_keys:
            if key not in self:
                raise SessionConfigError(
                    'settings.SESSION_CONFIGS: all configs must have a '
                    '"{}"'.format(key)
                )

        datatypes = {
            'app_sequence': list,
            'num_demo_participants': int,
            'name': str,
        }

        for key, datatype in datatypes.items():
            if not isinstance(self[key], datatype):
                msg = (
                    'SESSION_CONFIGS "{}": '
                    'the entry "{}" must be of type {}'
                ).format(self['name'], key, datatype.__name__)
                raise SessionConfigError(msg)

        # Allow non-ASCII chars in session config keys, because they are
        # configurable in the admin, so need to be readable by non-English
        # speakers. However, don't allow punctuation, spaces, etc.
        # They make it harder to reason about and could cause problems
        # later on. also could uglify the user's code.

        INVALID_IDENTIFIER_MSG = (
            'Key "{}" in settings.SESSION_CONFIGS '
            'must not contain spaces, punctuation, '
            'or other special characters. '
            'It can contain non-English characters, '
            'but it must be a valid Python variable name '
            'according to string.isidentifier().'
        )

        for key in self:
            if not key.isidentifier():
                raise SessionConfigError(INVALID_IDENTIFIER_MSG.format(key))

        validate_alphanumeric(
            self['name'],
            identifier_description='settings.SESSION_CONFIGS name'
        )

        app_sequence = self['app_sequence']
        if len(app_sequence) != len(set(app_sequence)):
            msg = (
                'settings.SESSION_CONFIGS: '
                'app_sequence of "{}" '
                'must not contain duplicate elements. '
                'If you want multiple rounds, '
                'you should set Constants.num_rounds.')
            raise SessionConfigError(msg.format(self['name']))

        if len(app_sequence) == 0:
            raise SessionConfigError(
                'settings.SESSION_CONFIGS: app_sequence cannot be empty.')

        self.setdefault('display_name', self['name'])
        self.setdefault('doc', '')

        self['participation_fee'] = RealWorldCurrency(
            self['participation_fee'])
Пример #9
0
    def post(self, request, **kwargs):
        form = self.get_form(data=request.POST, files=request.FILES)
        if not form.is_valid():
            return self.form_invalid(form)
        session = self.session
        cleaned_data = form.cleaned_data

        use_sandbox = bool(cleaned_data['use_sandbox'])
        if (not self.in_public_domain(request, **kwargs) and not use_sandbox):
            msg = ('<h1>Error: '
                   'oTree must run on a public domain for Mechanical Turk'
                   '</h1>')
            return HttpResponseServerError(msg)
        mturk_settings = session.config['mturk_hit_settings']

        url_landing_page = self.request.build_absolute_uri(
            reverse('MTurkLandingPage', args=(session.code, )))

        # updating schema from http to https
        # this is compulsory for MTurk exteranlQuestion
        #       (heroku does support by default)
        secured_url_landing_page = urlunparse(
            urlparse(url_landing_page)._replace(scheme='https'))

        # TODO: validate that there is enough money for the hit
        money_reward = cleaned_data['money_reward']

        # assign back to participation_fee, in case it was changed
        # in the form
        # need to convert back to RealWorldCurrency, because easymoney
        # MoneyFormField returns a decimal, not Money (not sure why)
        # see views.admin.EditSessionProperties
        session.config['participation_fee'] = RealWorldCurrency(money_reward)

        external_question = '''
        <ExternalQuestion xmlns="http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2006-07-14/ExternalQuestion.xsd">
          <ExternalURL>{}</ExternalURL>
          <FrameHeight>{}</FrameHeight>
        </ExternalQuestion>
        '''.format(secured_url_landing_page, mturk_settings['frame_height'])

        mturk_hit_parameters = {
            'Title':
            cleaned_data['title'],
            'Description':
            cleaned_data['description'],
            'Keywords':
            cleaned_data['keywords'],
            'Question':
            external_question,
            'MaxAssignments':
            cleaned_data['assignments'],
            'Reward':
            str(float(money_reward)),
            'AssignmentDurationInSeconds':
            60 * cleaned_data['minutes_allotted_per_assignment'],
            'LifetimeInSeconds':
            int(60 * 60 * cleaned_data['expiration_hours']),
            # prevent duplicate HITs
            'UniqueRequestToken':
            'otree_{}'.format(session.code),
        }

        if not use_sandbox:
            # drop requirements checks in sandbox mode.
            mturk_hit_parameters['QualificationRequirements'] = (
                mturk_settings.get('qualification_requirements', []))

        with MTurkClient(use_sandbox=use_sandbox,
                         request=request) as mturk_client:

            hit = mturk_client.create_hit(**mturk_hit_parameters)['HIT']

            session.mturk_HITId = hit['HITId']
            session.mturk_HITGroupId = hit['HITGroupId']
            session.mturk_use_sandbox = use_sandbox
            session.mturk_expiration = hit['Expiration'].timestamp()
            session.save()

        return redirect('MTurkCreateHIT', session.code)
Пример #10
0
    def clean(self):

        config_schema = schema.Schema({
            'name': str,
            'app_sequence': list,
            'participation_fee': object,
            'real_world_currency_per_point': object,
            'num_demo_participants': int,
            'doc': str,
            str: object,
        })

        try:
            config_schema.validate(self)
        except schema.SchemaError as e:
            raise ValueError('settings.SESSION_CONFIGS: {}'.format(e)) from None

        # Allow non-ASCII chars in session config keys, because they are
        # configurable in the admin, so need to be readable by non-English
        # speakers. However, don't allow punctuation, spaces, etc.
        # They make it harder to reason about and could cause problems
        # later on. also could uglify the user's code.

        INVALID_IDENTIFIER_MSG = (
            'Key "{}" in settings.SESSION_CONFIGS '
            'must not contain spaces, punctuation, '
            'or other special characters. '
            'It can contain non-English characters, '
            'but it must be a valid Python variable name '
            'according to string.isidentifier().'
        )

        for key in self:
            if not key.isidentifier():
                raise ValueError(INVALID_IDENTIFIER_MSG.format(key))

        validate_alphanumeric(
            self['name'],
            identifier_description='settings.SESSION_CONFIGS name'
        )

        app_sequence = self['app_sequence']
        if len(app_sequence) != len(set(app_sequence)):
            msg = (
                'settings.SESSION_CONFIGS: '
                'app_sequence of "{}" '
                'must not contain duplicate elements. '
                'If you want multiple rounds, '
                'you should set Constants.num_rounds.')
            raise ValueError(msg.format(self['name']))

        if len(app_sequence) == 0:
            raise ValueError(
                'settings.SESSION_CONFIGS: Need at least one subsession.')

        self.setdefault('display_name', self['name'])
        self.setdefault('doc', '')

        # TODO: fixed_pay is deprecated as of 2015-05-07,
        # in favor of participation_fee. make this required at some point.
        if (('participation_fee' not in self) and
                ('fixed_pay' in self)):
            warn_msg = (
                "'fixed_pay' is deprecated; "
                "you should rename it to 'participation_fee'.")
            warnings.warn(warn_msg, OtreeDeprecationWarning)

            self['participation_fee'] = self['fixed_pay']

        self['participation_fee'] = RealWorldCurrency(
            self['participation_fee'])

        # normalize to decimal so we can do multiplications, etc
        # quantize because the original value may be a float,
        # which when converted to Decimal may have some 'decimal junk'
        # like 0.010000000000000000208166817...
        self['real_world_currency_per_point'] = Decimal(
            self['real_world_currency_per_point']
        ).quantize(Decimal('0.00001'))
Пример #11
0
    def post(self, request, *args, **kwargs):
        form = self.get_form(
            data=request.POST,
            files=request.FILES
        )
        if not form.is_valid():
            return self.form_invalid(form)
        session = self.session
        in_sandbox = 'in_sandbox' in form.data
        # session can't be created
        if (not self.in_public_domain(request, *args, **kwargs) and
           not in_sandbox):
                msg = (
                    '<h1>Error: '
                    'oTree must run on a public domain for Mechanical Turk'
                    '</h1>')
                return HttpResponseServerError(msg)
        with MTurkConnection(self.request, in_sandbox) as mturk_connection:
            mturk_settings = session.config['mturk_hit_settings']
            qualification_id = mturk_settings.get(
                'grant_qualification_id', None)
            # verify that specified qualification type
            # for preventing retakes exists on mturk server
            if qualification_id:
                try:
                    mturk_connection.get_qualification_type(qualification_id)
                except MTurkRequestError as e:
                    code = 'AWS.MechanicalTurk.QualificationTypeDoesNotExist'
                    if e.error_code == code:
                        if in_sandbox:
                            sandbox_note = (
                                'You are currently using the sandbox, so you '
                                'can only grant qualifications that were '
                                'also created in the sandbox.')
                        else:
                            sandbox_note = (
                                'You are using the MTurk live site, so you '
                                'can only grant qualifications that were '
                                'also created on the live site, and not the '
                                'MTurk sandbox.')
                        msg = (
                            "In settings.py you specified qualification ID "
                            " '{}' which doesn't exist in your MTurk account. "
                            "Please check its validity. Note: {}".format(
                                qualification_id, sandbox_note))
                        if in_sandbox:
                            msg += ' You are currently'
                        messages.error(request, msg)
                        return HttpResponseRedirect(
                            reverse(
                                'MTurkCreateHIT', args=(session.code,)))
                else:
                    session.mturk_qualification_type_id = qualification_id

            url_landing_page = self.request.build_absolute_uri(
                reverse('MTurkLandingPage', args=(session.code,)))

            # updating schema from http to https
            # this is compulsory for MTurk exteranlQuestion
            # TODO: validate, that the server support https
            #       (heroku does support by default)
            secured_url_landing_page = urlunparse(
                urlparse(url_landing_page)._replace(scheme='https'))

            # TODO: validate that there is enought money for the hit
            money_reward = form.data['money_reward']
            reward = boto.mturk.price.Price(
                amount=float(money_reward))

            # assign back to participation_fee, in case it was changed
            # in the form
            # TODO: why do I have to explicitly
            # convert back to RealWorldCurrency?
            # shouldn't it already be that?
            session.config['participation_fee'] = RealWorldCurrency(
                money_reward)

            # creating external questions, that would be passed to the hit
            external_question = boto.mturk.question.ExternalQuestion(
                secured_url_landing_page, mturk_settings['frame_height'])

            qualifications = mturk_settings.get('qualification_requirements')

            # deprecated summer 2015: remove this
            if not qualifications and \
               hasattr(settings, 'MTURK_WORKER_REQUIREMENTS'):
                    warn_msg = (
                        'The MTURK_WORKER_REQUIREMENTS setting has been '
                        'deprecated. You should instead use '
                        '"qualification_requirements" as shown here: '
                        'https://github.com/oTree-org/oTree/blob/master/'
                        'settings.py')
                    warnings.warn(warn_msg, OtreeDeprecationWarning)
                    qualifications = settings.MTURK_WORKER_REQUIREMENTS

            mturk_hit_parameters = {
                'title': form.cleaned_data['title'],
                'description': form.cleaned_data['description'],
                'keywords': [
                    k.strip() for k in
                    form.cleaned_data['keywords'].split(',')
                ],
                'question': external_question,
                'max_assignments': form.cleaned_data['assignments'],
                'reward': reward,
                'response_groups': ('Minimal', 'HITDetail'),
                'qualifications': Qualifications(qualifications),
                'duration': datetime.timedelta(
                    minutes=(
                        form.cleaned_data['minutes_allotted_per_assignment']
                    )),
                'lifetime': datetime.timedelta(
                    minutes=int(60*form.cleaned_data['expiration_hours']))
            }

            hit = mturk_connection.create_hit(**mturk_hit_parameters)
            session.mturk_HITId = hit[0].HITId
            session.mturk_HITGroupId = hit[0].HITGroupId
            session.mturk_sandbox = in_sandbox
            session.save()

        return HttpResponseRedirect(
            reverse('MTurkCreateHIT', args=(session.code,)))