1
# Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/
3
# Permission is hereby granted, free of charge, to any person obtaining a
4
# copy of this software and associated documentation files (the
5
# "Software"), to deal in the Software without restriction, including
6
# without limitation the rights to use, copy, modify, merge, publish, dis-
7
# tribute, sublicense, and/or sell copies of the Software, and to permit
8
# persons to whom the Software is furnished to do so, subject to the fol-
11
# The above copyright notice and this permission notice shall be included
12
# in all copies or substantial portions of the Software.
14
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
15
# OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABIL-
16
# ITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
17
# SHALL THE AUTHOR BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
18
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
22
class Question(object):
24
QUESTION_XML_TEMPLATE = """<Question><QuestionIdentifier>%s</QuestionIdentifier>%s<IsRequired>%s</IsRequired>%s%s</Question>"""
25
DISPLAY_NAME_XML_TEMPLATE = """<DisplayName>%s</DisplayName>"""
27
def __init__(self, identifier, content, answer_spec, is_required=False, display_name=None): #amount=0.0, currency_code='USD'):
28
self.identifier = identifier
29
self.content = content
30
self.answer_spec = answer_spec
31
self.is_required = is_required
32
self.display_name = display_name
34
def get_as_params(self, label='Question', identifier=None):
36
if identifier is None:
37
raise ValueError("identifier (QuestionIdentifier) is required per MTurk spec.")
39
return { label : self.get_as_xml() }
42
# add the display name if required
45
display_name_xml = self.DISPLAY_NAME_XML_TEMPLATE %(self.display_name)
47
ret = Question.QUESTION_XML_TEMPLATE % (self.identifier,
49
str(self.is_required).lower(),
50
self.content.get_as_xml(),
51
self.answer_spec.get_as_xml())
55
class ExternalQuestion(object):
57
EXTERNAL_QUESTIONFORM_SCHEMA_LOCATION = "http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2006-07-14/ExternalQuestion.xsd"
58
EXTERNAL_QUESTION_XML_TEMPLATE = """<ExternalQuestion xmlns="%s"><ExternalURL>%s</ExternalURL><FrameHeight>%s</FrameHeight></ExternalQuestion>"""
60
def __init__(self, external_url, frame_height):
61
self.external_url = external_url
62
self.frame_height = frame_height
64
def get_as_params(self, label='ExternalQuestion'):
65
return { label : self.get_as_xml() }
68
ret = ExternalQuestion.EXTERNAL_QUESTION_XML_TEMPLATE % (ExternalQuestion.EXTERNAL_QUESTIONFORM_SCHEMA_LOCATION,
73
class OrderedContent(object):
77
def append(self, field, value):
78
"Expects field type and value"
79
self.items.append((field, value))
81
def get_binary_xml(self, field, value):
90
</Binary>""" % (value['type'],
95
def get_application_xml(self, field, value):
96
raise NotImplementedError("Application question content is not yet supported.")
99
default_handler = lambda f,v: '<%s>%s</%s>' % (f,v,f)
100
bulleted_list_handler = lambda _,list: '<List>%s</List>' % ''.join([('<ListItem>%s</ListItem>' % item) for item in list])
101
formatted_content_handler = lambda _,content: "<FormattedContent><![CDATA[%s]]></FormattedContent>" % content
102
application_handler = self.get_application_xml
103
binary_handler = self.get_binary_xml
106
for (field,value) in self.items:
107
handler = default_handler
109
handler = bulleted_list_handler
110
elif field == 'Application':
111
handler = application_handler
112
elif field == 'Binary':
113
handler = binary_handler
114
elif field == 'FormattedContent':
115
handler = formatted_content_handler
116
children = children + handler(field, value)
120
class Overview(object):
121
OVERVIEW_XML_TEMPLATE = """<Overview>%s</Overview>"""
124
self.ordered_content = OrderedContent()
126
def append(self, field, value):
127
self.ordered_content.append(field,value)
129
def get_as_params(self, label='Overview'):
130
return { label : self.get_as_xml() }
132
def get_as_xml(self):
133
ret = Overview.OVERVIEW_XML_TEMPLATE % (self.ordered_content.get_as_xml())
138
class QuestionForm(object):
140
QUESTIONFORM_SCHEMA_LOCATION = "http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchemas/2005-10-01/QuestionForm.xsd"
141
QUESTIONFORM_XML_TEMPLATE = """<QuestionForm xmlns="%s">%s</QuestionForm>"""
146
def append(self, item):
147
"Expects field type and value"
148
self.items.append(item)
150
def get_as_xml(self):
152
for item in self.items:
153
xml = xml + item.get_as_xml()
154
return QuestionForm.QUESTIONFORM_XML_TEMPLATE % (QuestionForm.QUESTIONFORM_SCHEMA_LOCATION, xml)
156
class QuestionContent(object):
157
QUESTIONCONTENT_XML_TEMPLATE = """<QuestionContent>%s</QuestionContent>"""
160
self.ordered_content = OrderedContent()
162
def append(self, field, value):
163
self.ordered_content.append(field,value)
165
def get_as_xml(self):
166
ret = QuestionContent.QUESTIONCONTENT_XML_TEMPLATE % (self.ordered_content.get_as_xml())
171
class AnswerSpecification(object):
173
ANSWERSPECIFICATION_XML_TEMPLATE = """<AnswerSpecification>%s</AnswerSpecification>"""
175
def __init__(self, spec):
177
def get_as_xml(self):
179
return AnswerSpecification.ANSWERSPECIFICATION_XML_TEMPLATE % self.spec.get_as_xml()
181
class FreeTextAnswer(object):
183
FREETEXTANSWER_XML_TEMPLATE = """<FreeTextAnswer>%s%s</FreeTextAnswer>""" # (constraints, default)
184
FREETEXTANSWER_CONSTRAINTS_XML_TEMPLATE = """<Constraints>%s%s%s</Constraints>""" # (is_numeric_xml, length_xml, regex_xml)
185
FREETEXTANSWER_LENGTH_XML_TEMPLATE = """<Length %s %s />""" # (min_length_attr, max_length_attr)
186
FREETEXTANSWER_ISNUMERIC_XML_TEMPLATE = """<IsNumeric %s %s />""" # (min_value_attr, max_value_attr)
187
FREETEXTANSWER_DEFAULTTEXT_XML_TEMPLATE = """<DefaultText>%s</DefaultText>""" # (default)
189
def __init__(self, default=None, min_length=None, max_length=None, is_numeric=False, min_value=None, max_value=None, format_regex=None):
190
self.default = default
191
self.min_length = min_length
192
self.max_length = max_length
193
self.is_numeric = is_numeric
194
self.min_value = min_value
195
self.max_value = max_value
196
self.format_regex = format_regex
198
def get_as_xml(self):
204
min_value_attr = """minValue="%d" """ % self.min_value
206
max_value_attr = """maxValue="%d" """ % self.max_value
207
is_numeric_xml = FreeTextAnswer.FREETEXTANSWER_ISNUMERIC_XML_TEMPLATE % (min_value_attr, max_value_attr)
210
if self.min_length or self.max_length:
214
min_length_attr = """minLength="%d" """
216
max_length_attr = """maxLength="%d" """
217
length_xml = FreeTextAnswer.FREETEXTANSWER_LENGTH_XML_TEMPLATE % (min_length_attr, max_length_attr)
220
if self.format_regex:
221
format_regex_attribs = '''regex="%s"''' %self.format_regex['regex']
223
error_text = self.format_regex.get('error_text', None)
225
format_regex_attribs += ' errorText="%s"' %error_text
227
flags = self.format_regex.get('flags', None)
229
format_regex_attribs += ' flags="%s"' %flags
231
regex_xml = """<AnswerFormatRegex %s/>""" %format_regex_attribs
234
if is_numeric_xml or length_xml or regex_xml:
235
constraints_xml = FreeTextAnswer.FREETEXTANSWER_CONSTRAINTS_XML_TEMPLATE % (is_numeric_xml, length_xml, regex_xml)
238
if self.default is not None:
239
default_xml = FreeTextAnswer.FREETEXTANSWER_DEFAULTTEXT_XML_TEMPLATE % self.default
241
return FreeTextAnswer.FREETEXTANSWER_XML_TEMPLATE % (constraints_xml, default_xml)
243
class FileUploadAnswer(object):
244
FILEUPLOADANSWER_XML_TEMLPATE = """<FileUploadAnswer><MinFileSizeInBytes>%d</MinFileSizeInBytes><MaxFileSizeInBytes>%d</MaxFileSizeInBytes></FileUploadAnswer>""" # (min, max)
245
DEFAULT_MIN_SIZE = 1024 # 1K (completely arbitrary!)
246
DEFAULT_MAX_SIZE = 5 * 1024 * 1024 # 5MB (completely arbitrary!)
248
def __init__(self, min=None, max=None):
252
self.min = FileUploadAnswer.DEFAULT_MIN_SIZE
254
self.max = FileUploadAnswer.DEFAULT_MAX_SIZE
256
def get_as_xml(self):
257
return FileUploadAnswer.FILEUPLOADANSWER_XML_TEMLPATE % (self.min, self.max)
259
class SelectionAnswer(object):
261
A class to generate SelectionAnswer XML data structures.
262
Does not yet implement Binary selection options.
264
SELECTIONANSWER_XML_TEMPLATE = """<SelectionAnswer>%s%s<Selections>%s</Selections></SelectionAnswer>""" # % (count_xml, style_xml, selections_xml)
265
SELECTION_XML_TEMPLATE = """<Selection><SelectionIdentifier>%s</SelectionIdentifier>%s</Selection>""" # (identifier, value_xml)
266
SELECTION_VALUE_XML_TEMPLATE = """<%s>%s</%s>""" # (type, value, type)
267
STYLE_XML_TEMPLATE = """<StyleSuggestion>%s</StyleSuggestion>""" # (style)
268
MIN_SELECTION_COUNT_XML_TEMPLATE = """<MinSelectionCount>%s</MinSelectionCount>""" # count
269
MAX_SELECTION_COUNT_XML_TEMPLATE = """<MaxSelectionCount>%s</MaxSelectionCount>""" # count
270
ACCEPTED_STYLES = ['radiobutton', 'dropdown', 'checkbox', 'list', 'combobox', 'multichooser']
271
OTHER_SELECTION_ELEMENT_NAME = 'OtherSelection'
273
def __init__(self, min=1, max=1, style=None, selections=None, type='text', other=False):
275
if style is not None:
276
if style in SelectionAnswer.ACCEPTED_STYLES:
277
self.style_suggestion = style
279
raise ValueError("style '%s' not recognized; should be one of %s" % (style, ', '.join(SelectionAnswer.ACCEPTED_STYLES)))
281
self.style_suggestion = None
283
if selections is None:
284
raise ValueError("SelectionAnswer.__init__(): selections must be a non-empty list of (content, identifier) tuples")
286
self.selections = selections
288
self.min_selections = min
289
self.max_selections = max
291
assert len(selections) >= self.min_selections, "# of selections is less than minimum of %d" % self.min_selections
292
#assert len(selections) <= self.max_selections, "# of selections exceeds maximum of %d" % self.max_selections
298
def get_as_xml(self):
299
if self.type == 'text':
301
elif self.type == 'binary':
304
raise ValueError("illegal type: %s; must be either 'text' or 'binary'" % str(self.type))
306
# build list of <Selection> elements
308
for tpl in self.selections:
309
value_xml = SelectionAnswer.SELECTION_VALUE_XML_TEMPLATE % (TYPE_TAG, tpl[0], TYPE_TAG)
310
selection_xml = SelectionAnswer.SELECTION_XML_TEMPLATE % (tpl[1], value_xml)
311
selections_xml += selection_xml
314
# add OtherSelection element as xml if available
315
if hasattr(self.other, 'get_as_xml'):
316
assert type(self.other) == FreeTextAnswer, 'OtherSelection can only be a FreeTextAnswer'
317
selections_xml += self.other.get_as_xml().replace('FreeTextAnswer', 'OtherSelection')
319
selections_xml += "<OtherSelection />"
321
if self.style_suggestion is not None:
322
style_xml = SelectionAnswer.STYLE_XML_TEMPLATE % self.style_suggestion
326
if self.style_suggestion != 'radiobutton':
327
count_xml = SelectionAnswer.MIN_SELECTION_COUNT_XML_TEMPLATE %self.min_selections
328
count_xml += SelectionAnswer.MAX_SELECTION_COUNT_XML_TEMPLATE %self.max_selections
332
ret = SelectionAnswer.SELECTIONANSWER_XML_TEMPLATE % (count_xml, style_xml, selections_xml)