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
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'))
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'])
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())
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())
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())
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, )))
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'])
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)
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'))
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,)))