Mercurial > p > roundup > code
changeset 7866:9bbc1d951677
issue2551331 - Fix repeat first/last methods.
The first() and last() methods for a variable defined by tal:repeat
now work as documented.
There is an undocumented same_part() method for repeat. It is called
by first/last and can cause them to return true when not at an end of
the Iterator sequence. I wrote a treatise on that function and what it
does. I have no idea why it does what it does.
Added tests for roundup/cgi/ZTUtils/Iterator.py
Also fixes issue with roman() found while writing tests. lower wasn't
being called and it was printing the lower() method signature.
Doc updates in references.txt to come in a future checkin. Clarifying
the repeat methods led to finding/fixing this.
| author | John Rouillard <rouilj@ieee.org> |
|---|---|
| date | Sun, 07 Apr 2024 20:52:17 -0400 |
| parents | ee586ff074ed |
| children | 1774fdf2010a |
| files | CHANGES.txt roundup/cgi/ZTUtils/Iterator.py roundup/cgi/templating.py test/test_templating.py |
| diffstat | 4 files changed, 164 insertions(+), 4 deletions(-) [+] |
line wrap: on
line diff
--- a/CHANGES.txt Sun Apr 07 15:33:33 2024 -0400 +++ b/CHANGES.txt Sun Apr 07 20:52:17 2024 -0400 @@ -118,6 +118,7 @@ - issue2551264 - REST X-Total-Count header and @total_size count incorrect when paginated - correct values are now returned. (John Rouillard) +- issue2551331 - Fix repeat first/last methods. (John Rouillard) Features:
--- a/roundup/cgi/ZTUtils/Iterator.py Sun Apr 07 15:33:33 2024 -0400 +++ b/roundup/cgi/ZTUtils/Iterator.py Sun Apr 07 20:52:17 2024 -0400 @@ -85,17 +85,42 @@ return s def roman(self, lower=lambda x:x.lower): - return lower(self.Roman()) + return self.Roman().lower() def first(self, name=None): if self.start: return 1 - return not self.same_part(name, self._last, self.item) + return self.same_part(name, self._last, self.item) def last(self, name=None): if self.end: return 1 - return not self.same_part(name, self.item, self._next) + return self.same_part(name, self.item, self._next) def same_part(self, name, ob1, ob2): + """I have no idea what this does. + + It returns True for first()/last() when there are more items + in the sequence. Called by first() to compare the previous + item in sequence and the current item. Caled by last() to + compare the current item and next item in sequence. + + Accepts a string version of the name of an attribute as 'name'. + + If no attribute name is provided, return True if the two items: + + are equal (==) - duplicate strings/integer/same object + + else False. + + If a non-existent attribute name is provided return False. + + If the attribute is present and the named attributes compare + equal (==) return True else False. + + No idea what use case this handles. Maybe this has + something to do with batching and first/last returning True + triggers a new group? + """ + if name is None: return ob1 == ob2 no = []
--- a/roundup/cgi/templating.py Sun Apr 07 15:33:33 2024 -0400 +++ b/roundup/cgi/templating.py Sun Apr 07 20:52:17 2024 -0400 @@ -2143,7 +2143,7 @@ If not editable, just display the value via plain(). In addition to being able to set arbitrary html properties - using prop=val arguments, the thre arguments: + using prop=val arguments, the three arguments: y_label, n_label, u_label let you control the labels associated with the yes, no (and optionally unknown/empty)
--- a/test/test_templating.py Sun Apr 07 15:33:33 2024 -0400 +++ b/test/test_templating.py Sun Apr 07 20:52:17 2024 -0400 @@ -4,6 +4,7 @@ from roundup.anypy.cgi_ import FieldStorage, MiniFieldStorage from roundup.cgi.templating import * +from roundup.cgi.ZTUtils.Iterator import Iterator from .test_actions import MockNull, true from .html_norm import NormalizingHtmlParser @@ -1119,6 +1120,139 @@ self.assertEqual(p.stext(), u2s(u'A string with <a href="mailto:cmeerw@example.com">cmeerw@example.com</a> *embedded* \u00df')) +class ZUtilsTestcase(TemplatingTestCase): + + def test_Iterator(self): + """Test all the iterator functions and properties. + """ + sequence = ['one', 'two', '3', 4] + i = Iterator(sequence) + for j in [ # element, item, 1st, last, even, odd, number, + # letter, Letter, roman, Roman + (1, "one", 1, 0, True, 0, 1, 'a', 'A', 'i', 'I'), + (1, "two", 0, 0, False, 1, 2, 'b', 'B', 'ii', 'II'), + (1, "3", 0, 0, True, 0, 3, 'c', 'C', 'iii', 'III'), + (1, 4, 0, 1, False, 1, 4, 'd', 'D', 'iv', 'IV'), + # next() fails with 0 when past end of sequence + # everything else is left at end of sequence + (0, 4, 0, 1, False, 1, 4, 'd', 'D', 'iv', 'IV'), + + + ]: + element = i.next() # returns 1 if next item else 0 + print(i.item) + self.assertEqual(element, j[0]) + self.assertEqual(i.item, j[1]) + self.assertEqual(i.first(), j[2]) + self.assertEqual(i.start, j[2]) + self.assertEqual(i.last(), j[3]) + self.assertEqual(i.end, j[3]) + self.assertIs(i.even(), j[4]) + self.assertEqual(i.odd(), j[5]) + self.assertEqual(i.number(), j[6]) + self.assertEqual(i.index, j[6] - 1) + self.assertEqual(i.nextIndex, j[6]) + self.assertEqual(i.letter(), j[7]) + self.assertEqual(i.Letter(), j[8]) + self.assertEqual(i.roman(), j[9]) + self.assertEqual(i.Roman(), j[10]) + + class I: + def __init__(self, name, data): + self.name = name + self.data = data + + sequence = [I('Al', 'd'), + I('Bob', 'e'), + I('Bob', 'd'), + I('Chip', 'd') + ] + + iterator = iter(sequence) + + # Iterator is supposed take both sequence and Python iterator. + for source in [sequence, iterator]: + i = Iterator(source) + + element = i.next() # returns 1 if next item else 0 + item1 = i.item + + # note these can trigger calls by first/last to same_part(). + # It can return true for first/last even when there are more + # items in the sequence. I am just testing the current + # implementation. Woe to the person who tries to change + # Iterator.py. + + self.assertEqual(element, 1) + # i.start == 1, so it bypasses name check + self.assertEqual(i.first(name='namea'), 1) + self.assertEqual(i.first(name='name'), 1) + # i.end == 0 so it uses name check in object + self.assertEqual(i.last(name='namea'), 0) + self.assertEqual(i.last(name='name'), 0) + + element = i.next() # returns 1 if next item else 0 + item2 = i.item + self.assertEqual(element, 1) + # i.start == 0 so it uses name check + # between item1 and item2 + self.assertEqual(i.first(name='namea'), 0) + self.assertEqual(i.first(name='name'), 0) + # i.end == 0 so it uses name check in object + # between item2 and the next item item3 + self.assertEqual(i.last(name='namea'), 0) + self.assertEqual(i.last(name='name'), True) + + element = i.next() # returns 1 if next item else 0 + item3 = i.item + self.assertEqual(element, 1) + # i.start == 0 so it uses name check + self.assertEqual(i.first(name='namea'), 0) + self.assertEqual(i.first(name='name'), 1) + # i.end == 0 so it uses name check in object + # between item3 and the next item item4 + self.assertEqual(i.last(name='namea'), 0) + self.assertEqual(i.last(name='name'), 0) + + element = i.next() # returns 1 if next item else 0 + item4 = i.item + self.assertEqual(element, 1) + # i.start == 0 so it uses name check + self.assertEqual(i.first(name='namea'), 0) + self.assertEqual(i.first(name='name'), 0) + # i.end == 0 so it uses name check in object + # last two object have same name (1) + self.assertEqual(i.last(name='namea'), 1) + self.assertEqual(i.last(name='name'), 1) + + element = i.next() # returns 1 if next item else 0 + self.assertEqual(element, 0) + + # this is the underlying call for first/last + # when i.start/i.end are 0 + # use non-existing attribute name, same item + self.assertIs(i.same_part('namea', item2, item2), False) + # use correct attribute name + self.assertIs(i.same_part('name', item2, item2), True) + # use no attribute name + self.assertIs(i.same_part(None, item2, item2), True) + + # use non-existing attribute name, different item + # non-matching names + self.assertIs(i.same_part('namea', item1, item2), False) + # use correct attribute name + self.assertIs(i.same_part('name', item1, item2), False) + # use no attribute name + self.assertIs(i.same_part(None, item1, item2), False) + + # use non-existing attribute name, different item + # matching names + self.assertIs(i.same_part('namea', item2, item3), False) + # use correct attribute name + self.assertIs(i.same_part('name', item2, item3), True) + # use no attribute name + self.assertIs(i.same_part(None, item2, item3), False) + r''' class HTMLPermissions: def is_edit_ok(self):
