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):

Roundup Issue Tracker: http://roundup-tracker.org/