Merge pull request #195 from whybin/serialize
Serialize form elements
Gael Pasgrimaud authored 5 years ago
GitHub committed 5 years ago
3 | 3 | # |
4 | 4 | # Distributed under the BSD license, see LICENSE.txt |
5 | 5 | from .cssselectpatch import JQueryTranslator |
6 | from collections import OrderedDict | |
6 | 7 | from .openers import url_opener |
7 | 8 | from .text import extract_text |
8 | 9 | from copy import deepcopy |
423 | 424 | """return the xml root element |
424 | 425 | """ |
425 | 426 | if self._parent is not no_default: |
426 | return self._parent.getroottree() | |
427 | return self._parent[0].getroottree() | |
427 | 428 | return self[0].getroottree() |
428 | 429 | |
429 | 430 | @property |
1038 | 1039 | return 'on' |
1039 | 1040 | else: |
1040 | 1041 | return val |
1041 | # <input> and everything else. | |
1042 | # <input> | |
1043 | elif tag.tag == 'input': | |
1044 | val = self._copy(tag).attr('value') | |
1045 | return val.replace('\n', '') if val else '' | |
1046 | # everything else. | |
1042 | 1047 | return self._copy(tag).attr('value') or '' |
1043 | 1048 | |
1044 | 1049 | def _set_value(pq, value): |
1517 | 1522 | setattr(PyQuery, name, fn) |
1518 | 1523 | fn = Fn() |
1519 | 1524 | |
1525 | ||
1526 | ######## | |
1527 | # AJAX # | |
1528 | ######## | |
1529 | ||
1530 | @with_camel_case_alias | |
1531 | def serialize_array(self): | |
1532 | """Serialize form elements as an array of dictionaries, whose structure | |
1533 | mirrors that produced by the jQuery API. Notably, it does not handle the | |
1534 | deprecated `keygen` form element. | |
1535 | ||
1536 | >>> d = PyQuery('<form><input name="order" value="spam"></form>') | |
1537 | >>> d.serialize_array() == [{'name': 'order', 'value': 'spam'}] | |
1538 | True | |
1539 | >>> d.serializeArray() == [{'name': 'order', 'value': 'spam'}] | |
1540 | True | |
1541 | """ | |
1542 | return list(map( | |
1543 | lambda p: {'name': p[0], 'value': p[1]}, | |
1544 | self.serialize_pairs() | |
1545 | )) | |
1546 | ||
1547 | def serialize(self): | |
1548 | """Serialize form elements as a URL-encoded string. | |
1549 | ||
1550 | >>> h = ( | |
1551 | ... '<form><input name="order" value="spam">' | |
1552 | ... '<input name="order2" value="baked beans"></form>' | |
1553 | ... ) | |
1554 | >>> d = PyQuery(h) | |
1555 | >>> d.serialize() | |
1556 | 'order=spam&order2=baked%20beans' | |
1557 | """ | |
1558 | return urlencode(self.serialize_pairs()).replace('+', '%20') | |
1559 | ||
1560 | ||
1520 | 1561 | ##################################################### |
1521 | 1562 | # Additional methods that are not in the jQuery API # |
1522 | 1563 | ##################################################### |
1564 | ||
1565 | @with_camel_case_alias | |
1566 | def serialize_pairs(self): | |
1567 | """Serialize form elements as an array of 2-tuples conventional for | |
1568 | typical URL-parsing operations in Python. | |
1569 | ||
1570 | >>> d = PyQuery('<form><input name="order" value="spam"></form>') | |
1571 | >>> d.serialize_pairs() | |
1572 | [('order', 'spam')] | |
1573 | >>> d.serializePairs() | |
1574 | [('order', 'spam')] | |
1575 | """ | |
1576 | # https://github.com/jquery/jquery/blob | |
1577 | # /2d4f53416e5f74fa98e0c1d66b6f3c285a12f0ce/src/serialize.js#L14 | |
1578 | _submitter_types = ['submit', 'button', 'image', 'reset', 'file'] | |
1579 | ||
1580 | controls = self._copy([]) | |
1581 | # Expand list of form controls | |
1582 | for el in self.items(): | |
1583 | if el[0].tag == 'form': | |
1584 | form_id = el.attr('id') | |
1585 | if form_id: | |
1586 | # Include inputs outside of their form owner | |
1587 | root = self._copy(el.root.getroot()) | |
1588 | controls.extend(root( | |
1589 | '#%s :not([form]):input, [form="%s"]:input' | |
1590 | % (form_id, form_id))) | |
1591 | else: | |
1592 | controls.extend(el(':not([form]):input')) | |
1593 | elif el[0].tag == 'fieldset': | |
1594 | controls.extend(el(':input')) | |
1595 | else: | |
1596 | controls.extend(el) | |
1597 | # Filter controls | |
1598 | selector = '[name]:enabled:not(button)' # Not serializing image button | |
1599 | selector += ''.join(map( | |
1600 | lambda s: ':not([type="%s"])' % s, | |
1601 | _submitter_types)) | |
1602 | controls = controls.filter(selector) | |
1603 | ||
1604 | def _filter_out_unchecked(_, el): | |
1605 | el = controls._copy(el) | |
1606 | return not el.is_(':checkbox:not(:checked)') \ | |
1607 | and not el.is_(':radio:not(:checked)') | |
1608 | controls = controls.filter(_filter_out_unchecked) | |
1609 | ||
1610 | # jQuery serializes inputs with the datalist element as an ancestor | |
1611 | # contrary to WHATWG spec as of August 2018 | |
1612 | # | |
1613 | # xpath = 'self::*[not(ancestor::datalist)]' | |
1614 | # results = [] | |
1615 | # for tag in controls: | |
1616 | # results.extend(tag.xpath(xpath, namespaces=controls.namespaces)) | |
1617 | # controls = controls._copy(results) | |
1618 | ||
1619 | # Serialize values | |
1620 | ret = [] | |
1621 | for field in controls: | |
1622 | val = self._copy(field).val() | |
1623 | if isinstance(val, list): | |
1624 | ret.extend(map( | |
1625 | lambda v: (field.attrib['name'], v.replace('\n', '\r\n')), | |
1626 | val | |
1627 | )) | |
1628 | else: | |
1629 | ret.append((field.attrib['name'], val.replace('\n', '\r\n'))) | |
1630 | return ret | |
1631 | ||
1632 | @with_camel_case_alias | |
1633 | def serialize_dict(self): | |
1634 | """Serialize form elements as an ordered dictionary. Multiple values | |
1635 | corresponding to the same input name are concatenated into one list. | |
1636 | ||
1637 | >>> d = PyQuery('''<form> | |
1638 | ... <input name="order" value="spam"> | |
1639 | ... <input name="order" value="eggs"> | |
1640 | ... <input name="order2" value="ham"> | |
1641 | ... </form>''') | |
1642 | >>> d.serialize_dict() | |
1643 | OrderedDict([('order', ['spam', 'eggs']), ('order2', 'ham')]) | |
1644 | >>> d.serializeDict() | |
1645 | OrderedDict([('order', ['spam', 'eggs']), ('order2', 'ham')]) | |
1646 | """ | |
1647 | ret = OrderedDict() | |
1648 | for name, val in self.serialize_pairs(): | |
1649 | if name not in ret: | |
1650 | ret[name] = val | |
1651 | elif not isinstance(ret[name], list): | |
1652 | ret[name] = [ret[name], val] | |
1653 | else: | |
1654 | ret[name].append(val) | |
1655 | return ret | |
1523 | 1656 | |
1524 | 1657 | @property |
1525 | 1658 | def base_url(self): |
5 | 5 | import os |
6 | 6 | import sys |
7 | 7 | from lxml import etree |
8 | from pyquery.pyquery import PyQuery as pq | |
8 | from pyquery.pyquery import PyQuery as pq, no_default | |
9 | 9 | from webtest import http |
10 | 10 | from webtest.debugapp import debug_app |
11 | 11 | from .compat import PY3k |
160 | 160 | self.assertEqual(isinstance(doc.root, etree._ElementTree), True) |
161 | 161 | self.assertEqual(doc.encoding, 'UTF-8') |
162 | 162 | |
163 | child = doc.children().eq(0) | |
164 | self.assertNotEqual(child._parent, no_default) | |
165 | self.assertTrue(isinstance(child.root, etree._ElementTree)) | |
166 | ||
163 | 167 | def test_selector_from_doc(self): |
164 | 168 | doc = etree.fromstring(self.html) |
165 | 169 | assert len(self.klass(doc)) == 1 |
381 | 385 | <input name="eggs" value="Eggs"> |
382 | 386 | <input type="checkbox" value="Bacon"> |
383 | 387 | <input type="radio" value="Ham"> |
388 | ''' | |
389 | ||
390 | html2_newline = ''' | |
391 | <input id="newline-text" type="text" name="order" value="S | |
392 | pam"> | |
393 | <input id="newline-radio" type="radio" name="order" value="S | |
394 | pam"> | |
384 | 395 | ''' |
385 | 396 | |
386 | 397 | html3 = ''' |
469 | 480 | self.assertEqual(d('input[name="eggs"]').val(), '43') |
470 | 481 | self.assertEqual(d('input:checkbox').val(), '44') |
471 | 482 | self.assertEqual(d('input:radio').val(), '45') |
483 | ||
484 | def test_val_for_inputs_with_newline(self): | |
485 | d = pq(self.html2_newline) | |
486 | self.assertEqual(d('#newline-text').val(), 'Spam') | |
487 | self.assertEqual(d('#newline-radio').val(), 'S\npam') | |
472 | 488 | |
473 | 489 | def test_val_for_textarea(self): |
474 | 490 | d = pq(self.html3) |
572 | 588 | self.assertIn(replacement, new_html) |
573 | 589 | |
574 | 590 | |
591 | class TestAjax(TestCase): | |
592 | ||
593 | html = ''' | |
594 | <div id="div"> | |
595 | <input form="dispersed" name="order" value="spam"> | |
596 | </div> | |
597 | <form id="dispersed"> | |
598 | <div><input name="order" value="eggs"></div> | |
599 | <input form="dispersed" name="order" value="ham"> | |
600 | <input form="other-form" name="order" value="nothing"> | |
601 | <input form="" name="order" value="nothing"> | |
602 | </form> | |
603 | <form id="other-form"> | |
604 | <input form="dispersed" name="order" value="tomato"> | |
605 | </form> | |
606 | <form class="no-id"> | |
607 | <input form="dispersed" name="order" value="baked beans"> | |
608 | <input name="spam" value="Spam"> | |
609 | </form> | |
610 | ''' | |
611 | ||
612 | html2 = ''' | |
613 | <form id="first"> | |
614 | <input name="order" value="spam"> | |
615 | <fieldset> | |
616 | <input name="fieldset" value="eggs"> | |
617 | <input id="input" name="fieldset" value="ham"> | |
618 | </fieldset> | |
619 | </form> | |
620 | <form id="datalist"> | |
621 | <datalist><div><input name="datalist" value="eggs"></div></datalist> | |
622 | <input type="checkbox" name="checkbox" checked> | |
623 | <input type="radio" name="radio" checked> | |
624 | </form> | |
625 | ''' | |
626 | ||
627 | html3 = ''' | |
628 | <form> | |
629 | <input name="order" value="spam"> | |
630 | <input id="noname" value="sausage"> | |
631 | <fieldset disabled> | |
632 | <input name="order" value="sausage"> | |
633 | </fieldset> | |
634 | <input name="disabled" value="ham" disabled> | |
635 | <input type="submit" name="submit" value="Submit"> | |
636 | <input type="button" name="button" value=""> | |
637 | <input type="image" name="image" value=""> | |
638 | <input type="reset" name="reset" value="Reset"> | |
639 | <input type="file" name="file" value=""> | |
640 | <button type="submit" name="submit" value="submit"></button> | |
641 | <input type="checkbox" name="spam"> | |
642 | <input type="radio" name="eggs"> | |
643 | </form> | |
644 | ''' | |
645 | ||
646 | html4 = ''' | |
647 | <form> | |
648 | <input name="spam" value="Spam/ | |
649 | spam"> | |
650 | <select name="order" multiple> | |
651 | <option value="baked | |
652 | beans" selected> | |
653 | <option value="tomato" selected> | |
654 | <option value="spam"> | |
655 | </select> | |
656 | <textarea name="multiline">multiple | |
657 | lines | |
658 | of text</textarea> | |
659 | </form> | |
660 | ''' | |
661 | ||
662 | def test_serialize_pairs_form_id(self): | |
663 | d = pq(self.html) | |
664 | self.assertEqual(d('#div').serialize_pairs(), []) | |
665 | self.assertEqual(d('#dispersed').serialize_pairs(), [ | |
666 | ('order', 'spam'), ('order', 'eggs'), ('order', 'ham'), | |
667 | ('order', 'tomato'), ('order', 'baked beans'), | |
668 | ]) | |
669 | self.assertEqual(d('.no-id').serialize_pairs(), [ | |
670 | ('spam', 'Spam'), | |
671 | ]) | |
672 | ||
673 | def test_serialize_pairs_form_controls(self): | |
674 | d = pq(self.html2) | |
675 | self.assertEqual(d('fieldset').serialize_pairs(), [ | |
676 | ('fieldset', 'eggs'), ('fieldset', 'ham'), | |
677 | ]) | |
678 | self.assertEqual(d('#input, fieldset, #first').serialize_pairs(), [ | |
679 | ('order', 'spam'), ('fieldset', 'eggs'), ('fieldset', 'ham'), | |
680 | ('fieldset', 'eggs'), ('fieldset', 'ham'), ('fieldset', 'ham'), | |
681 | ]) | |
682 | self.assertEqual(d('#datalist').serialize_pairs(), [ | |
683 | ('datalist', 'eggs'), ('checkbox', 'on'), ('radio', 'on'), | |
684 | ]) | |
685 | ||
686 | def test_serialize_pairs_filter_controls(self): | |
687 | d = pq(self.html3) | |
688 | self.assertEqual(d('form').serialize_pairs(), [ | |
689 | ('order', 'spam') | |
690 | ]) | |
691 | ||
692 | def test_serialize_pairs_form_values(self): | |
693 | d = pq(self.html4) | |
694 | self.assertEqual(d('form').serialize_pairs(), [ | |
695 | ('spam', 'Spam/spam'), ('order', 'baked\r\nbeans'), | |
696 | ('order', 'tomato'), ('multiline', 'multiple\r\nlines\r\nof text'), | |
697 | ]) | |
698 | ||
699 | def test_serialize_array(self): | |
700 | d = pq(self.html4) | |
701 | self.assertEqual(d('form').serialize_array(), [ | |
702 | {'name': 'spam', 'value': 'Spam/spam'}, | |
703 | {'name': 'order', 'value': 'baked\r\nbeans'}, | |
704 | {'name': 'order', 'value': 'tomato'}, | |
705 | {'name': 'multiline', 'value': 'multiple\r\nlines\r\nof text'}, | |
706 | ]) | |
707 | ||
708 | def test_serialize(self): | |
709 | d = pq(self.html4) | |
710 | self.assertEqual( | |
711 | d('form').serialize(), | |
712 | 'spam=Spam%2Fspam&order=baked%0D%0Abeans&order=tomato&' | |
713 | 'multiline=multiple%0D%0Alines%0D%0Aof%20text' | |
714 | ) | |
715 | ||
716 | def test_serialize_dict(self): | |
717 | d = pq(self.html4) | |
718 | self.assertEqual(d('form').serialize_dict(), { | |
719 | 'spam': 'Spam/spam', | |
720 | 'order': ['baked\r\nbeans', 'tomato'], | |
721 | 'multiline': 'multiple\r\nlines\r\nof text', | |
722 | }) | |
723 | ||
724 | ||
575 | 725 | class TestMakeLinks(TestCase): |
576 | 726 | |
577 | 727 | html = ''' |