You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

299 lines
9.5 KiB

4 years ago
  1. from lxml.etree import XPath, ElementBase
  2. from lxml.html import fromstring, XHTML_NAMESPACE
  3. from lxml.html import _forms_xpath, _options_xpath, _nons, _transform_result
  4. from lxml.html import defs
  5. import copy
  6. try:
  7. basestring
  8. except NameError:
  9. # Python 3
  10. basestring = str
  11. __all__ = ['FormNotFound', 'fill_form', 'fill_form_html',
  12. 'insert_errors', 'insert_errors_html',
  13. 'DefaultErrorCreator']
  14. class FormNotFound(LookupError):
  15. """
  16. Raised when no form can be found
  17. """
  18. _form_name_xpath = XPath('descendant-or-self::form[name=$name]|descendant-or-self::x:form[name=$name]', namespaces={'x':XHTML_NAMESPACE})
  19. _input_xpath = XPath('|'.join(['descendant-or-self::'+_tag for _tag in ('input','select','textarea','x:input','x:select','x:textarea')]),
  20. namespaces={'x':XHTML_NAMESPACE})
  21. _label_for_xpath = XPath('//label[@for=$for_id]|//x:label[@for=$for_id]',
  22. namespaces={'x':XHTML_NAMESPACE})
  23. _name_xpath = XPath('descendant-or-self::*[@name=$name]')
  24. def fill_form(
  25. el,
  26. values,
  27. form_id=None,
  28. form_index=None,
  29. ):
  30. el = _find_form(el, form_id=form_id, form_index=form_index)
  31. _fill_form(el, values)
  32. def fill_form_html(html, values, form_id=None, form_index=None):
  33. result_type = type(html)
  34. if isinstance(html, basestring):
  35. doc = fromstring(html)
  36. else:
  37. doc = copy.deepcopy(html)
  38. fill_form(doc, values, form_id=form_id, form_index=form_index)
  39. return _transform_result(result_type, doc)
  40. def _fill_form(el, values):
  41. counts = {}
  42. if hasattr(values, 'mixed'):
  43. # For Paste request parameters
  44. values = values.mixed()
  45. inputs = _input_xpath(el)
  46. for input in inputs:
  47. name = input.get('name')
  48. if not name:
  49. continue
  50. if _takes_multiple(input):
  51. value = values.get(name, [])
  52. if not isinstance(value, (list, tuple)):
  53. value = [value]
  54. _fill_multiple(input, value)
  55. elif name not in values:
  56. continue
  57. else:
  58. index = counts.get(name, 0)
  59. counts[name] = index + 1
  60. value = values[name]
  61. if isinstance(value, (list, tuple)):
  62. try:
  63. value = value[index]
  64. except IndexError:
  65. continue
  66. elif index > 0:
  67. continue
  68. _fill_single(input, value)
  69. def _takes_multiple(input):
  70. if _nons(input.tag) == 'select' and input.get('multiple'):
  71. # FIXME: multiple="0"?
  72. return True
  73. type = input.get('type', '').lower()
  74. if type in ('radio', 'checkbox'):
  75. return True
  76. return False
  77. def _fill_multiple(input, value):
  78. type = input.get('type', '').lower()
  79. if type == 'checkbox':
  80. v = input.get('value')
  81. if v is None:
  82. if not value:
  83. result = False
  84. else:
  85. result = value[0]
  86. if isinstance(value, basestring):
  87. # The only valid "on" value for an unnamed checkbox is 'on'
  88. result = result == 'on'
  89. _check(input, result)
  90. else:
  91. _check(input, v in value)
  92. elif type == 'radio':
  93. v = input.get('value')
  94. _check(input, v in value)
  95. else:
  96. assert _nons(input.tag) == 'select'
  97. for option in _options_xpath(input):
  98. v = option.get('value')
  99. if v is None:
  100. # This seems to be the default, at least on IE
  101. # FIXME: but I'm not sure
  102. v = option.text_content()
  103. _select(option, v in value)
  104. def _check(el, check):
  105. if check:
  106. el.set('checked', '')
  107. else:
  108. if 'checked' in el.attrib:
  109. del el.attrib['checked']
  110. def _select(el, select):
  111. if select:
  112. el.set('selected', '')
  113. else:
  114. if 'selected' in el.attrib:
  115. del el.attrib['selected']
  116. def _fill_single(input, value):
  117. if _nons(input.tag) == 'textarea':
  118. input.text = value
  119. else:
  120. input.set('value', value)
  121. def _find_form(el, form_id=None, form_index=None):
  122. if form_id is None and form_index is None:
  123. forms = _forms_xpath(el)
  124. for form in forms:
  125. return form
  126. raise FormNotFound(
  127. "No forms in page")
  128. if form_id is not None:
  129. form = el.get_element_by_id(form_id)
  130. if form is not None:
  131. return form
  132. forms = _form_name_xpath(el, name=form_id)
  133. if forms:
  134. return forms[0]
  135. else:
  136. raise FormNotFound(
  137. "No form with the name or id of %r (forms: %s)"
  138. % (id, ', '.join(_find_form_ids(el))))
  139. if form_index is not None:
  140. forms = _forms_xpath(el)
  141. try:
  142. return forms[form_index]
  143. except IndexError:
  144. raise FormNotFound(
  145. "There is no form with the index %r (%i forms found)"
  146. % (form_index, len(forms)))
  147. def _find_form_ids(el):
  148. forms = _forms_xpath(el)
  149. if not forms:
  150. yield '(no forms)'
  151. return
  152. for index, form in enumerate(forms):
  153. if form.get('id'):
  154. if form.get('name'):
  155. yield '%s or %s' % (form.get('id'),
  156. form.get('name'))
  157. else:
  158. yield form.get('id')
  159. elif form.get('name'):
  160. yield form.get('name')
  161. else:
  162. yield '(unnamed form %s)' % index
  163. ############################################################
  164. ## Error filling
  165. ############################################################
  166. class DefaultErrorCreator(object):
  167. insert_before = True
  168. block_inside = True
  169. error_container_tag = 'div'
  170. error_message_class = 'error-message'
  171. error_block_class = 'error-block'
  172. default_message = "Invalid"
  173. def __init__(self, **kw):
  174. for name, value in kw.items():
  175. if not hasattr(self, name):
  176. raise TypeError(
  177. "Unexpected keyword argument: %s" % name)
  178. setattr(self, name, value)
  179. def __call__(self, el, is_block, message):
  180. error_el = el.makeelement(self.error_container_tag)
  181. if self.error_message_class:
  182. error_el.set('class', self.error_message_class)
  183. if is_block and self.error_block_class:
  184. error_el.set('class', error_el.get('class', '')+' '+self.error_block_class)
  185. if message is None or message == '':
  186. message = self.default_message
  187. if isinstance(message, ElementBase):
  188. error_el.append(message)
  189. else:
  190. assert isinstance(message, basestring), (
  191. "Bad message; should be a string or element: %r" % message)
  192. error_el.text = message or self.default_message
  193. if is_block and self.block_inside:
  194. if self.insert_before:
  195. error_el.tail = el.text
  196. el.text = None
  197. el.insert(0, error_el)
  198. else:
  199. el.append(error_el)
  200. else:
  201. parent = el.getparent()
  202. pos = parent.index(el)
  203. if self.insert_before:
  204. parent.insert(pos, error_el)
  205. else:
  206. error_el.tail = el.tail
  207. el.tail = None
  208. parent.insert(pos+1, error_el)
  209. default_error_creator = DefaultErrorCreator()
  210. def insert_errors(
  211. el,
  212. errors,
  213. form_id=None,
  214. form_index=None,
  215. error_class="error",
  216. error_creator=default_error_creator,
  217. ):
  218. el = _find_form(el, form_id=form_id, form_index=form_index)
  219. for name, error in errors.items():
  220. if error is None:
  221. continue
  222. for error_el, message in _find_elements_for_name(el, name, error):
  223. assert isinstance(message, (basestring, type(None), ElementBase)), (
  224. "Bad message: %r" % message)
  225. _insert_error(error_el, message, error_class, error_creator)
  226. def insert_errors_html(html, values, **kw):
  227. result_type = type(html)
  228. if isinstance(html, basestring):
  229. doc = fromstring(html)
  230. else:
  231. doc = copy.deepcopy(html)
  232. insert_errors(doc, values, **kw)
  233. return _transform_result(result_type, doc)
  234. def _insert_error(el, error, error_class, error_creator):
  235. if _nons(el.tag) in defs.empty_tags or _nons(el.tag) == 'textarea':
  236. is_block = False
  237. else:
  238. is_block = True
  239. if _nons(el.tag) != 'form' and error_class:
  240. _add_class(el, error_class)
  241. if el.get('id'):
  242. labels = _label_for_xpath(el, for_id=el.get('id'))
  243. if labels:
  244. for label in labels:
  245. _add_class(label, error_class)
  246. error_creator(el, is_block, error)
  247. def _add_class(el, class_name):
  248. if el.get('class'):
  249. el.set('class', el.get('class')+' '+class_name)
  250. else:
  251. el.set('class', class_name)
  252. def _find_elements_for_name(form, name, error):
  253. if name is None:
  254. # An error for the entire form
  255. yield form, error
  256. return
  257. if name.startswith('#'):
  258. # By id
  259. el = form.get_element_by_id(name[1:])
  260. if el is not None:
  261. yield el, error
  262. return
  263. els = _name_xpath(form, name=name)
  264. if not els:
  265. # FIXME: should this raise an exception?
  266. return
  267. if not isinstance(error, (list, tuple)):
  268. yield els[0], error
  269. return
  270. # FIXME: if error is longer than els, should it raise an error?
  271. for el, err in zip(els, error):
  272. if err is None:
  273. continue
  274. yield el, err