Code

Adding ZTUtils to the dist
authorrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 30 Aug 2002 08:25:34 +0000 (08:25 +0000)
committerrichard <richard@57a73879-2fb5-44c3-a270-3262357dd7e2>
Fri, 30 Aug 2002 08:25:34 +0000 (08:25 +0000)
git-svn-id: http://svn.roundup-tracker.org/svnroot/roundup/trunk@1000 57a73879-2fb5-44c3-a270-3262357dd7e2

ZTUtils/Batch.py [new file with mode: 0644]
ZTUtils/CHANGES.txt [new file with mode: 0644]
ZTUtils/HISTORY.txt [new file with mode: 0644]
ZTUtils/Iterator.py [new file with mode: 0644]
ZTUtils/SimpleTree.py [new file with mode: 0644]
ZTUtils/Tree.py [new file with mode: 0644]
ZTUtils/Zope.py [new file with mode: 0644]
ZTUtils/__init__.py [new file with mode: 0644]

diff --git a/ZTUtils/Batch.py b/ZTUtils/Batch.py
new file mode 100644 (file)
index 0000000..88494f8
--- /dev/null
@@ -0,0 +1,121 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+# 
+##############################################################################
+__doc__='''Batch class, for iterating over a sequence in batches
+
+$Id: Batch.py,v 1.1 2002-08-30 08:25:33 richard Exp $'''
+__version__='$Revision: 1.1 $'[11:-2]
+
+from ExtensionClass import Base
+
+class LazyPrevBatch(Base):
+    def __of__(self, parent):
+        return Batch(parent._sequence, parent._size,
+                     parent.first - parent._size + parent.overlap, 0,
+                     parent.orphan, parent.overlap)
+
+class LazyNextBatch(Base):
+    def __of__(self, parent):
+        try: parent._sequence[parent.end]
+        except IndexError: return None
+        return Batch(parent._sequence, parent._size,
+                     parent.end - parent.overlap, 0,
+                     parent.orphan, parent.overlap)
+
+class LazySequenceLength(Base):
+    def __of__(self, parent):
+        parent.sequence_length = l = len(parent._sequence)
+        return l
+
+class Batch(Base):
+    """Create a sequence batch"""
+    __allow_access_to_unprotected_subobjects__ = 1
+
+    previous = LazyPrevBatch()
+    next = LazyNextBatch()
+    sequence_length = LazySequenceLength()
+
+    def __init__(self, sequence, size, start=0, end=0,
+                 orphan=0, overlap=0):
+        '''Encapsulate "sequence" in batches of "size".
+
+        Arguments: "start" and "end" are 0-based indexes into the
+        sequence.  If the next batch would contain no more than
+        "orphan" elements, it is combined with the current batch.
+        "overlap" is the number of elements shared by adjacent
+        batches.  If "size" is not specified, it is computed from
+        "start" and "end".  Failing that, it is 7.
+
+        Attributes: Note that the "start" attribute, unlike the
+        argument, is a 1-based index (I know, lame).  "first" is the
+        0-based index.  "length" is the actual number of elements in
+        the batch.
+
+        "sequence_length" is the length of the original, unbatched, sequence
+        '''
+
+        start = start + 1
+
+        start,end,sz = opt(start,end,size,orphan,sequence)
+
+        self._sequence = sequence
+        self.size = sz
+        self._size = size
+        self.start = start
+        self.end = end
+        self.orphan = orphan
+        self.overlap = overlap
+        self.first = max(start - 1, 0)
+        self.length = self.end - self.first
+        if self.first == 0:
+            self.previous = None
+
+
+    def __getitem__(self, index):
+        if index < 0:
+            if index + self.end < self.first: raise IndexError, index
+            return self._sequence[index + self.end]
+        
+        if index >= self.length: raise IndexError, index
+        return self._sequence[index+self.first]
+
+    def __len__(self):
+        return self.length
+
+def opt(start,end,size,orphan,sequence):
+    if size < 1:
+        if start > 0 and end > 0 and end >= start:
+            size=end+1-start
+        else: size=7
+
+    if start > 0:
+
+        try: sequence[start-1]
+        except IndexError: start=len(sequence)
+
+        if end > 0:
+            if end < start: end=start
+        else:
+            end=start+size-1
+            try: sequence[end+orphan-1]
+            except IndexError: end=len(sequence)
+    elif end > 0:
+        try: sequence[end-1]
+        except IndexError: end=len(sequence)
+        start=end+1-size
+        if start - 1 < orphan: start=1
+    else:
+        start=1
+        end=start+size-1
+        try: sequence[end+orphan-1]
+        except IndexError: end=len(sequence)
+    return start,end,size
diff --git a/ZTUtils/CHANGES.txt b/ZTUtils/CHANGES.txt
new file mode 100644 (file)
index 0000000..e2ad284
--- /dev/null
@@ -0,0 +1,28 @@
+ZTUtils changes
+
+  This file contains change information for the current release. 
+  Change information for previous versions can be found in the
+  file HISTORY.txt.
+
+    Version 1.5
+
+      Features Added
+
+        - Added 'sequence_length' attribute to batches.      
+
+        - Under Python 2.2, Iterator both accepts and produces Python
+          iterator objects.
+
+        - first() and last() methods allow you to tell whether the
+          current element is different from the next or previous
+          element.  This is most useful when the sequence is sorted.
+
+      Bugs Fixed
+
+        - Handle both string and class Unauthorized exceptions.
+
+        - Batch construction masked sequence errors, such as
+          Unauthorized.
+
+        - Orphan defaulted to different values in different places.
+
diff --git a/ZTUtils/HISTORY.txt b/ZTUtils/HISTORY.txt
new file mode 100644 (file)
index 0000000..ee37155
--- /dev/null
@@ -0,0 +1,57 @@
+ZTUtils history
+
+  This file contains change information for previous versions of
+  ZTUtils. Change information for the current release can be found
+  in the file CHANGES.txt.
+
+
+    Version 1.1.3
+
+      Brown-bag bugfix release.
+
+    Version 1.1.2
+
+      Bugs Fixed
+
+        - Orphans defaulted to 3, which was confusing and out of sync
+          with DTML-In.
+
+        - Orphan batches were broken.
+
+    Version 1.1.1
+
+      Bugs Fixed
+
+        - Python 1.5.2-incompatible changes crept in.
+
+    Version 1.1.0
+
+      Features Added
+
+        - TreeMakers have a setChildAccess() method that you can use
+          to control tree construction.  Child nodes can be accessed
+          through either an attribute name or callback function.
+          Children fetched by attribute name can be filtered through a
+          callback function.
+
+        - A new LazyFilter class allows you to filter a sequence using
+          Zope security and an optional filter callback function.  The
+          security and filter tests are lazy, meaning they are
+          performed as late as possible.  
+
+          The optional 'skip' argument determines the reaction when
+          access to a sequence element is refused by the Zope security
+          policy.  The default (None) is to raise the 'Unauthorized'
+          exception.  If a string is passed, such elements are
+          skipped.  If the string is non-empty, it is treated as a
+          permission name, and the element is skipped if the user
+          doesn't have that permission on the element.
+
+        - The Zope versions of TreeMaker, SimpleTreeMaker, and Batch
+          now use LazyFilter. The TreeMakers have a setSkip() method
+          that can be used to set the 'skip' value. Batch has an
+          optional 'skip_unauthorized' argument that is passed to
+          LazyFilter as 'skip'.
+
+        - Utility functions make_query(), url_query(), and
+          make_hidden_input() have been added.
diff --git a/ZTUtils/Iterator.py b/ZTUtils/Iterator.py
new file mode 100644 (file)
index 0000000..cf3d51c
--- /dev/null
@@ -0,0 +1,198 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+# 
+##############################################################################
+__doc__='''Iterator class
+
+Unlike the builtin iterators of Python 2.2+, these classes are
+designed to maintain information about the state of an iteration.
+The Iterator() function accepts either a sequence or a Python
+iterator.  The next() method fetches the next item, and returns
+true if it succeeds.
+
+$Id: Iterator.py,v 1.1 2002-08-30 08:25:34 richard Exp $'''
+__version__='$Revision: 1.1 $'[11:-2]
+
+import string
+
+class Iterator:
+    '''Simple Iterator class'''
+
+    __allow_access_to_unprotected_subobjects__ = 1
+
+    nextIndex = 0
+    def __init__(self, seq):
+        self.seq = seq
+        for inner in seqInner, iterInner:
+            if inner._supports(seq):
+                self._inner = inner
+                self._prep_next = inner.prep_next
+                return
+        raise TypeError, "Iterator does not support %s" % `seq`
+
+    def __getattr__(self, name):
+        try:
+            inner = getattr(self._inner, 'it_' + name)
+        except AttributeError:
+            raise AttributeError, name
+        return inner(self)
+
+    def next(self):
+        if not (hasattr(self, '_next') or self._prep_next(self)):
+            return 0
+        self.index = i = self.nextIndex
+        self.nextIndex = i+1
+        self._advance(self)
+        return 1
+
+    def _advance(self, it):
+        self.item = self._next
+        del self._next
+        del self.end
+        self._advance = self._inner.advance
+        self.start = 1
+            
+    def number(self): return self.nextIndex
+
+    def even(self): return not self.index % 2
+
+    def odd(self): return self.index % 2
+
+    def letter(self, base=ord('a'), radix=26):
+        index = self.index
+        s = ''
+        while 1:
+            index, off = divmod(index, radix)
+            s = chr(base + off) + s
+            if not index: return s
+
+    def Letter(self):
+        return self.letter(base=ord('A'))
+
+    def Roman(self, rnvalues=(
+                    (1000,'M'),(900,'CM'),(500,'D'),(400,'CD'),
+                    (100,'C'),(90,'XC'),(50,'L'),(40,'XL'),
+                    (10,'X'),(9,'IX'),(5,'V'),(4,'IV'),(1,'I')) ):
+        n = self.index + 1
+        s = ''
+        for v, r in rnvalues:
+            rct, n = divmod(n, v)
+            s = s + r * rct
+        return s
+
+    def roman(self, lower=string.lower):
+        return lower(self.Roman())
+
+    def first(self, name=None):
+        if self.start: return 1
+        return not 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)
+
+    def same_part(self, name, ob1, ob2):
+        if name is None:
+            return ob1 == ob2
+        no = []
+        return getattr(ob1, name, no) == getattr(ob2, name, no) is not no
+
+    def __iter__(self):
+        return IterIter(self)
+
+class InnerBase:
+    '''Base Inner class for Iterators'''
+    # Prep sets up ._next and .end
+    def prep_next(self, it):
+        it.next = self.no_next
+        it.end = 1
+        return 0
+
+    # Advance knocks them down
+    def advance(self, it):
+        it._last = it.item
+        it.item = it._next
+        del it._next
+        del it.end
+        it.start = 0
+            
+    def no_next(self, it):
+        return 0
+
+    def it_end(self, it):
+        if hasattr(it, '_next'):
+            return 0
+        return not self.prep_next(it)
+
+class SeqInner(InnerBase):
+    '''Inner class for sequence Iterators'''
+
+    def _supports(self, ob):
+        try: ob[0]
+        except TypeError: return 0
+        except: pass
+        return 1
+
+    def prep_next(self, it):
+        i = it.nextIndex
+        try:
+            it._next = it.seq[i]
+        except IndexError:
+            it._prep_next = self.no_next
+            it.end = 1
+            return 0
+        it.end = 0
+        return 1
+
+    def it_length(self, it):
+        it.length = l = len(it.seq)
+        return l
+
+try:
+    StopIteration=StopIteration
+except NameError:
+    StopIteration="StopIteration"
+
+class IterInner(InnerBase):
+    '''Iterator inner class for Python iterators'''
+
+    def _supports(self, ob):
+        try:
+            if hasattr(ob, 'next') and (ob is iter(ob)):
+                return 1
+        except:
+            return 0
+
+    def prep_next(self, it):
+        try:
+            it._next = it.seq.next()
+        except StopIteration:
+            it._prep_next = self.no_next
+            it.end = 1
+            return 0
+        it.end = 0
+        return 1
+
+class IterIter:
+    def __init__(self, it):
+        self.it = it
+        self.skip = it.nextIndex > 0 and not it.end
+    def next(self):
+        it = self.it
+        if self.skip:
+            self.skip = 0
+            return it.item
+        if it.next():
+            return it.item
+        raise StopIteration
+
+seqInner = SeqInner()
+iterInner = IterInner()
diff --git a/ZTUtils/SimpleTree.py b/ZTUtils/SimpleTree.py
new file mode 100644 (file)
index 0000000..ca32073
--- /dev/null
@@ -0,0 +1,57 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+# 
+##############################################################################
+__doc__='''Simple Tree classes
+
+$Id: SimpleTree.py,v 1.1 2002-08-30 08:25:34 richard Exp $'''
+__version__='$Revision: 1.1 $'[11:-2]
+
+from Tree import TreeMaker, TreeNode, b2a
+
+class SimpleTreeNode(TreeNode):
+    def branch(self):
+        if self.state == 0:
+            return {'link': None, 'img': '&nbsp;&nbsp;'}
+
+        if self.state < 0:
+            setst = 'expand'
+            exnum = self.aq_parent.expansion_number
+            img = 'pl'
+        else:
+            setst = 'collapse'
+            exnum = self.expansion_number
+            img = 'mi'
+
+        base = self.aq_acquire('baseURL')
+        obid = self.id
+        pre = self.aq_acquire('tree_pre')
+
+        return {'link': '?%s-setstate=%s,%s,%s#%s' % (pre, setst[0],
+                                                      exnum, obid, obid),
+        'img': '<img src="%s/p_/%s" alt="%s" border="0">' % (base, img, setst)}
+        
+
+class SimpleTreeMaker(TreeMaker):
+    '''Generate Simple Trees'''
+
+    def __init__(self, tree_pre="tree"):
+        self.tree_pre = tree_pre
+
+    def node(self, object):
+        node = SimpleTreeNode()
+        node.object = object
+        node.id = b2a(self.getId(object))
+        return node
+
+    def markRoot(self, node):
+        node.tree_pre = self.tree_pre
+        node.baseURL = node.object.REQUEST['BASEPATH1']
diff --git a/ZTUtils/Tree.py b/ZTUtils/Tree.py
new file mode 100644 (file)
index 0000000..ea0b562
--- /dev/null
@@ -0,0 +1,240 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+# 
+##############################################################################
+__doc__='''Tree manipulation classes
+
+$Id: Tree.py,v 1.1 2002-08-30 08:25:34 richard Exp $'''
+__version__='$Revision: 1.1 $'[11:-2]
+
+from Acquisition import Explicit
+from ComputedAttribute import ComputedAttribute
+
+class TreeNode(Explicit):
+    __allow_access_to_unprotected_subobjects__ = 1
+    state = 0 # leaf
+    height = 1
+    size = 1
+    def __init__(self):
+        self._child_list = []
+    def _add_child(self, child):
+        'Add a child which already has all of its children.'
+        self._child_list.append(child)
+        self.height = max(self.height, child.height + 1)
+        self.size = self.size + child.size
+    def flat(self):
+        'Return a flattened preorder list of tree nodes'
+        items = []
+        self.walk(items.append)
+        return items
+    def walk(self, f, data=None):
+        'Preorder walk this tree, passing each node to a function'
+        if data is None:
+            f(self)
+        else:
+            f(self, data)
+        for child in self._child_list:
+            child.__of__(self).walk(f, data)
+    def _depth(self):
+        return self.aq_parent.depth + 1
+    depth = ComputedAttribute(_depth, 1)
+    def __getitem__(self, index):
+        return self._child_list[index].__of__(self)
+    def __len__(self):
+        return len(self._child_list)
+
+_marker = [] 
+        
+class TreeMaker:
+    '''Class for mapping a hierachy of objects into a tree of nodes.'''
+
+    __allow_access_to_unprotected_subobjects__ = 1
+
+    _id = 'tpId'
+    _values = 'tpValues'
+    _assume_children = 0
+    _values_filter = None
+    _values_function = None
+    _expand_root = 1
+
+    def setChildAccess(self, attrname=_marker, filter=_marker,
+                       function=_marker):
+        '''Set the criteria for fetching child nodes.
+
+        Child nodes can be accessed through either an attribute name
+        or callback function.  Children fetched by attribute name can
+        be filtered through a callback function.
+        '''
+        if function is _marker:
+            self._values_function = None
+            if attrname is not _marker:
+                self._values = str(attrname)
+            if filter is not _marker:
+                self._values_filter = filter
+        else:
+            self._values_function = function
+    
+    def tree(self, root, expanded=None, subtree=0):
+        '''Create a tree from root, with specified nodes expanded.
+
+        "expanded" must be false, true, or a mapping.
+        Each key of the mapping is the id of a top-level expanded
+        node, and each value is the "expanded" value for the
+        children of that node.
+        '''
+        node = self.node(root)
+        child_exp = expanded
+        if not simple_type(expanded):
+            # Assume a mapping
+            expanded = expanded.has_key(node.id)
+            child_exp = child_exp.get(node.id)
+        if expanded or (not subtree and self._expand_root):
+            children = self.getChildren(root)
+            if children:
+                node.state = 1 # expanded
+                for child in children:
+                    node._add_child(self.tree(child, child_exp, 1))
+        elif self.hasChildren(root):
+            node.state = -1 # collapsed
+        if not subtree:
+            node.depth = 0
+            if hasattr(self, 'markRoot'):
+                self.markRoot(node)
+        return node
+
+    def node(self, object):
+        node = TreeNode()
+        node.object = object
+        node.id = b2a(self.getId(object))
+        return node
+    
+    def getId(self, object):
+        id_attr = self._id
+        if hasattr(object, id_attr):
+            obid = getattr(object, id_attr)
+            if not simple_type(obid): obid = obid()
+            return obid
+        if hasattr(object, '_p_oid'): return str(object._p_oid)
+        return id(object)
+
+    def hasChildren(self, object):
+        if self._assume_children:
+            return 1
+        return self.getChildren(object)
+
+    def getChildren(self, object):
+        if self._values_function is not None:
+            return self._values_function(object)
+        if self._values_filter and hasattr(object, 'aq_acquire'):
+            return object.aq_acquire(self._values, aqcallback,
+                                     self._values_filter)()
+        return getattr(object, self._values)()
+
+def simple_type(ob,
+                is_simple={type(''):1, type(0):1, type(0.0):1,
+                           type(0L):1, type(None):1 }.has_key):
+    return is_simple(type(ob))
+
+def aqcallback(self, inst, parent, name, value, filter):
+    return filter(self, inst, parent, name, value)
+
+from binascii import b2a_base64, a2b_base64
+import string
+from string import split, join, translate
+
+a2u_map = string.maketrans('+/=', '-._')
+u2a_map = string.maketrans('-._', '+/=')
+
+def b2a(s):
+    '''Encode a value as a cookie- and url-safe string.
+
+    Encoded string use only alpahnumeric characters, and "._-".
+    '''
+    s = str(s)
+    if len(s) <= 57:
+        return translate(b2a_base64(s)[:-1], a2u_map)
+    frags = []
+    for i in range(0, len(s), 57):
+        frags.append(b2a_base64(s[i:i + 57])[:-1])
+    return translate(join(frags, ''), a2u_map)
+
+def a2b(s):
+    '''Decode a b2a-encoded string.'''
+    s = translate(s, u2a_map)
+    if len(s) <= 76:
+        return a2b_base64(s)
+    frags = []
+    for i in range(0, len(s), 76):
+        frags.append(a2b_base64(s[i:i + 76]))
+    return join(frags, '')
+
+def encodeExpansion(nodes):
+    '''Encode the expanded node ids of a tree into a string.
+
+    Accepts a list of nodes, such as that produced by root.flat().
+    Marks each expanded node with an expansion_number attribute.
+    Since node ids are encoded, the resulting string is safe for
+    use in cookies and URLs.
+    '''
+    steps = []
+    last_depth = -1
+    n = 0
+    for node in nodes:
+        if node.state <=0: continue
+        dd = last_depth - node.depth + 1
+        last_depth = node.depth
+        if dd > 0:
+            steps.append('.' * dd)
+        steps.append(node.id)
+        node.expansion_number = n
+        n = n + 1
+    return join(steps, ':')
+        
+def decodeExpansion(s, nth=None):
+    '''Decode an expanded node map from a string.
+
+    If nth is an integer, also return the (map, key) pair for the nth entry.
+    '''
+    map = m = {}
+    mstack = []
+    pop = 0
+    nth_pair = None
+    if nth is not None:
+        nth_pair = (None, None)
+    for step in split(s, ':'):
+        if step[:1] == '.':
+            pop = len(step) - 1
+            continue
+        if pop < 0:
+            mstack.append(m)
+            m[obid] = {}
+            m = m[obid]
+        elif map:
+            m[obid] = None
+        if len(step) == 0:
+            return map
+        obid = step
+        if pop > 0:
+            m = mstack[-pop]
+            del mstack[-pop:]
+        pop = -1
+        if nth == 0:
+            nth_pair = (m, obid)
+            nth = None
+        elif nth is not None:
+            nth = nth - 1
+    m[obid] = None
+    if nth == 0:
+        return map, (m, obid)
+    if nth_pair is not None:
+        return map, nth_pair
+    return map
+
diff --git a/ZTUtils/Zope.py b/ZTUtils/Zope.py
new file mode 100644 (file)
index 0000000..1f84078
--- /dev/null
@@ -0,0 +1,302 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+# 
+##############################################################################
+__doc__='''Zope-specific versions of ZTUTils classes
+
+$Id: Zope.py,v 1.1 2002-08-30 08:25:34 richard Exp $'''
+__version__='$Revision: 1.1 $'[11:-2]
+
+import sys, cgi, urllib, cgi
+from Tree import encodeExpansion, decodeExpansion, TreeMaker
+from SimpleTree import SimpleTreeMaker
+from Batch import Batch
+from Products.ZCatalog.Lazy import Lazy
+from AccessControl import getSecurityManager
+from string import split, join
+from types import StringType, ListType, IntType, FloatType
+from DateTime import DateTime
+
+try:
+    from AccessControl.ZopeGuards import guarded_getitem
+except ImportError:
+    Unauthorized = 'Unauthorized'
+    def guarded_getitem(object, index):
+        v = object[index]
+        if getSecurityManager().validate(object, object, index, v):
+            return v
+        raise Unauthorized, 'unauthorized access to element %s' % `i`
+else:
+    from AccessControl import Unauthorized
+
+class LazyFilter(Lazy):
+    # A LazyFilter that checks with the security policy
+
+    def __init__(self, seq, test=None, skip=None):
+        self._seq=seq
+        self._data=[]
+        self._eindex=-1
+        self._test=test
+        if not (skip is None or str(skip) == skip): 
+            raise TypeError, 'Skip must be None or a string'
+        self._skip = skip 
+
+    def __getitem__(self,index):
+        data=self._data
+        try: s=self._seq
+        except AttributeError: return data[index]
+
+        i=index
+        if i < 0: i=len(self)+i
+        if i < 0: raise IndexError, index
+
+        ind=len(data)
+        if i < ind: return data[i]
+        ind=ind-1
+
+        test=self._test
+        e=self._eindex
+        skip = self._skip
+        while i > ind:
+            e = e + 1
+            try:
+                try: v = guarded_getitem(s, e)
+                except Unauthorized, vv:
+                    if skip is None:
+                        self._eindex = e
+                        msg = '(item %s): %s' % (index, vv)
+                        raise Unauthorized, msg, sys.exc_info()[2]
+                    skip_this = 1
+                else:
+                    skip_this = 0
+            except IndexError:
+                del self._test
+                del self._seq
+                del self._eindex
+                raise IndexError, index
+            if skip_this: continue
+            if skip and not getSecurityManager().checkPermission(skip, v):
+                continue
+            if test is None or test(v):
+                data.append(v)
+                ind=ind+1
+        self._eindex=e
+        return data[i]
+
+class TreeSkipMixin:
+    '''Mixin class to make trees test security, and allow
+    skipping of unauthorized objects. '''
+    skip = None
+    def setSkip(self, skip):
+        self.skip = skip
+        return self
+    def getChildren(self, object):
+        return LazyFilter(self._getChildren(object), skip=self.skip)
+
+class TreeMaker(TreeSkipMixin, TreeMaker):
+    _getChildren = TreeMaker.getChildren
+
+class SimpleTreeMaker(TreeSkipMixin, SimpleTreeMaker):
+    _getChildren = SimpleTreeMaker.getChildren
+    def cookieTree(self, root_object, default_state=None):
+        '''Make a tree with state stored in a cookie.'''
+        tree_pre = self.tree_pre
+        state_name = '%s-state' % tree_pre
+        set_name = '%s-setstate' % tree_pre
+
+        req = root_object.REQUEST
+        state = req.get(state_name)
+        if state:
+            setst = req.form.get(set_name)
+            if setst:
+                st, pn, expid = split(setst, ',')
+                state, (m, obid) = decodeExpansion(state, int(pn))
+                if m is None:
+                    pass
+                elif st == 'e':
+                    if m[obid] is None:
+                        m[obid] = {expid: None}
+                    else:
+                        m[obid][expid] = None
+                elif st == 'c' and m is not state and obid==expid:
+                    del m[obid]
+            else:
+                state = decodeExpansion(state)
+        else:
+            state = default_state
+        tree = self.tree(root_object, state)
+        rows = tree.flat()
+        req.RESPONSE.setCookie(state_name, encodeExpansion(rows))
+        return tree, rows
+
+# Make the Batch class test security, and let it skip unauthorized.
+_Batch = Batch
+class Batch(Batch):
+    def __init__(self, sequence, size, start=0, end=0,
+                 orphan=0, overlap=0, skip_unauthorized=None):
+        sequence = LazyFilter(sequence, skip=skip_unauthorized)
+        _Batch.__init__(self, sequence, size, start, end,
+                        orphan, overlap)
+
+# These functions are meant to be used together in templates that use
+# trees or batches.  For example, given a batch with a 'bstart' query
+# argument, you would use "url_query(request, omit='bstart')" to get
+# the base for the batching links, then append 
+# "make_query(bstart=batch.previous.first)" to one and
+# "make_query(bstart=batch.end)" to the other.
+
+def make_query(*args, **kwargs):
+    '''Construct a URL query string, with marshalling markup.
+
+    If there are positional arguments, they must be dictionaries.
+    They are combined with the dictionary of keyword arguments to form
+    a dictionary of query names and values.
+
+    Query names (the keys) must be strings.  Values may be strings,
+    integers, floats, or DateTimes, and they may also be lists or
+    namespaces containing these types.  Names and string values
+    should not be URL-quoted.  All arguments are marshalled with
+    complex_marshal().
+    '''
+
+    d = {}
+    for arg in args:
+        d.update(arg)
+    d.update(kwargs)
+
+    uq = urllib.quote
+    qlist = complex_marshal(d.items())
+    for i in range(len(qlist)):
+        k, m, v = qlist[i]
+        qlist[i] = '%s%s=%s' % (uq(k), m, uq(str(v)))
+
+    return join(qlist, '&')
+                
+def make_hidden_input(*args, **kwargs):
+    '''Construct a set of hidden input elements, with marshalling markup.
+
+    If there are positional arguments, they must be dictionaries.
+    They are combined with the dictionary of keyword arguments to form
+    a dictionary of query names and values.
+
+    Query names (the keys) must be strings.  Values may be strings,
+    integers, floats, or DateTimes, and they may also be lists or
+    namespaces containing these types.  All arguments are marshalled with
+    complex_marshal().
+    '''
+
+    d = {}
+    for arg in args:
+        d.update(arg)
+    d.update(kwargs)
+
+    hq = cgi.escape
+    qlist = complex_marshal(d.items())
+    for i in range(len(qlist)):
+        k, m, v = qlist[i]
+        qlist[i] = ('<input type="hidden" name="%s%s" value="%s">'
+                    % (hq(k), m, hq(str(v))))
+
+    return join(qlist, '\n')
+                
+def complex_marshal(pairs):
+    '''Add request marshalling information to a list of name-value pairs.
+
+    Names must be strings.  Values may be strings,
+    integers, floats, or DateTimes, and they may also be lists or
+    namespaces containing these types.
+
+    The list is edited in place so that each (name, value) pair
+    becomes a (name, marshal, value) triple.  The middle value is the
+    request marshalling string.  Integer, float, and DateTime values
+    will have ":int", ":float", or ":date" as their marshal string.
+    Lists will be flattened, and the elements given ":list" in
+    addition to their simple marshal string.  Dictionaries will be
+    flattened and marshalled using ":record".
+    '''
+    i = len(pairs)
+    while i > 0:
+        i = i - 1
+        k, v = pairs[i]
+        m = ''
+        sublist = None
+        if isinstance(v, StringType):
+            pass
+        elif hasattr(v, 'items'):
+            sublist = []
+            for sk, sv in v.items():
+                sm = simple_marshal(sv)
+                sublist.append(('%s.%s' % (k, sk), '%s:record' % sm,  sv))
+        elif isinstance(v, ListType):
+            sublist = []
+            for sv in v:
+                sm = simple_marshal(sv)
+                sublist.append((k, '%s:list' % sm, sv))
+        else:
+            m = simple_marshal(v)
+        if sublist is None:
+            pairs[i] = (k, m, v)
+        else:
+            pairs[i:i + 1] = sublist
+
+    return pairs
+
+def simple_marshal(v):
+    if isinstance(v, StringType):
+        return ''
+    if isinstance(v, IntType):
+        return ':int'
+    if isinstance(v, FloatType):
+        return ':float'
+    if isinstance(v, DateTime):
+        return ':date'
+    return ''
+
+def url_query(request, req_name="URL", omit=None):
+    '''Construct a URL with a query string, using the current request.
+
+    request: the request object
+    req_name: the name, such as "URL1" or "BASEPATH1", to get from request
+    omit: sequence of name of query arguments to omit.  If a name
+    contains a colon, it is treated literally.  Otherwise, it will
+    match each argument name that starts with the name and a period or colon. 
+    '''
+
+    base = request[req_name]
+    qs = request.get('QUERY_STRING', '')
+    
+    if qs and omit:
+        qsparts = split(qs, '&')
+
+        if isinstance(omit, StringType):
+            omits = {omit: None}
+        else:
+            omits = {}
+            for name in omit:
+                omits[name] = None
+        omitted = omits.has_key
+
+        unq = urllib.unquote
+        for i in range(len(qsparts)):
+            name = unq(split(qsparts[i], '=', 1)[0])
+            if omitted(name):
+                qsparts[i] = ''
+            name = split(name, ':', 1)[0]
+            if omitted(name):
+                qsparts[i] = ''
+            name = split(name, '.', 1)[0]
+            if omitted(name):
+                qsparts[i] = ''
+            
+        qs = join(filter(None, qsparts), '&')
+
+    # We alway append '?' since arguments will be appended to the URL
+    return '%s?%s' % (base, qs)
diff --git a/ZTUtils/__init__.py b/ZTUtils/__init__.py
new file mode 100644 (file)
index 0000000..133f817
--- /dev/null
@@ -0,0 +1,31 @@
+##############################################################################
+#
+# Copyright (c) 2001 Zope Corporation and Contributors. All Rights Reserved.
+# 
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+# 
+##############################################################################
+__doc__='''Package of template utility classes and functions.
+
+$Id: __init__.py,v 1.1 2002-08-30 08:25:34 richard Exp $'''
+__version__='$Revision: 1.1 $'[11:-2]
+
+from Batch import Batch
+from Iterator import Iterator
+from Tree import TreeMaker, encodeExpansion, decodeExpansion, a2b, b2a
+from SimpleTree import SimpleTreeMaker
+
+import sys
+if sys.modules.has_key('Zope'):
+    del sys
+    __allow_access_to_unprotected_subobjects__ = 1
+    __roles__ = None
+
+    from Zope import Batch, TreeMaker, SimpleTreeMaker, LazyFilter
+    from Zope import url_query, make_query, make_hidden_input
+