| OLD | NEW |
| (Empty) | |
| 1 # Copyright (c) 2006,2007 Mitch Garnaat http://garnaat.org/ |
| 2 # |
| 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- |
| 9 # lowing conditions: |
| 10 # |
| 11 # The above copyright notice and this permission notice shall be included |
| 12 # in all copies or substantial portions of the Software. |
| 13 # |
| 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 |
| 20 # IN THE SOFTWARE. |
| 21 |
| 22 |
| 23 class Question(object): |
| 24 template = "<Question>%(items)s</Question>" |
| 25 |
| 26 def __init__(self, identifier, content, answer_spec, |
| 27 is_required=False, display_name=None): |
| 28 # copy all of the parameters into object attributes |
| 29 self.__dict__.update(vars()) |
| 30 del self.self |
| 31 |
| 32 def get_as_params(self, label='Question'): |
| 33 return {label: self.get_as_xml()} |
| 34 |
| 35 def get_as_xml(self): |
| 36 items = [ |
| 37 SimpleField('QuestionIdentifier', self.identifier), |
| 38 SimpleField('IsRequired', str(self.is_required).lower()), |
| 39 self.content, |
| 40 self.answer_spec, |
| 41 ] |
| 42 if self.display_name is not None: |
| 43 items.insert(1, SimpleField('DisplayName', self.display_name)) |
| 44 items = ''.join(item.get_as_xml() for item in items) |
| 45 return self.template % vars() |
| 46 |
| 47 try: |
| 48 from lxml import etree |
| 49 |
| 50 class ValidatingXML(object): |
| 51 |
| 52 def validate(self): |
| 53 import urllib2 |
| 54 schema_src_file = urllib2.urlopen(self.schema_url) |
| 55 schema_doc = etree.parse(schema_src_file) |
| 56 schema = etree.XMLSchema(schema_doc) |
| 57 doc = etree.fromstring(self.get_as_xml()) |
| 58 schema.assertValid(doc) |
| 59 except ImportError: |
| 60 class ValidatingXML(object): |
| 61 |
| 62 def validate(self): |
| 63 pass |
| 64 |
| 65 |
| 66 class ExternalQuestion(ValidatingXML): |
| 67 """ |
| 68 An object for constructing an External Question. |
| 69 """ |
| 70 schema_url = "http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchem
as/2006-07-14/ExternalQuestion.xsd" |
| 71 template = '<ExternalQuestion xmlns="%(schema_url)s"><ExternalURL>%%(externa
l_url)s</ExternalURL><FrameHeight>%%(frame_height)s</FrameHeight></ExternalQuest
ion>' % vars() |
| 72 |
| 73 def __init__(self, external_url, frame_height): |
| 74 self.external_url = external_url |
| 75 self.frame_height = frame_height |
| 76 |
| 77 def get_as_params(self, label='ExternalQuestion'): |
| 78 return {label: self.get_as_xml()} |
| 79 |
| 80 def get_as_xml(self): |
| 81 return self.template % vars(self) |
| 82 |
| 83 |
| 84 class XMLTemplate: |
| 85 def get_as_xml(self): |
| 86 return self.template % vars(self) |
| 87 |
| 88 |
| 89 class SimpleField(object, XMLTemplate): |
| 90 """ |
| 91 A Simple name/value pair that can be easily rendered as XML. |
| 92 |
| 93 >>> SimpleField('Text', 'A text string').get_as_xml() |
| 94 '<Text>A text string</Text>' |
| 95 """ |
| 96 template = '<%(field)s>%(value)s</%(field)s>' |
| 97 |
| 98 def __init__(self, field, value): |
| 99 self.field = field |
| 100 self.value = value |
| 101 |
| 102 |
| 103 class Binary(object, XMLTemplate): |
| 104 template = """<Binary><MimeType><Type>%(type)s</Type><SubType>%(subtype)s</S
ubType></MimeType><DataURL>%(url)s</DataURL><AltText>%(alt_text)s</AltText></Bin
ary>""" |
| 105 |
| 106 def __init__(self, type, subtype, url, alt_text): |
| 107 self.__dict__.update(vars()) |
| 108 del self.self |
| 109 |
| 110 |
| 111 class List(list): |
| 112 """A bulleted list suitable for OrderedContent or Overview content""" |
| 113 def get_as_xml(self): |
| 114 items = ''.join('<ListItem>%s</ListItem>' % item for item in self) |
| 115 return '<List>%s</List>' % items |
| 116 |
| 117 |
| 118 class Application(object): |
| 119 template = "<Application><%(class_)s>%(content)s</%(class_)s></Application>" |
| 120 parameter_template = "<Name>%(name)s</Name><Value>%(value)s</Value>" |
| 121 |
| 122 def __init__(self, width, height, **parameters): |
| 123 self.width = width |
| 124 self.height = height |
| 125 self.parameters = parameters |
| 126 |
| 127 def get_inner_content(self, content): |
| 128 content.append_field('Width', self.width) |
| 129 content.append_field('Height', self.height) |
| 130 for name, value in self.parameters.items(): |
| 131 value = self.parameter_template % vars() |
| 132 content.append_field('ApplicationParameter', value) |
| 133 |
| 134 def get_as_xml(self): |
| 135 content = OrderedContent() |
| 136 self.get_inner_content(content) |
| 137 content = content.get_as_xml() |
| 138 class_ = self.__class__.__name__ |
| 139 return self.template % vars() |
| 140 |
| 141 |
| 142 class HTMLQuestion(ValidatingXML): |
| 143 schema_url = 'http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchem
as/2011-11-11/HTMLQuestion.xsd' |
| 144 template = '<HTMLQuestion xmlns=\"%(schema_url)s\"><HTMLContent><![CDATA[<!D
OCTYPE html>%%(html_form)s]]></HTMLContent><FrameHeight>%%(frame_height)s</Frame
Height></HTMLQuestion>' % vars() |
| 145 |
| 146 def __init__(self, html_form, frame_height): |
| 147 self.html_form = html_form |
| 148 self.frame_height = frame_height |
| 149 |
| 150 def get_as_params(self, label="HTMLQuestion"): |
| 151 return {label: self.get_as_xml()} |
| 152 |
| 153 def get_as_xml(self): |
| 154 return self.template % vars(self) |
| 155 |
| 156 |
| 157 class JavaApplet(Application): |
| 158 def __init__(self, path, filename, *args, **kwargs): |
| 159 self.path = path |
| 160 self.filename = filename |
| 161 super(JavaApplet, self).__init__(*args, **kwargs) |
| 162 |
| 163 def get_inner_content(self, content): |
| 164 content = OrderedContent() |
| 165 content.append_field('AppletPath', self.path) |
| 166 content.append_field('AppletFilename', self.filename) |
| 167 super(JavaApplet, self).get_inner_content(content) |
| 168 |
| 169 |
| 170 class Flash(Application): |
| 171 def __init__(self, url, *args, **kwargs): |
| 172 self.url = url |
| 173 super(Flash, self).__init__(*args, **kwargs) |
| 174 |
| 175 def get_inner_content(self, content): |
| 176 content = OrderedContent() |
| 177 content.append_field('FlashMovieURL', self.url) |
| 178 super(Flash, self).get_inner_content(content) |
| 179 |
| 180 |
| 181 class FormattedContent(object, XMLTemplate): |
| 182 schema_url = 'http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchem
as/2006-07-14/FormattedContentXHTMLSubset.xsd' |
| 183 template = '<FormattedContent><![CDATA[%(content)s]]></FormattedContent>' |
| 184 |
| 185 def __init__(self, content): |
| 186 self.content = content |
| 187 |
| 188 |
| 189 class OrderedContent(list): |
| 190 |
| 191 def append_field(self, field, value): |
| 192 self.append(SimpleField(field, value)) |
| 193 |
| 194 def get_as_xml(self): |
| 195 return ''.join(item.get_as_xml() for item in self) |
| 196 |
| 197 |
| 198 class Overview(OrderedContent): |
| 199 template = '<Overview>%(content)s</Overview>' |
| 200 |
| 201 def get_as_params(self, label='Overview'): |
| 202 return {label: self.get_as_xml()} |
| 203 |
| 204 def get_as_xml(self): |
| 205 content = super(Overview, self).get_as_xml() |
| 206 return self.template % vars() |
| 207 |
| 208 |
| 209 class QuestionForm(ValidatingXML, list): |
| 210 """ |
| 211 From the AMT API docs: |
| 212 |
| 213 The top-most element of the QuestionForm data structure is a |
| 214 QuestionForm element. This element contains optional Overview |
| 215 elements and one or more Question elements. There can be any |
| 216 number of these two element types listed in any order. The |
| 217 following example structure has an Overview element and a |
| 218 Question element followed by a second Overview element and |
| 219 Question element--all within the same QuestionForm. |
| 220 |
| 221 :: |
| 222 |
| 223 <QuestionForm xmlns="[the QuestionForm schema URL]"> |
| 224 <Overview> |
| 225 [...] |
| 226 </Overview> |
| 227 <Question> |
| 228 [...] |
| 229 </Question> |
| 230 <Overview> |
| 231 [...] |
| 232 </Overview> |
| 233 <Question> |
| 234 [...] |
| 235 </Question> |
| 236 [...] |
| 237 </QuestionForm> |
| 238 |
| 239 QuestionForm is implemented as a list, so to construct a |
| 240 QuestionForm, simply append Questions and Overviews (with at least |
| 241 one Question). |
| 242 """ |
| 243 schema_url = "http://mechanicalturk.amazonaws.com/AWSMechanicalTurkDataSchem
as/2005-10-01/QuestionForm.xsd" |
| 244 xml_template = """<QuestionForm xmlns="%(schema_url)s">%%(items)s</QuestionF
orm>""" % vars() |
| 245 |
| 246 def is_valid(self): |
| 247 return ( |
| 248 any(isinstance(item, Question) for item in self) |
| 249 and |
| 250 all(isinstance(item, (Question, Overview)) for item in self) |
| 251 ) |
| 252 |
| 253 def get_as_xml(self): |
| 254 assert self.is_valid(), "QuestionForm contains invalid elements" |
| 255 items = ''.join(item.get_as_xml() for item in self) |
| 256 return self.xml_template % vars() |
| 257 |
| 258 |
| 259 class QuestionContent(OrderedContent): |
| 260 template = '<QuestionContent>%(content)s</QuestionContent>' |
| 261 |
| 262 def get_as_xml(self): |
| 263 content = super(QuestionContent, self).get_as_xml() |
| 264 return self.template % vars() |
| 265 |
| 266 |
| 267 class AnswerSpecification(object): |
| 268 template = '<AnswerSpecification>%(spec)s</AnswerSpecification>' |
| 269 |
| 270 def __init__(self, spec): |
| 271 self.spec = spec |
| 272 |
| 273 def get_as_xml(self): |
| 274 spec = self.spec.get_as_xml() |
| 275 return self.template % vars() |
| 276 |
| 277 |
| 278 class Constraints(OrderedContent): |
| 279 template = '<Constraints>%(content)s</Constraints>' |
| 280 |
| 281 def get_as_xml(self): |
| 282 content = super(Constraints, self).get_as_xml() |
| 283 return self.template % vars() |
| 284 |
| 285 |
| 286 class Constraint(object): |
| 287 def get_attributes(self): |
| 288 pairs = zip(self.attribute_names, self.attribute_values) |
| 289 attrs = ' '.join( |
| 290 '%s="%d"' % (name, value) |
| 291 for (name, value) in pairs |
| 292 if value is not None |
| 293 ) |
| 294 return attrs |
| 295 |
| 296 def get_as_xml(self): |
| 297 attrs = self.get_attributes() |
| 298 return self.template % vars() |
| 299 |
| 300 |
| 301 class NumericConstraint(Constraint): |
| 302 attribute_names = 'minValue', 'maxValue' |
| 303 template = '<IsNumeric %(attrs)s />' |
| 304 |
| 305 def __init__(self, min_value=None, max_value=None): |
| 306 self.attribute_values = min_value, max_value |
| 307 |
| 308 |
| 309 class LengthConstraint(Constraint): |
| 310 attribute_names = 'minLength', 'maxLength' |
| 311 template = '<Length %(attrs)s />' |
| 312 |
| 313 def __init__(self, min_length=None, max_length=None): |
| 314 self.attribute_values = min_length, max_length |
| 315 |
| 316 |
| 317 class RegExConstraint(Constraint): |
| 318 attribute_names = 'regex', 'errorText', 'flags' |
| 319 template = '<AnswerFormatRegex %(attrs)s />' |
| 320 |
| 321 def __init__(self, pattern, error_text=None, flags=None): |
| 322 self.attribute_values = pattern, error_text, flags |
| 323 |
| 324 def get_attributes(self): |
| 325 pairs = zip(self.attribute_names, self.attribute_values) |
| 326 attrs = ' '.join( |
| 327 '%s="%s"' % (name, value) |
| 328 for (name, value) in pairs |
| 329 if value is not None |
| 330 ) |
| 331 return attrs |
| 332 |
| 333 |
| 334 class NumberOfLinesSuggestion(object): |
| 335 template = '<NumberOfLinesSuggestion>%(num_lines)s</NumberOfLinesSuggestion>
' |
| 336 |
| 337 def __init__(self, num_lines=1): |
| 338 self.num_lines = num_lines |
| 339 |
| 340 def get_as_xml(self): |
| 341 num_lines = self.num_lines |
| 342 return self.template % vars() |
| 343 |
| 344 |
| 345 class FreeTextAnswer(object): |
| 346 template = '<FreeTextAnswer>%(items)s</FreeTextAnswer>' |
| 347 |
| 348 def __init__(self, default=None, constraints=None, num_lines=None): |
| 349 self.default = default |
| 350 if constraints is None: |
| 351 self.constraints = Constraints() |
| 352 else: |
| 353 self.constraints = Constraints(constraints) |
| 354 self.num_lines = num_lines |
| 355 |
| 356 def get_as_xml(self): |
| 357 items = [self.constraints] |
| 358 if self.default: |
| 359 items.append(SimpleField('DefaultText', self.default)) |
| 360 if self.num_lines: |
| 361 items.append(NumberOfLinesSuggestion(self.num_lines)) |
| 362 items = ''.join(item.get_as_xml() for item in items) |
| 363 return self.template % vars() |
| 364 |
| 365 |
| 366 class FileUploadAnswer(object): |
| 367 template = """<FileUploadAnswer><MaxFileSizeInBytes>%(max_bytes)d</MaxFileSi
zeInBytes><MinFileSizeInBytes>%(min_bytes)d</MinFileSizeInBytes></FileUploadAnsw
er>""" |
| 368 |
| 369 def __init__(self, min_bytes, max_bytes): |
| 370 assert 0 <= min_bytes <= max_bytes <= 2 * 10 ** 9 |
| 371 self.min_bytes = min_bytes |
| 372 self.max_bytes = max_bytes |
| 373 |
| 374 def get_as_xml(self): |
| 375 return self.template % vars(self) |
| 376 |
| 377 |
| 378 class SelectionAnswer(object): |
| 379 """ |
| 380 A class to generate SelectionAnswer XML data structures. |
| 381 Does not yet implement Binary selection options. |
| 382 """ |
| 383 SELECTIONANSWER_XML_TEMPLATE = """<SelectionAnswer>%s%s<Selections>%s</Selec
tions></SelectionAnswer>""" # % (count_xml, style_xml, selections_xml) |
| 384 SELECTION_XML_TEMPLATE = """<Selection><SelectionIdentifier>%s</SelectionIde
ntifier>%s</Selection>""" # (identifier, value_xml) |
| 385 SELECTION_VALUE_XML_TEMPLATE = """<%s>%s</%s>""" # (type, value, type) |
| 386 STYLE_XML_TEMPLATE = """<StyleSuggestion>%s</StyleSuggestion>""" # (style) |
| 387 MIN_SELECTION_COUNT_XML_TEMPLATE = """<MinSelectionCount>%s</MinSelectionCou
nt>""" # count |
| 388 MAX_SELECTION_COUNT_XML_TEMPLATE = """<MaxSelectionCount>%s</MaxSelectionCou
nt>""" # count |
| 389 ACCEPTED_STYLES = ['radiobutton', 'dropdown', 'checkbox', 'list', 'combobox'
, 'multichooser'] |
| 390 OTHER_SELECTION_ELEMENT_NAME = 'OtherSelection' |
| 391 |
| 392 def __init__(self, min=1, max=1, style=None, selections=None, type='text', o
ther=False): |
| 393 |
| 394 if style is not None: |
| 395 if style in SelectionAnswer.ACCEPTED_STYLES: |
| 396 self.style_suggestion = style |
| 397 else: |
| 398 raise ValueError("style '%s' not recognized; should be one of %s
" % (style, ', '.join(SelectionAnswer.ACCEPTED_STYLES))) |
| 399 else: |
| 400 self.style_suggestion = None |
| 401 |
| 402 if selections is None: |
| 403 raise ValueError("SelectionAnswer.__init__(): selections must be a n
on-empty list of (content, identifier) tuples") |
| 404 else: |
| 405 self.selections = selections |
| 406 |
| 407 self.min_selections = min |
| 408 self.max_selections = max |
| 409 |
| 410 assert len(selections) >= self.min_selections, "# of selections is less
than minimum of %d" % self.min_selections |
| 411 #assert len(selections) <= self.max_selections, "# of selections exceeds
maximum of %d" % self.max_selections |
| 412 |
| 413 self.type = type |
| 414 |
| 415 self.other = other |
| 416 |
| 417 def get_as_xml(self): |
| 418 if self.type == 'text': |
| 419 TYPE_TAG = "Text" |
| 420 elif self.type == 'binary': |
| 421 TYPE_TAG = "Binary" |
| 422 else: |
| 423 raise ValueError("illegal type: %s; must be either 'text' or 'binary
'" % str(self.type)) |
| 424 |
| 425 # build list of <Selection> elements |
| 426 selections_xml = "" |
| 427 for tpl in self.selections: |
| 428 value_xml = SelectionAnswer.SELECTION_VALUE_XML_TEMPLATE % (TYPE_TAG
, tpl[0], TYPE_TAG) |
| 429 selection_xml = SelectionAnswer.SELECTION_XML_TEMPLATE % (tpl[1], va
lue_xml) |
| 430 selections_xml += selection_xml |
| 431 |
| 432 if self.other: |
| 433 # add OtherSelection element as xml if available |
| 434 if hasattr(self.other, 'get_as_xml'): |
| 435 assert isinstance(self.other, FreeTextAnswer), 'OtherSelection c
an only be a FreeTextAnswer' |
| 436 selections_xml += self.other.get_as_xml().replace('FreeTextAnswe
r', 'OtherSelection') |
| 437 else: |
| 438 selections_xml += "<OtherSelection />" |
| 439 |
| 440 if self.style_suggestion is not None: |
| 441 style_xml = SelectionAnswer.STYLE_XML_TEMPLATE % self.style_suggesti
on |
| 442 else: |
| 443 style_xml = "" |
| 444 |
| 445 if self.style_suggestion != 'radiobutton': |
| 446 count_xml = SelectionAnswer.MIN_SELECTION_COUNT_XML_TEMPLATE %self.m
in_selections |
| 447 count_xml += SelectionAnswer.MAX_SELECTION_COUNT_XML_TEMPLATE %self.
max_selections |
| 448 else: |
| 449 count_xml = "" |
| 450 |
| 451 ret = SelectionAnswer.SELECTIONANSWER_XML_TEMPLATE % (count_xml, style_x
ml, selections_xml) |
| 452 |
| 453 # return XML |
| 454 return ret |
| OLD | NEW |