Exemplo n.º 1
0
class DBList(MutableSequence, Persistent):
    def __init__(self, source=None):
        self.plist = ZPersistentList()
        if source is not None:
            self.extend(source) # TODO: extend the plist instead, after
            # validating the types of the items in source
    
    def __getitem__(self, index):
        return self.plist[index]
    
    def __setitem__(self, index, value):
        if not isinstance(value, (basestring, int, long, bool, type(None), DBDict, DBList)):
            raise TypeError("Values can only be those supported by the ZODB backend.")
        self.plist[index] = value
    
    def __delitem__(self, index):
        del self.plist[index]
    
    def insert(self, index, value):
        if not isinstance(value, (basestring, int, long, bool, type(None), DBDict, DBList)):
            raise TypeError("Values can only be those supported by the ZODB backend.")
        self.plist.insert(index, value)
    
    def __len__(self):
        return len(self.plist)
    
    def __iter__(self):
        return self.plist.__iter__()
    
    def __contains__(self, value):
        return self.plist.__contains__(value)
Exemplo n.º 2
0
class GalleryAlbum(GalleryContainer):
    def __init__(self,
                 name,
                 description=None,
                 long_description=None,
                 location=None,
                 date_from=datetime.datetime.now(),
                 date_to=None,
                 parent=None):
        GalleryContainer.__init__(self, name, description, parent=parent)
        self.long_description = long_description
        self.location = location
        self.date_from = date_from
        self.date_to = date_to
        self.__pictures = PersistentList()

    @property
    def pictures(self):
        return self.__pictures

    def add(self, item):
        self[item.__name__] = item

    def insert(self, index, item):
        GalleryContainer.add(self, item)
        self.__pictures.insert(index, item)

    def __setitem__(self, name, item):
        GalleryContainer.__setitem__(self, name, item)
        self.__pictures.append(item)

    def __delitem__(self, name):
        self.__pictures.remove(self[name])
        GalleryContainer.__delitem__(self, name)
Exemplo n.º 3
0
class GalleryAlbum(GalleryContainer):
    def __init__(self, name, description=None, long_description=None,
                 location=None,
                 date_from=datetime.datetime.now(), date_to=None,
                 parent=None):
        GalleryContainer.__init__(self, name, description, parent=parent)
        self.long_description = long_description
        self.location = location
        self.date_from = date_from
        self.date_to = date_to
        self.__pictures = PersistentList()

    @property
    def pictures(self):
        return self.__pictures

    def add(self, item):
        self[item.__name__] = item

    def insert(self, index, item):
        GalleryContainer.add(self, item)
        self.__pictures.insert(index, item)

    def __setitem__(self, name, item):
        GalleryContainer.__setitem__(self, name, item)
        self.__pictures.append(item)

    def __delitem__(self, name):
        self.__pictures.remove(self[name])
        GalleryContainer.__delitem__(self, name)
class NyPortalLanguageManager(Persistent):

    implements(ILanguageAvailability)

    def __init__(self, default_langs=[('en', 'English')]):
        if not isinstance(default_langs, list):
            raise ValueError("Default languages must be a list of touples"
                             " (code, name)")
        self.portal_languages = PersistentList(default_langs)

    def getAvailableLanguages(self):
        """Return a sequence of language tags for available languages
        """
        return tuple([ x[0] for x in self.portal_languages ])

    def addAvailableLanguage(self, lang_code, lang_name=None):
        """Adds available language in portal"""
        lang_code = normalize_code(lang_code)
        if not lang_name:
            n = NyLanguages()
            lang_name = n.get_language_name(lang_code)
        if lang_code not in self.getAvailableLanguages():
            self.portal_languages.append((lang_code, lang_name))

    def delAvailableLanguage(self, lang):
        lang = normalize_code(lang)
        pos = list(self.getAvailableLanguages()).index(lang)
        if pos > -1:
            if len(self.getAvailableLanguages()) == 1:
                raise ValueError("Can not delete the only available language")
            else:
                self.portal_languages.pop(pos)

    # MORE:
    def set_default_language(self, lang):
        lang = normalize_code(lang)
        if lang not in self.getAvailableLanguages():
            raise ValueError("Language %s is not provided by portal" % lang)
        available = list(self.getAvailableLanguages())
        if len(available)==1:
            return
        pos = available.index(lang)
        new_default = self.portal_languages.pop(pos)
        self.portal_languages.insert(0, new_default)

    def get_default_language(self):
        return self.portal_languages[0][0]

    def get_language_name(self, code):
        pos = list(self.getAvailableLanguages()).index(code)
        if pos > -1:
            return self.portal_languages[pos][1]
        else:
            return "???"
Exemplo n.º 5
0
class NyPortalLanguageManager(Persistent):

    implements(ILanguageAvailability)

    def __init__(self, default_langs=[('en', 'English')]):
        if not isinstance(default_langs, list):
            raise ValueError("Default languages must be a list of touples"
                             " (code, name)")
        self.portal_languages = PersistentList(default_langs)

    def getAvailableLanguages(self):
        """Return a sequence of language tags for available languages
        """
        return tuple([x[0] for x in self.portal_languages])

    def addAvailableLanguage(self, lang_code, lang_name=None):
        """Adds available language in portal"""
        lang_code = normalize_code(lang_code)
        if not lang_name:
            n = NyLanguages()
            lang_name = n.get_language_name(lang_code)
        if lang_code not in self.getAvailableLanguages():
            self.portal_languages.append((lang_code, lang_name))

    def delAvailableLanguage(self, lang):
        lang = normalize_code(lang)
        pos = list(self.getAvailableLanguages()).index(lang)
        if pos > -1:
            if len(self.getAvailableLanguages()) == 1:
                raise ValueError("Can not delete the only available language")
            else:
                self.portal_languages.pop(pos)

    # MORE:
    def set_default_language(self, lang):
        lang = normalize_code(lang)
        if lang not in self.getAvailableLanguages():
            raise ValueError("Language %s is not provided by portal" % lang)
        available = list(self.getAvailableLanguages())
        if len(available) == 1:
            return
        pos = available.index(lang)
        new_default = self.portal_languages.pop(pos)
        self.portal_languages.insert(0, new_default)

    def get_default_language(self):
        return self.portal_languages[0][0]

    def get_language_name(self, code):
        pos = list(self.getAvailableLanguages()).index(code)
        if pos > -1:
            return self.portal_languages[pos][1]
        else:
            return "???"
 def setCheckMailCallback(self, validator, position=None):
     """Add the validator to the callback chain at the correct position.
     If no position is passed, just append it to the end of the chain.
     """
     chain = getattr(self, '_check_mail_callback_chain', None)
     if chain is None:
         chain = PersistentList()
         self._check_mail_callback_chain = chain
     if position is None:
         chain.append(validator)
     else:
         chain.insert(position, validator)
Exemplo n.º 7
0
class Ordering(Persistent):
    """ Store information about the ordering of items within a folder.
    """
    implements(IOrdering)

    def __init__(self):
        Persistent.__init__(self)
        self._items = PersistentList()

    def sync(self, entries):
        # Go do some cleanup.  Any items that are in the folder but
        # not in the ordering, put at end.  Any items that are in the
        # ordering but not in the folder, remove.

        for local_name in self._items:
            if local_name not in entries:
                # Item is in ordering but not in context, remove from
                # ordering.
                self._items.remove(local_name)

        for entry_name in entries:
            if entry_name not in self._items:
                # Item is in folder but not in ordering, append to
                # end.
                self._items.append(entry_name)

    def moveUp(self, name):
        # Move the item with __name__ == name up a position.  If at
        # the beginning, move to last position.

        position = self._items.index(name)
        del self._items[position]
        if position == 0:
            # Roll over to the end
            self._items.append(name)
        else:
            self._items.insert(position - 1, name)

    def moveDown(self, name):
        # Move the item with __name__ == name down a position.  If at
        # the end, move to the first position.

        position = self._items.index(name)
        list_length = len(self._items)
        del self._items[position]
        if position == (list_length - 1):
            # Roll over to the end
            self._items.insert(0, name)
        else:
            self._items.insert(position + 1, name)

    def add(self, name):
        # When a new item is added to a folder, put it at the end.
        if name not in self._items:
            self._items.append(name)

    def remove(self, name):
        # When an existing item is removed from folder, remove from
        # ordering.  Sure would be nice to use events to do this for
        # us.
        if name in self._items:
            self._items.remove(name)

    def items(self):
        return self._items

    def previous_name(self, current_name):
        # Given a position in the list, get the next name, or None if
        # at the end of the list

        position = self._items.index(current_name)
        if position == 0:
            # We are at the end of the list, so return None
            return None
        else:
            return self._items[position - 1]

    def next_name(self, current_name):
        # Given a position in the list, get the next name, or None if
        # at the end of the list

        position = self._items.index(current_name)
        if position == (len(self._items)-1):
            # We are at the end of the list, so return None
            return None
        else:
            return self._items[position + 1]
Exemplo n.º 8
0
    def checkTheWorld(self):
        # Test constructors
        u = PersistentList()
        u0 = PersistentList(l0)
        u1 = PersistentList(l1)
        u2 = PersistentList(l2)

        uu = PersistentList(u)
        uu0 = PersistentList(u0)
        uu1 = PersistentList(u1)
        uu2 = PersistentList(u2)

        v = PersistentList(tuple(u))

        class OtherList:
            def __init__(self, initlist):
                self.__data = initlist

            def __len__(self):
                return len(self.__data)

            def __getitem__(self, i):
                return self.__data[i]

        v0 = PersistentList(OtherList(u0))
        vv = PersistentList("this is also a sequence")

        # Test __repr__
        eq = self.assertEqual

        eq(str(u0), str(l0), "str(u0) == str(l0)")
        eq(repr(u1), repr(l1), "repr(u1) == repr(l1)")
        eq(repr(u2), repr(l2), "repr(u2) == repr(l2)")

        # Test __cmp__ and __len__

        # Py3: No cmp() or __cmp__ anymore.
        if PY2:

            def mycmp(a, b):
                r = cmp(a, b)
                if r < 0: return -1
                if r > 0: return 1
                return r

            all = [l0, l1, l2, u, u0, u1, u2, uu, uu0, uu1, uu2]
            for a in all:
                for b in all:
                    eq(mycmp(a, b), mycmp(len(a), len(b)),
                       "mycmp(a, b) == mycmp(len(a), len(b))")

        # Test __getitem__

        for i in range(len(u2)):
            eq(u2[i], i, "u2[i] == i")

        # Test __setitem__

        uu2[0] = 0
        uu2[1] = 100
        try:
            uu2[2] = 200
        except IndexError:
            pass
        else:
            self.fail("uu2[2] shouldn't be assignable")

        # Test __delitem__

        del uu2[1]
        del uu2[0]
        try:
            del uu2[0]
        except IndexError:
            pass
        else:
            self.fail("uu2[0] shouldn't be deletable")

        # Test __getslice__

        for i in range(-3, 4):
            eq(u2[:i], l2[:i], "u2[:i] == l2[:i]")
            eq(u2[i:], l2[i:], "u2[i:] == l2[i:]")
            for j in range(-3, 4):
                eq(u2[i:j], l2[i:j], "u2[i:j] == l2[i:j]")

        # Test __setslice__

        for i in range(-3, 4):
            u2[:i] = l2[:i]
            eq(u2, l2, "u2 == l2")
            u2[i:] = l2[i:]
            eq(u2, l2, "u2 == l2")
            for j in range(-3, 4):
                u2[i:j] = l2[i:j]
                eq(u2, l2, "u2 == l2")

        uu2 = u2[:]
        uu2[:0] = [-2, -1]
        eq(uu2, [-2, -1, 0, 1], "uu2 == [-2, -1, 0, 1]")
        uu2[0:] = []
        eq(uu2, [], "uu2 == []")

        # Test __contains__
        for i in u2:
            self.assertTrue(i in u2, "i in u2")
        for i in min(u2) - 1, max(u2) + 1:
            self.assertTrue(i not in u2, "i not in u2")

        # Test __delslice__

        uu2 = u2[:]
        del uu2[1:2]
        del uu2[0:1]
        eq(uu2, [], "uu2 == []")

        uu2 = u2[:]
        del uu2[1:]
        del uu2[:1]
        eq(uu2, [], "uu2 == []")

        # Test __add__, __radd__, __mul__ and __rmul__

        #self.assertTrue(u1 + [] == [] + u1 == u1, "u1 + [] == [] + u1 == u1")
        self.assertTrue(u1 + [1] == u2, "u1 + [1] == u2")
        #self.assertTrue([-1] + u1 == [-1, 0], "[-1] + u1 == [-1, 0]")
        self.assertTrue(u2 == u2 * 1 == 1 * u2, "u2 == u2*1 == 1*u2")
        self.assertTrue(u2 + u2 == u2 * 2 == 2 * u2, "u2+u2 == u2*2 == 2*u2")
        self.assertTrue(u2 + u2 + u2 == u2 * 3 == 3 * u2,
                        "u2+u2+u2 == u2*3 == 3*u2")

        # Test append

        u = u1[:]
        u.append(1)
        eq(u, u2, "u == u2")

        # Test insert

        u = u2[:]
        u.insert(0, -1)
        eq(u, [-1, 0, 1], "u == [-1, 0, 1]")

        # Test pop

        u = PersistentList([0, -1, 1])
        u.pop()
        eq(u, [0, -1], "u == [0, -1]")
        u.pop(0)
        eq(u, [-1], "u == [-1]")

        # Test remove

        u = u2[:]
        u.remove(1)
        eq(u, u1, "u == u1")

        # Test count
        u = u2 * 3
        eq(u.count(0), 3, "u.count(0) == 3")
        eq(u.count(1), 3, "u.count(1) == 3")
        eq(u.count(2), 0, "u.count(2) == 0")

        # Test index

        eq(u2.index(0), 0, "u2.index(0) == 0")
        eq(u2.index(1), 1, "u2.index(1) == 1")
        try:
            u2.index(2)
        except ValueError:
            pass
        else:
            self.fail("expected ValueError")

        # Test reverse

        u = u2[:]
        u.reverse()
        eq(u, [1, 0], "u == [1, 0]")
        u.reverse()
        eq(u, u2, "u == u2")

        # Test sort

        u = PersistentList([1, 0])
        u.sort()
        eq(u, u2, "u == u2")

        # Test extend

        u = u1[:]
        u.extend(u2)
        eq(u, u1 + u2, "u == u1 + u2")
Exemplo n.º 9
0
class PSetList(Persistent):
    """PSetList is a persistent set list object that mostly acts just like a normal Python list for PSet objects.
    These lists can be saved in the database just like any other persistent objects. It can optionally be initialized
    with another list of PSet objects and a name. Additionally, it will also have an attribute 'creation_date' and
    a unique uuid attribute 'id'. PSetLists are considered equal if they have the same 'id'.

    Except for the usual list methods like 'extend' and 'append', the PCardList is functional in style, meaning that
    calling any of the other filtering or querying methods return new PCardList objects leaving the original untouched.

    args:
        sets (PSetList, PersistentList[PSet], list[PSet], tuple[PSet]): Initial sets of the list.
        name (str): Name of the set list.
    """

    def __init__(self, sets=None, name=''):
        if isinstance(sets, PSetList):
            self._sets = PersistentList(sets.sets)
        elif isinstance(sets, (PersistentList, list, tuple)):
            self._sets = PersistentList(sets)
        elif not sets:
            self._sets = PersistentList()
        else:
            raise TypeError

        self.name = name
        self.creation_date = datetime.datetime.now()
        self.id = uuid.uuid4()

    def __getitem__(self, item):
        if isinstance(item, int):
            return self._sets.__getitem__(item)
        else:
            return PSetList(self._sets.__getitem__(item))

    def __setitem__(self, key, value):
        self._sets.__setitem__(key, value)

    def __iter__(self):
        return iter(self._sets)

    def __str__(self):
        return str(self._sets)

    def __repr__(self):
        return repr(self._sets)

    def __add__(self, other):
        if isinstance(other, PSetList):
            return PSetList(self.sets + other.sets)
        elif isinstance(other, (PersistentList, list, tuple)):
            return PSetList(self.sets + other)
        elif isinstance(other, PSet):
            new_sets = PersistentList(self.sets)
            new_sets.append(other)
            return PSetList(new_sets)
        else:
            raise TypeError

    def __radd__(self, other):
        if isinstance(other, PSetList):
            return PSetList(self.sets + other.sets)
        elif isinstance(other, (PersistentList, list, tuple)):
            return PSetList(self.sets + other)
        elif isinstance(other, PSet):
            new_sets = PersistentList(self.sets)
            new_sets.append(other)
            return PSetList(new_sets)
        else:
            raise TypeError

    def __iadd__(self, other):
        if isinstance(other, PSetList):
            return PSetList(self.sets + other.sets)
        elif isinstance(other, (PersistentList, list, tuple)):
            return PSetList(self.sets + other)
        elif isinstance(other, PSet):
            new_sets = PersistentList(self.sets)
            new_sets.append(other)
            return PSetList(new_sets)
        else:
            raise TypeError

    def __len__(self):
        return len(self.sets)

    def __contains__(self, pset):
        return self.sets.__contains__(pset)

    def __eq__(self, other):
        if isinstance(other, PSetList):
            return self.id == other.id

    def append(self, pset):
        """Appends the given set object to this list in-place.

        Args:
            pset (PSet): The set object to append.
        """
        self.sets.append(pset)

    def extend(self, psets):
        """Extends the list with a list of set objects in-place.

        Args:
            psets (PSetList, list, tuple, PersistentList): A PSetList, PersistentList, list or a tuple of
                set objects to extend this list with.
        """
        if isinstance(psets, PSetList):
            self.sets.extend(psets.sets)
        elif isinstance(psets, (PersistentList, list, tuple)):
            self.sets.extend(psets)
        else:
            raise TypeError

    def insert(self, index, pset):
        """Inserts a set object to a given index in this list in-place.

        Args:
            pset (PSet): The set object to be inserted in the given index in this list.
            index (int): The index to insert the given set object.
        """
        self._sets.insert(index, pset)

    def index(self, pset):
        """Returns the index where the given set object is located in this list.

        Args:
            pset (PSet): The set object to be searched.
        """
        self._sets.index(pset)

    def clear(self):
        """Clears this list."""
        self._sets.clear()

    def remove(self, pset):
        """Removes a given set from this list in-place.

        Args:
            pset (PSet): A set object to remove from this list.
        """
        self._sets.remove(pset)

    def pop(self, index):
        """Removes a set from a given index from this list in-place.

        Args:
            index (int): An index to remove a set from.
        """
        self._sets.pop(index)

    def count(self, pset):
        """Returns the number of given set objects in this list. Sets are considered same if they have the same code.

        Args:
            pset (pset): A set object to count.

        Returns:
            int: The number of given set objects in this list
        """
        return self._sets.count(pset)

    def sort(self, func):
        """Sorts the sets of this list with a given function in-place. The given function should return some
        attribute of a set object by which this list is sorted.

        Args:
            func: A function to sort this list with.
        """
        self._sets.sort(key=func)

    def filter(self, func):
        """Filters the sets of this list with a given function in-place. The new list contains all the cards
        for which the given function returns True.

        Args:
            func: A function to filter with.
        """
        self._sets.filter(key=func)

    def sorted(self, func):
        """Returns a new list with the sets of this list sorted with a given function. The given function should return
        some attribute of a set object by which this list is sorted.

        Args:
            func: A function to sort this list with.

        Returns:
            PCardList: A new instance of this list sorted.

        """
        return PSetList(sorted(self.sets, key=func))

    def filtered(self, func):
        """Returns a new list filtered with a given function. The new list contains all the sets
        for which the given function returns True.

        Args:
            func: A function to filter with.

        Returns:
            PCardList: A new instance of this list filtered.

        """
        return PSetList(list(filter(func, self.sets)))

    def where(self, invert=False, **kwargs):
        """Returns a new list of sets for which any of the given keyword arguments match partly or completely with the
        attributes of the sets in this list. The arguments should be any set attribute names such as 'name',
        'type' and 'block'. String attributes are case insensitive and it is enough that the argument is a
        substring. For list arguments the order does not matter and it is enough for one of the elements to match.

        The search can also be inverted by setting invert=True so that all the cards NOT matching will be returned.

        Note that searching for Null arguments is not supported.

        Args:
            invert: If True, a list of sets NOT matching the arguments is returned.
            **kwargs: Arguments to match with the attributes of this list's sets.

        Returns:
            bool: A new list of sets for which any of the given keyword arguments match partly or completely.
        """
        del_keys = []

        for (key, val) in kwargs.items():
            if not val:
                msg = 'Ignoring an empty or null value for keyword {}. Null or empty values are not supported.'
                warnings.warn(msg.format(key))
                del_keys.append(key)
            elif len(self.sets) == 0:
                msg = 'Searching an empty list.'
                warnings.warn(msg)
            elif not hasattr(self.sets[0], key):
                msg = 'Ignoring an unrecognized keyword {}. Make sure you are using correct api type and spelling.'
                warnings.warn(msg.format(key))
                del_keys.append(key)

        for key in del_keys:
            del kwargs[key]

        if not invert:
            return PSetList([pset for pset in self if pset.matches_any(**kwargs)])
        else:
            return PSetList([pset for pset in self if not pset.matches_any(**kwargs)])

    def where_exactly(self, invert=False, **kwargs):
        """Returns a new list of sets for which all of the given keyword arguments match completely with the attributes
        of the sets in this list. The arguments should be any set attribute names such as 'name', 'type' and 'block'.
        String attributes are case insensitive and must match exactly. For list arguments the order does not
        matter and and each element must match exactly.

        The search can also be inverted by setting invert=True so that all the cards NOT matching will be returned.

        Note that searching for Null arguments is not supported.

        Args:
            invert: If True, a list of sets NOT matching the arguments is returned.
            **kwargs: Arguments to match with the attributes of this list's cards.

        Returns:
            bool: A new list of sets for which all of the given keyword arguments match completely.
        """
        del_keys = []

        for (key, val) in kwargs.items():
            if not val:
                msg = 'Ignoring an empty or null value for keyword {}. Null or empty values are not supported.'
                warnings.warn(msg.format(key))
                del_keys.append(key)
            elif len(self.sets) == 0:
                msg = 'Searching an empty list.'
                warnings.warn(msg)
            elif not hasattr(self.sets[0], key):
                msg = 'Ignoring an unrecognized keyword {}. Make sure you are using correct api type and spelling.'
                warnings.warn(msg.format(key))
                del_keys.append(key)

        for key in del_keys:
            del kwargs[key]

        if not invert:
            return PSetList([pset for pset in self if pset.matches_all(**kwargs)])
        else:
            return PSetList([pset for pset in self if not pset.matches_all(**kwargs)])

    def pprint(self):
        """Prints out the contents of this list in a nice readable way."""
        print(self.pprint_str())

    def pprint_str(self):
        """Returns a nice readable string of the contents of this list.

        Returns:
            str: a string of the contents of this list in a nice readable format.
        """

        if len(self) == 0:
            if self.name:
                return 'Empty set list "{}" created at {}\n'.format(self.name, str(self.creation_date))
            else:
                return 'Unnamed empty set list created at {}\n'.format(self.creation_date)

        pp_str = ''

        if self.name:
            pp_str += 'Set list "{}" created at {}\n'.format(self.name, str(self.creation_date))
        else:
            pp_str += 'Unnamed set list created at {}\n'.format(self.creation_date)

        longest_name = max(len(pset.name) for pset in self.sets)
        longest_type = max(len(getattr(pset, 'set_type', getattr(pset, 'type', ''))) for pset in self.sets)
        longest_block = max(len(pset.block) if pset.block else 0 for pset in self.sets)
        longest_code = max(len(pset.code) if pset.code else 0 for pset in self.sets)

        pp_str += '-' * (longest_name + longest_type + longest_block + longest_code + 17)
        pp_str += '\n'

        format_str = '{name:{w1}s}   {code:{w2}s}   {block:{w3}s}   {type:{w4}s}   {cards}\n'
        pp_str += format_str.format(name='Set',
                                    w1=longest_name,
                                    code='Code',
                                    w2=longest_code,
                                    block='Block',
                                    w3=longest_block,
                                    type='Type',
                                    w4=longest_type,
                                    cards='Cards')
        pp_str += '-' * (longest_name + longest_type + longest_block + longest_code + 17)
        pp_str += '\n'

        for pset in self.sets:
            format_str = '{name:{w1}s}   {code:{w2}s}   {block:{w3}s}   {type:{w4}s}   {cards}\n'
            pp_str += format_str.format(name=pset.name,
                                        w1=longest_name,
                                        code=pset.code,
                                        w2=longest_code,
                                        block=pset.block if pset.block else '',
                                        w3=longest_block,
                                        type=getattr(pset, 'set_type', getattr(pset, 'type', '')),
                                        w4=longest_type,
                                        cards=len(pset))

        return pp_str

    @property
    def api_type(self):
        try:
            return self.sets[0].api_type
        except IndexError:
            return 'unspecified'

    @property
    def json(self):
        pset_json_dicts = []

        for pset in self.sets:
            json_dict = dict(pset.__dict__)
            del json_dict['_cards']
            del json_dict['_sideboard']
            del json_dict['creation_date']
            del json_dict['id']

            if len(pset) > 0:
                json_dict['cards'] = [card.__dict__ for card in pset.cards]
                pset_json_dicts.append(json_dict)

        return json.dumps({'sets': pset_json_dicts}, sort_keys=True, indent=4)

    @property
    def sets(self):
        return self._sets

    @sets.setter
    def sets(self, sets):
        if isinstance(sets, PSetList):
            self._sets = PersistentList(sets.sets)
        elif isinstance(sets, (list, PersistentList, tuple)):
            self._sets = PersistentList(sets)
        elif not sets:
            self._sets = PersistentList()
        else:
            raise TypeError
Exemplo n.º 10
0
class RecordContainer(Persistent):
    """
    Base/default record container uses PersistentDict for entry storage
    and PersistentList to store ordered keys.  This base container class
    does not advocate one place of storage for the container in a ZODB
    over another, so subclass implementations may choose to implement a
    container within a placeful (e.g. OFS or CMF Content item) or placeless
    (local utility) storage context.  Only a placeless context is supported
    by direct users of this class (without subclassing).

    For a container with hundreds of items or more, consider using instead
    BTreeRecordContainer as an implementation or base class, as it should
    handle memory usage and insert performance much better for larger sets
    of records.

    Usage
    -----

    RecordContainer acts as CRUD controller for working with records.

    The RecordContainer is an addressible object in the system, either as a
    registered utility (or with a subclass as "contentish" (CMF) content).

    Records themselves are not content, but data that are possibly
    non-atomic elements of an atomic content item (if the container is
    implemented in a subclass of RecordContainer as contentish).

    Usage:
    ------

    We need a record container object:

    >>> from uu.record.base import Record, RecordContainer
    >>> container = RecordContainer()
    >>> from uu.record.interfaces import IRecordContainer
    >>> assert IRecordContainer.providedBy(container)

    Record containers have length and containment checks:

    >>> assert len(container) == 0
    >>> import uuid  # keys for entries are stringified UUIDs
    >>> randomuid = str(uuid.uuid4())
    >>> assert randomuid not in container
    >>> assert container.get(randomuid, None) is None

    And they have keys/values/items methods like a mapping:

    >>> assert container.keys() == ()
    >>> assert container.values() == ()
    >>> assert container.items() == ()  # of course, these are empty now.

    Before we add records to a container, we need to create them; there are
    two possible ways to do this:

    >>> from uu.record.base import Record
    >>> entry1 = Record()
    >>> entry2 = container.create()  # preferred factory

    Both factory mechanisms create an entry item with a record_uid attribute:

    >>> from uu.record.interfaces import IRecord
    >>> assert IRecord.providedBy(entry1)
    >>> assert IRecord.providedBy(entry2)
    >>> is_uuid = lambda u: isinstance(u, str) and len(u) == 36
    >>> assert is_uuid(entry1.record_uid)
    >>> assert is_uuid(entry2.record_uid)

    And, these are RFC 4122 UUIDs, so even randomly generated 128-bit ids
    have near zero chance of collision:

    >>> assert entry1.record_uid != entry2.record_uid
    >>> assert entry2.record_uid != randomuid

    The record objects provide plone.uuid.interfaces.IAttributeUUID as an
    alternative way to get the UUID value (string representation) by
    adapting to IUUID:

    >>> from zope.configuration import xmlconfig
    >>> import plone.uuid
    >>> c = xmlconfig.file('configure.zcml', plone.uuid)  # load registrations
    >>> from plone.uuid.interfaces import IUUID, IAttributeUUID
    >>> from zope.component import queryAdapter
    >>> assert IAttributeUUID.providedBy(entry1)
    >>> assert queryAdapter(entry1, IUUID) is not None
    >>> assert queryAdapter(entry1, IUUID) == entry1.record_uid

    Now when we have a parent context with a schema, the created entries will
    be signed with the schema and provide it.

    RecordContainer.create() is the preferred factory when processing data.
    This is because it can take a mapping of keys/values, and copy each
    field name/value onto object attributes -- if and only if the attribute
    in question matches a type whitelist and a name blacklist filter.

    >>> entry4 = container.create(data={'record_uid':randomuid})
    >>> assert entry4.record_uid == randomuid
    >>> entry5 = container.create(data={'count':5})
    >>> assert entry5.count == 5
    >>> entry6 = container.create(data={'_bad_name'    : True,
    ...                                  'count'        : 2,
    ...                                  'bad_value'    : lambda x: x })
    >>> assert not hasattr(entry6, '_bad_name')  # no leading underscores
    >>> assert entry6.count == 2
    >>> assert not hasattr(entry6, 'bad_value')  # function not copied!

    Of course, merely using the record container object as a factory for
    new records does not mean they are stored within (yet):

    >>> assert entry4.record_uid not in container
    >>> assert entry4.record_uid not in container.keys()

    Let's add an item:

    >>> container.add(entry4)

    There are two ways to check for containment, by either key or value:

    >>> assert entry4 in container
    >>> assert entry4.record_uid in container

    We can get records using a (limited, read) mapping-like interface:

    >>> assert len(container) == 1  # we just added the first entry
    >>> assert container.values()[0] is entry4
    >>> assert container.get(entry4.record_uid) is entry4
    >>> assert container[entry4.record_uid] is entry4

    We can deal with references to entries also NOT in the container:

    >>> import uuid
    >>> randomuid = str(uuid.uuid4())
    >>> assert randomuid not in container
    >>> assert container.get(str(uuid.uuid4()), None) is None
    >>> assert entry1.record_uid not in container

    And we can check containment on either an instance or a UID; checking on
    an instance is just a convenience that uses its UID (record_uid) field
    to check for actual containment:

    >>> assert entry4.record_uid in container
    >>> assert entry4 in container  # shortcut!

    However, it should be noted for good measure:

    >>> assert entry4 in container.values()
    >>> assert entry4.record_uid in container.keys()
    >>> assert entry4 not in container.keys()  # of course!
    >>> assert (entry4.record_uid, entry4) in container.items()

    We can modify a record contained directly; this is the most direct and
    low-level update interface for any entry:

    >>> _marker = object()
    >>> assert getattr(entry4, 'title', _marker) is _marker
    >>> entry4.title = u'Curious George'
    >>> assert container.get(entry4.record_uid).title == u'Curious George'

    We can add another record:

    >>> container.add(entry6)
    >>> assert entry6 in container
    >>> assert entry6.record_uid in container
    >>> assert len(container) == 2

    Keys, values, items are always ordered; since we added entry4, then
    entry6 previously, they will return in that order:

    >>> expected_order = (entry4, entry6)
    >>> expected_uid_order = tuple([e.record_uid for e in expected_order])
    >>> expected_items_order = tuple(zip(expected_uid_order, expected_order))
    >>> assert tuple(container.keys()) == expected_uid_order
    >>> assert tuple(container.values()) == expected_order
    >>> assert tuple(container.items()) == expected_items_order

    We can re-order this; let's move entry6 up to position 0 (first):

    >>> container.reorder(entry6, offset=0)
    >>> expected_order = (entry6, entry4)
    >>> expected_uid_order = tuple([e.record_uid for e in expected_order])
    >>> expected_items_order = tuple(zip(expected_uid_order, expected_order))
    >>> assert tuple(container.keys()) == expected_uid_order
    >>> assert tuple(container.values()) == expected_order
    >>> assert tuple(container.items()) == expected_items_order

    We can also re-order by UID instead of record/entry reference:

    >>> container.reorder(entry6.record_uid, offset=1)  # where it was before
    >>> expected_order = (entry4, entry6)
    >>> expected_uid_order = tuple([e.record_uid for e in expected_order])
    >>> expected_items_order = tuple(zip(expected_uid_order, expected_order))
    >>> assert tuple(container.keys()) == expected_uid_order
    >>> assert tuple(container.values()) == expected_order
    >>> assert tuple(container.items()) == expected_items_order

    And we can remove records from containment by UID or by reference (note,
    del(container[key]) uses __delitem__ since a container is a writable
    mapping):

    >>> del(container[entry6])
    >>> assert entry6 not in container
    >>> assert entry6.record_uid not in container
    >>> assert len(container) == 1
    >>> assert entry4 in container
    >>> del(container[entry4.record_uid])
    >>> assert entry4 not in container
    >>> assert len(container) == 0

    Earlier, direct update of objects was demonstrated: get an object and
    modify its properties.  This attribute-setting mechanism is the best
    low-level interface, but it does not (a) support a wholesale update
    from either a field dictionary/mapping nor another object providing
    IRecord needing its data to be copied; nor (b) support notification
    of zope.lifecycle object events.

    Given these needs, a high level interface for update exists, with the
    record object acting as a controller for updating contained entries.
    This provides for update via another entry (a field-by-field copy) or
    from a data dictionary/mapping.

    >>> newuid = str(uuid.uuid4())
    >>> data = {    'record_uid' : newuid,
    ...             'title'      : u'George',
    ...             'count'      : 9,
    ...        }
    >>> assert len(container) == 0  # empty, nothing in there yet!
    >>> assert newuid not in container

    Note, update() returns an entry; return value can be ignored if caller
    deems it not useful.

    >>> entry = container.update(data)
    >>> assert newuid in container  # update implies adding!
    >>> assert entry is container.get(newuid)
    >>> assert entry.title == data['title']
    >>> assert entry.count == data['count']

    Now, the entry we just modified was also added.  We can modify it again:

    >>> data = {    'record_uid' : newuid,
    ...             'title'      : u'Curious George',
    ...             'count'      : 2,
    ...        }
    >>> entry = container.update(data)
    >>> assert newuid in container     # same uid
    >>> entry.title
    u'Curious George'
    >>> entry.count
    2
    >>> assert len(container) == 1     # same length, nothing new was added.

    We could also create a stand-in entry for which data is copied to the
    permanent entry with the same UUID on update:

    >>> temp_entry = container.create()
    >>> temp_entry.record_uid = newuid      # overwrite with the uid of entry
    >>> temp_entry.title = u'Monkey jumping on the bed'
    >>> temp_entry.count = 0

    temp_entry is a stand-in which we will pass to update(), when we really
    intend to modify entry (they have the same UID):

    >>> real_entry = container.update(temp_entry)
    >>> assert container.get(newuid) is not temp_entry
    >>> assert container.get(newuid) is entry  # still the same object...
    >>> assert container.get(newuid) is real_entry
    >>> entry.title                             # ...but data is modified!
    u'Monkey jumping on the bed'
    >>> entry.count
    0
    >>> assert len(container) == 1     # same length, nothing new was added.


    JSON integration
    ----------------

    As a convenience, update_all() parses JSON into a data dict for use by
    update(), using the Python 2.6 json library (aka/was: simplejson):

    >>> party_form = RecordContainer()
    >>> entry = party_form.create()
    >>> party_form.add(entry)
    >>> data = {  # mock data we'll serialize to JSON
    ...     'record_uid': entry.record_uid,  # which record to update
    ...     'name'      : 'Me',
    ...     'birthday'  : u'77/06/01',
    ...     'party_time': u'11/06/05 12:00',
    ...     }
    >>> import json  # requires Python >= 2.6
    >>> data['name'] = 'Chunky monkey'
    >>> serialized = json.dumps([data,], indent=2)  # JSON array of one item...
    >>> print serialized  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
    [
      {
        "party_time": "11/06/05 12:00",
        "birthday": "77/06/01",
        "name": "Chunky monkey",
        "record_uid": "..."
      }
    ]

    The JSON created above is useful enough for demonstration, despite being
    only a single-item list.

    >>> assert getattr(entry, 'name', _marker) is _marker  # before, no attr
    >>> party_form.update_all(serialized)
    >>> entry.name  # after update
    u'Chunky monkey'

    update_all() also takes a singular record, not just a JSON array:

    >>> data['name'] = 'Curious George'
    >>> serialized = json.dumps(data, indent=2)  # JSON object, not array.
    >>> print serialized  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
    {
      "party_time": "11/06/05 12:00",
      "birthday": "77/06/01",
      "name": "Curious George",
      "record_uid": "..."
    }
    >>> entry.name  # before
    u'Chunky monkey'
    >>> party_form.update_all(serialized)
    >>> entry.name  # after update
    u'Curious George'

    JSON parsing also supports a "bundle" or wrapper object around a list of
    entries, where the wrapper contains metadata about the form itself, not
    its entries (currently, this is just the process_changes field, which
    is sourced from the JSON bundle/wrapper object field called 'notes').
    When wrapped, the list of entries is named 'entries' inside the wrapper.

    >>> data['name'] = u'Party monkey'
    >>> serialized = json.dumps({'notes'    : 'something changed',
    ...                          'entries'  : [data,]},
    ...                         indent=2)  # JSON array of one item...
    >>> entry.name  # before
    u'Curious George'
    >>> party_form.update_all(serialized)
    >>> entry.name  # after
    u'Party monkey'

    It should be noted that update_all() removes entries not in the data
    payload, and it preserves the order contained in the JSON entries.

    Object events
    -------------

    CRUD methods on a controlling object should have some means of extension,
    pluggable to code that should subscribe to CRUD (object lifecycle) events.
    We notify four distinct zope.lifecycleevent object event types:

    1. Object created (zope.lifecycleevent.interfaces.IObjectCreatedEvent)

    2. Object addded to container:
        (zope.lifecycleevent.interfaces.IObjectAddedEvent).

    3. Object modified (zope.lifecycleevent.interfaces.IObjectModifiedEvent)

    4. Object removed (zope.lifecycleevent.interfaces.IObjectRemovedEvent)

    Note: the create() operation both creates and modifies: as such, both
    created and modified events are fired off, and since most creations also
    are followed by an add() to a container, you may have three events to
    subscribe to early in a new entry's lifecycle.

    First, some necessary imports of events and the @adapter decorator:

    >>> from zope.component import adapter
    >>> from zope.lifecycleevent import IObjectCreatedEvent
    >>> from zope.lifecycleevent import IObjectModifiedEvent
    >>> from zope.lifecycleevent import IObjectRemovedEvent
    >>> from zope.lifecycleevent import IObjectAddedEvent

    Let's define dummy handlers:

    >>> @adapter(IRecord, IObjectCreatedEvent)
    ... def handle_create(context, event):
    ...     print 'object created'
    ...
    >>> @adapter(IRecord, IObjectModifiedEvent)
    ... def handle_modify(context, event):
    ...     print 'object modified'
    ...
    >>> @adapter(IRecord, IObjectRemovedEvent)
    ... def handle_remove(context, event):
    ...     print 'object removed'
    ...
    >>> @adapter(IRecord, IObjectAddedEvent)
    ... def handle_add(context, event):
    ...     print 'object added'
    ...

    Next, let's configure zope.event to use zope.component event
    subscribers; most frameworks using zope.lifecycleevent already do
    this, but we will configure this explicitly for documentation
    and testing purposes, only if not already enabled:

    >>> import zope.event
    >>> from zope.component import getGlobalSiteManager
    >>> gsm = getGlobalSiteManager()

    Importing zope.component.event puts dispatch() in zope.event.subscribers:

    >>> from zope.component import event
    >>> assert event.dispatch in zope.event.subscribers
    
    Now, let's register the handlers:

    >>> for h in (handle_create, handle_modify, handle_remove, handle_add):
    ...     gsm.registerHandler(h)
    ...

    Usually, these handlers will be registered in the global site manager
    via ZCML and zope.configuration, but they are registered in Python
    above for documentation/testing purposes.

    We can watch these event handlers get fired when CRUD methods are called.

    Object creation, with and without data:

    >>> newentry = container.create()      # should print 'object created'
    object created
    >>> another_uid = str(uuid.uuid4())
    >>> newentry = container.create({'count':88})
    object modified
    object created

    Object addition:

    >>> container.add(newentry)
    object added
    >>>

    Object removal:

    >>> del(container[newentry.record_uid])  # via __delitem__()
    object removed

    Object update (existing object):

    >>> entry = container.values()[0]
    >>> entry = container.update({'record_uid' : entry.record_uid,
    ...                            'title'      : u'Me'})
    object modified

    Object modified (new object or not contained):

    >>> random_uid = str(uuid.uuid4())
    >>> entry = container.update({'record_uid' : random_uid,
    ...                            'title'      : u'Bananas'})
    object modified
    object created
    object added

    Event handlers for modification can know what fields are modified; let's
    create a more interesting modification handler that prints the names of
    changed fields.

    >>> from zope.lifecycleevent.interfaces import IAttributes
    >>> unregistered = gsm.unregisterHandler(handle_modify)
    >>> @adapter(IRecord, IObjectModifiedEvent)
    ... def handle_modify(context, event):
    ...     if event.descriptions:
    ...         attr_desc = [d for d in event.descriptions
    ...                         if (IAttributes.providedBy(d))]
    ...         if attr_desc:
    ...             field_names = attr_desc[0].attributes
    ...         print tuple(field_names)
    >>> gsm.registerHandler(handle_modify)

    >>> entry = container.values()[0]
    >>> entry = container.update({'record_uid' : entry.record_uid,
    ...                            'title'      : u'Hello'})
    ('title',)

    Finally, clean up and remove all the dummy handlers:
    >>> for h in (handle_create, handle_modify, handle_remove, handle_add):
    ...     success = gsm.unregisterHandler(h)
    ...

    """

    implements(IRecordContainer)

    # whitelist types of objects to copy on data update:

    TYPE_WHITELIST = (
        int,
        long,
        str,
        unicode,
        bool,
        float,
        time.time,
        datetime,
        date,
        timedelta,
        decimal.Decimal,
    )

    SEQUENCE_WHITELIST = (
        list,
        tuple,
        set,
        frozenset,
        PersistentList,
    )

    MAPPING_WHITELIST = (
        dict,
        PersistentDict,
    )

    RECORD_INTERFACE = IRecord

    factory = Record

    def __init__(self, factory=Record, _impl=PersistentDict):
        self._entries = _impl()
        self._order = PersistentList()
        self.factory = factory

    # IWriteContainer methods:

    def _update_size(self):
        self._size = len(self._order)
        self._p_changed = True

    def __setitem__(self, key, value):
        if isinstance(key, uuid.UUID) or isinstance(key, unicode):
            key = str(key)
        elif not (isinstance(key, str) and len(key) == 36):
            raise KeyError('key does not appear to be string UUID: %s', key)
        if not self.RECORD_INTERFACE.providedBy(value):
            raise ValueError('Record value must provide %s' %
                             (self.RECORD_INTERFACE.__identifier__))
        self._entries[key] = value
        if key not in self._order:
            self._order.append(key)
            self._update_size()

    def __delitem__(self, record):
        uid = record
        if self.RECORD_INTERFACE.providedBy(record):
            uid = str(record.record_uid)
        elif isinstance(record, uuid.UUID):
            uid = str(record)
        if not (isinstance(uid, str) and len(uid) == 36):
            raise ValueError('record neither record object nor UUID')
        if uid not in self._entries:
            raise ValueError('record not found contained within')
        if uid in self._order:
            self._order.remove(uid)
            self._update_size()
        if not self.RECORD_INTERFACE.providedBy(record):
            record = self._entries.get(uid)  # need ref for event notify below
        del (self._entries[uid])
        notify(ObjectRemovedEvent(record, self, uid))

    # IRecordContainer and IOrdered re-ordering methods:

    def reorder(self, record, offset):
        """
        Reorder a record (either UUID or object with record_uid attribute)
        in self._order, if record exists.  If no UUID exists in self._order,
        raise a ValueError.  Offset must be non-negative integer.
        """
        uid = record
        offset = abs(int(offset))
        if self.RECORD_INTERFACE.providedBy(record):
            uid = record.record_uid
        if not uid or uid not in self._order:
            raise ValueError('cannot find record to move for id %s' % uid)
        self._order.insert(offset, self._order.pop(self._order.index(uid)))

    def updateOrder(self, order):
        """Provides zope.container.interfaces.IOrdered.updateOrder"""
        if len(order) != len(self._order):
            raise ValueError('invalid number of keys')
        s_order = set(order)
        if len(order) != len(s_order):
            raise ValueError('duplicate keys in order')
        if s_order - set(self._order):
            raise ValueError('unknown key(s) provided in order')
        if not isinstance(order, PersistentList):
            order = PersistentList(order)
        self._order = order

    # IReadContainer interface methods:

    def get(self, uid, default=None):
        """
        Get object providing IRecord for given UUID uid or return None
        """
        if self.RECORD_INTERFACE.providedBy(uid):
            uid = uid.record_uid  # special case to support __contains__() impl
        v = self._entries.get(str(uid), default)
        if v and getattr(v, '_v_parent', None) is None:
            v._v_parent = self  # container marks item with itself as context
        return v

    def __contains__(self, record):
        """
        Given record as either IRecord object or UUID, is record contained?
        """
        if self.RECORD_INTERFACE.providedBy(record):
            return self.get(record, None) is not None
        return str(record) in self._entries

    def __len__(self):
        """
        return length of record entries
        """
        size = getattr(aq_base(self), '_size', None)
        return size if size is not None else len(self._order)

    def __getitem__(self, key):
        """Get item by UID key"""
        v = self.get(key, None)
        if v is None:
            raise KeyError('unknown UID for record entry')
        return v

    def keys(self):
        """return tuple with elements ordered"""
        return tuple(self._order)

    def values(self):
        """return tuple of records in order"""
        return tuple([t[1] for t in self.items()])

    def items(self):
        """return ordered pairs of key/values"""
        return tuple([(uid, self.get(uid)) for uid in self._order])

    def __iter__(self):
        return self._order.__iter__()

    # IRecordContainer-specific CRUD methods:

    def _type_whitelist_validation(self, value):
        vtype = type(value)
        if vtype in self.MAPPING_WHITELIST:
            for k, v in value.items():
                if not (k in self.TYPE_WHITELIST and v in self.TYPE_WHITELIST):
                    raise ValueError('Unsupported mapping key/value type')
        elif vtype in self.SEQUENCE_WHITELIST:
            for v in value:
                if v not in self.TYPE_WHITELIST:
                    raise ValueError('Unsupported sequence value type')
        else:
            if vtype not in self.TYPE_WHITELIST:
                raise ValueError('Unsupported data type')

    def _populate_record(self, record, data):
        """
        Given mapping of data, copy values to attributes on record.

        Subclasses may override to provide schema validation, selective
        copy of names, and normalization of values if/as necessary.
        """
        changelog = []
        for key, value in data.items():
            if key.startswith('_'):
                continue  # invalid key
            if key == 'record_uid':
                self.record_uid = str(value)
                continue
            try:
                self._type_whitelist_validation(value)
            except ValueError:
                continue  # skip problem name!
            existing_value = getattr(self, key, None)
            if value != existing_value:
                changelog.append(key)
                setattr(record, key, value)
        if changelog:
            record._p_changed = True
            changelog = [
                Attributes(self.RECORD_INTERFACE, name) for name in changelog
            ]
            notify(ObjectModifiedEvent(record, *changelog))

    def create(self, data=None):
        """
        Alternative factory for an IRecord object, does not store object.
        If data is not None, copy fields from data.
        """
        if data is None:
            data = {}
        uid = data.get('record_uid', str(uuid.uuid4()))  # get or random uuid
        record = self.factory(context=self, uid=uid)
        if data and (hasattr(data, 'get') and hasattr(data, 'items')):
            self._before_populate(record, data)
            self._populate_record(record, data)
        notify(ObjectCreatedEvent(record))
        return record

    def add(self, record):
        """
        Add a record to container, append UUID to end of order; over-
        write existing entry if already exists for a UUID (in such case
        leave order as-is).
        """
        uid = str(record.record_uid)
        if not uid:
            raise ValueError('record has empty UUID')
        self._entries[uid] = record
        if uid not in self._order:
            self._order.append(uid)
            self._update_size()
        notify(ObjectAddedEvent(record, self, uid))

    def _ad_hoc_fieldlist(self, record):
        attrs = [name for name in dir(record) if not name.startswith('_')]
        fieldnames = []
        for name in attrs:
            v = getattr(record, name)
            try:
                self._type_whitelist_validation(v)
                fieldnames.append(name)
            except ValueError:
                pass  # ignore name
        return fieldnames

    def _filtered_data(self, data):
        fieldnames = self._ad_hoc_fieldlist(data)
        if IRecord.providedBy(data):
            return dict([(k, getattr(data, k, None)) for k in fieldnames])
        return dict([(k, data.get(k, None)) for k in fieldnames])

    def _before_populate(self, record, data):
        pass  # hook for subclasses

    def _before_update_notification(self, record, data):
        pass  # hook for subclasses

    def notify_data_changed(self):
        notify(ObjectModifiedEvent(self, Attributes(IRecordContainer,
                                                    'items')))

    def update(self, data, suppress_notify=False):
        """
        Given data, which may be a dict of field key/values or an actual
        IRecord providing object, update existing entry given a UUID, or
        add the entry if an entry for that UUID does not yet exist.  The
        update should copy all values for every key provided.  Specialized
        or schema-bound subclasses of this interface may execute more
        elaborate rules on what data is copied and how it is normalized.

        Pre-condition:

          * All new (added) entries updated this way must contain a record_uid
            field with a string UUID.

        Post-condition:

          * New items should always be handled through self.create() and then
            self.add().

          * Method returns modified record.

          * Should notify at least zope.lifecycleevent.IObjectModifiedEvent,
            (if changes, detection of which is left up to implementation).

          * On creation of new records, should notify both
            IObjectCreatedEvent and IObjectAddedEvent (the record container
            is the context of record).

        """
        if self.RECORD_INTERFACE.providedBy(data):
            uid = data.record_uid
            data = self._filtered_data(data)
        else:
            uid = data.get('record_uid', None)
        if uid is None:
            raise ValueError('empty record UID on update')
        uid = str(uid)
        record = self.get(uid, None)
        if record is not None:
            # existing record, already known/saved
            self._before_populate(record, data)
            self._populate_record(record, data)  # also notifies modified event
        else:
            # new, create, then add
            record = self.create(data)  # notifies created, modified for record
            self.add(record)  # notified added event
        self._before_update_notification(record, data)
        if (not suppress_notify) and getattr(record, '_p_changed', None):
            self.notify_data_changed()
        return record

    def _process_container_metadata(self, data):
        return False  # hook for subclasses

    def update_all(self, data):
        """
        Given sequence of data dictionaries or a JSON serialization
        thereof, update each item.  Raises ValueError on missing UID of
        any item/entry.  Also supports JSON serialization of a single
        record/entry dict.
        """
        _modified = False
        if isinstance(data, basestring):
            _data = json.loads(data)
            if isinstance(_data, dict):
                # dict might be singluar item, or wrapping object; a wrapping
                # object would have a list called 'entries'
                if 'entries' in _data and isinstance(_data['entries'], list):
                    _modified = self._process_container_metadata(_data)
                    # wrapper, get entries from within.
                    _data = _data['entries']
                else:
                    # singular record, not a wrapper
                    _data = [_data]  # wrap singular item update in list
            _keynorm = lambda o: dict([(str(k), v) for k, v in o.items()])
            data = [_keynorm(o) for o in _data]
        uids = [str(o['record_uid']) for o in data]
        existing_uids = set(self.keys())
        added_uids = set(uids) - existing_uids
        modified_uids = set(uids).intersection(existing_uids)
        for entry_data in data:
            if 'record_uid' not in entry_data:
                raise ValueError('record missing UID')
            record = self.update(entry_data, suppress_notify=True)
            if not _modified and getattr(record, '_p_changed', None):
                _modified = True
        remove_uids = existing_uids - set(uids)
        for deluid in remove_uids:
            del (self[deluid])  # remove any previous entries not in the form
        self._order = PersistentList(uids)  # replace old with new uid order
        if added_uids or modified_uids:
            _modified = True
        if data and _modified:
            self.notify_data_changed()  # notify just once
Exemplo n.º 11
0
class NyPortalLanguageManager(Persistent):
    """
    Portal_i18n has an instance of this type, accessible by *get_lang_manager()*
    method. It supplies add/edit/remove/set_default operations with languages
    available in portal and it is also used to get current available
    languages, default language and manage display order.

    """
    implements(INyLanguageManagement)

    # by default, the display order is the creation order, default lang first
    custom_display_order = None

    def __init__(self, default_langs=[('en', 'English')]):
        if not isinstance(default_langs, list):
            raise ValueError("Default languages must be a list of touples"
                             " (code, name)")
        self.portal_languages = PersistentList(default_langs)

    def getAvailableLanguages(self):
        """Return a sequence of language tags/codes for available languages
        """
        if self.custom_display_order is None:
            return tuple([x[0] for x in self.portal_languages])
        else:
            return tuple(self.custom_display_order)

    def addAvailableLanguage(self, lang_code, lang_name=None):
        """Adds available language in portal"""
        lang_code = normalize_code(lang_code)
        if not lang_name:
            lang_name = get_iso639_name(lang_code)
        if lang_code not in self.getAvailableLanguages():
            self.portal_languages.append((lang_code, lang_name))
            self.set_display_order()

    def delAvailableLanguage(self, lang):
        """
        Deletes specified language from available languages list in portal

        """
        lang = normalize_code(lang)
        available = [x[0] for x in self.portal_languages]
        if lang in available:
            if len(available) == 1:
                raise ValueError("Can not delete the only available language")
            else:
                pos = available.index(lang)
                self.portal_languages.pop(pos)
                self.set_display_order()

    # MORE:
    def set_display_order(self, operation=None):
        """
        `operation` is 'x-y' where x and y are consecutive indices
        about to be switched.
        If operation is None, custom display order is "refreshed" if it is
        defined and if there were any changes in available portal languages.

        """
        if self.custom_display_order is None and operation is None:
            # no operation, no custom order
            return

        creation_order = [x[0] for x in self.portal_languages]

        if operation is None:
            # explore for changes - new added lang or removed language
            # custom_display_order obviously not None
            added = set(creation_order) - set(self.custom_display_order)
            rmed = set(self.custom_display_order) - set(creation_order)
            self.custom_display_order.extend(list(added))
            for r in rmed:
                self.custom_display_order.remove(r)
        else:
            # we have a "move operation" request
            if self.custom_display_order is None:
                self.custom_display_order = PersistentList(creation_order)
            switch = map(int, operation.split("-"))
            assert ((switch[0] - switch[1])**2 == 1)
            acc = self.custom_display_order.pop(switch[0])
            self.custom_display_order.insert(switch[1], acc)

    def set_default_language(self, lang):
        """
        Sets default language in language manager. Default language
        is mainly used in negotiation. Also rearranges langs order: first
        is default, the rest are sorted alphabetically.

        """
        lang = normalize_code(lang)
        if lang not in self.getAvailableLanguages():
            raise ValueError("Language %s is not provided by portal" % lang)
        available = [x[0] for x in self.portal_languages]
        if len(available) == 1:
            return
        pos = available.index(lang)
        new_default = self.portal_languages.pop(pos)
        self.portal_languages.insert(0, new_default)
        self.set_display_order()

    def get_default_language(self):
        """ Returns default language """
        return self.portal_languages[0][0]

    def get_language_name(self, code):
        """
        Returns the name of a language available in portal, '???' otherwise.

        """
        available = list(self.getAvailableLanguages())
        if code in available:
            pos = [lcode for lcode, name in self.portal_languages].index(code)
            return self.portal_languages[pos][1]
        else:
            return "???"
class SimpleSpsso(HomogenousContainer, Sso):
  """Zope 2 implementation of a simple SAML2 Spsso."""

  implements(ISimpleSpsso)

  SC_SCHEMAS = (ISimpleSpsso,)
  CONTENT_TYPE = AttributeConsumingService

  security = ClassSecurityInfo()

  security.declareObjectProtected(manage_saml)
  security.declarePublic("authenticate")

  # as we only support http-post and this requires signing
  wants_assertions_signed = True

  # newly introduced
  nameid_formats = ()
  allow_create = True

  
  def __init__(self, **kw):
    SchemaConfigured.__init__(self, **kw)
    Sso.__init__(self)
    self.__keys = PersistentList()
    self.new_key()

  def authenticate(self, idp, ok, fail, authn_context_class=None, passive=False, force=False, acs_index=None, REQUEST=None):
    """authenticate via *idp*."""
    r = REQUEST or self.REQUEST; R = r.response
    if authn_context_class is None:
      authn_context_class = self.default_authn_context_class
    if authn_context_class is not None:
      authn_context_class = normalize_class(authn_context_class)
    if not force:
      # see whether we have a valid authentication satisfying the requirements
      session = self.get_authentication_session(r)
      if session:
        comparison = (
          authn_context_class is None and -1
          or compare_classes(authn_context_class,
                             session["authn_context_class"]
                             )
          )
        if comparison is not None and comparison <= 0:
          return R.redirect(ok)
    # must authenticate
    from dm.saml2.pyxb.protocol import AuthnRequest, RequestedAuthnContext, \
         NameIDPolicy
    from dm.saml2.pyxb.assertion import AuthnContextClassRef
    req = AuthnRequest(ForceAuthn=force, IsPassive=passive)
    if authn_context_class is not None:
      req.RequestedAuthnContext = RequestedAuthnContext(
        AuthnContextClassRef(authn_context_class)
        )
    if acs_index is not None:
      req.AttributeConsumingServiceIndex = acs_index
    self.customize_authn_request(req)
    relay_state = self.store((req.ID, ok, fail))
    nip = NameIDPolicy(AllowCreate=self.allow_create)
    nifs = INameidFormatSupport(self).supported
    if len(nifs) == 1: nip.Format = nifs[0]
    req.NameIDPolicy = nip
    return self.deliver(
      Target(eid=idp, role="idpsso", endpoint="SingleSignOnService",
             sign_msg_attr="WantAuthnRequestsSigned",
             ),
      None, req, relay_state
      )

  # optionally overridden by derived classes to customize the request
  def customize_authn_request(self, req): pass

  def _process_AuthnStatement(self, subject, s):
    info = dict(
      name_qualifier=subject.NameQualifier,
      nameid_format=subject.Format,
      sp_name_qualifier=subject.SPNameQualifier,
      nameid=subject.value(),
      authn_time=pyxb_to_datetime(as_utc(s.AuthnInstant)),
      valid_until=pyxb_to_datetime(as_utc(s.SessionNotOnOrAfter)),
      session_id=s.SessionIndex,
      authn_context_class=s.AuthnContext.AuthnContextClassRef,
      )
    info["user_id"] = self.format_user_id(info)
    self._set_cookie(self.session_cookie_name, info)

  def _process_AttributeStatement(self, subject, s):
    # build map of known attributes -- we might want to cache this information
    attrs = {}
    for acs in self.objectValues():
      for att in acs.objectValues():
        attrs[(att.format, att.title)] = att
    # now process the attributes
    info = {}
    for att in s.Attribute:
      d = attrs.get(
        (att.NameFormat or normalize_attrname_format("unspecified"),
         att.Name
         ))
      if d is None: continue # do not know this attribute
      info[d.getId()] = xs_convert_from_xml(
        att.AttributeValue, d.is_sequence, d.type
        )
    self._set_cookie(self.attribute_cookie_name, info)
    user_info = self._get_cookie(self.REQUEST, self.session_cookie_name)
    notify(SamlUserAuthenticated(user_info['user_id']))



  def get_authentication_session(self, request):
    info = self._get_cookie(request, self.session_cookie_name)
    if info is None: return
    if info["valid_until"] is not None:
      if utcnow() >= info["valid_until"]: return # no longer valid
    return info


  def invalidate_authentication_session(self, request):
    expire = request.response.expireCookie
    params = self._cookie_params()
    expire(self.session_cookie_name, **params)
    expire(self.attribute_cookie_name, **params)


  def get_attributes(self, request):
    # only called when there is an auth session
    #auths = self.get_authentication_session()
    #if auths is None: return
    return self._get_cookie(request, self.attribute_cookie_name)


  def format_user_id(self, info):
    """format user id from dict *info*.

    Almost surely, the `nameid` key will be used.
    To ensure uniqueness, you might need the `name_qualifier`.
    `nameid_format` may allow specific decisions about
    the user id format.

    The primary purpose of this method is customization.
    Be warned however, that changing the format will invalidate
    existing user references.
    """
    return "%(name_qualifier)s::%(nameid)s" % info


  def _cookie_params(self):
    params = dict(path=self.cookie_path, httpOnly=True)
    if self.cookie_domain: params["domain"] = self.cookie_domain
    return params

  def _set_cookie(self, name, info):
    self.REQUEST.response.setCookie(name, self._encode(info),
                                    **self._cookie_params()
                                    )

  def _get_cookie(self, req, name):
    return self._decode(req.cookies.get(name))

  def _encode(self, info):
    """encode *info* for use as integrity protected cookie value."""
    s = self._serialize(info)
    key = self.__keys[0]
    if self.encrypt_cookies:
      value = getUtility(IEncryption).encrypt(key, s)
    else:
      value = hmac.new(key, s, digest_module).digest() + s
    return value.encode("base64").replace("\n", "").replace(" ", "")

  def _decode(self, v):
    """decode cookie value *v* into an info object (or `None`)."""
    if not v: return
    v = v.decode("base64")
    deserialize = self._deserialize
    if self.encrypt_cookies:
      decrypt = getUtility(IEncryption).decrypt
      for k in self.__keys:
        # try to decrypt and deserialize
        try: return deserialize(decrypt(key, v))
        except:
          # probably a wrong key
          pass
      return None # unable to understand the cookie
    else:
      dl = hmac.new("", "", digest_module).digest_size
      digest = v[:dl]; v = v[dl:]
      for k in self.__keys:
        if digest == hmac.new(k, v, digest_module).digest():
          return deserialize(v)
      return None # unable to authenticate

  def _serialize(self, obj):
    return compress(dumps(obj, -1), 9)

  def _deserialize(self, s):
    return loads(decompress(s))

  def add_attribute_metadata(self, descriptor):
    from dm.saml2.pyxb.metadata import AttributeConsumingService, \
         RequestedAttribute, ServiceName, ServiceDescription
    from dm.saml2.pyxb.assertion import Attribute
    # check uniqueness of "index" -- should probably happen earlier
    seen_indexes = set()
    for acs in self.objectValues():
      ads = acs.objectValues()
      if not ads: continue
      index = acs.index
      if index in seen_indexes:
        raise ValueError("duplicate acs index %d for sp %s"
                         % (index, descriptor.getId())
                         )
      seen_indexes.add(index)
      acs_md = AttributeConsumingService(
        ServiceName(acs.title, lang=acs.language),
        index=index, isDefault=acs.is_default,
        )
      if acs.description:
        acs_md.ServiceDescription.append(
          ServiceDescription(asc.description, lang=acs.language)
          )
      for ad in ads:
        acs_md.RequestedAttribute.append(
          RequestedAttribute(
            NameFormat=ad.format,
            Name=ad.title,
            FriendlyName=ad.id,
            isRequired=ad.required,
            )
          )
      descriptor.AttributeConsumingService.append(acs_md)

      

  ## key handling
  KEY_LENGTH = 32
  KEY_NUMBER = 5

  def _create_key(self):
    kl = self.KEY_LENGTH
    try:
      from os import urandom
      bytes = urandom(kl)
    except ImportError: # no "urandom"
      from random import randrange
      bytes = [chr(randrange(256)) for i in range(kl)]
    return "".join(bytes)

  def clear_keys(self):
    del self.__keys[:]

  def new_key(self):
    self.__keys.insert(0, self._create_key())
    del self.__keys[self.KEY_NUMBER:]
Exemplo n.º 13
0
class SimpleSpsso(HomogenousContainer, Sso):
    """Zope 2 implementation of a simple SAML2 Spsso."""

    implements(ISimpleSpsso)

    SC_SCHEMAS = (ISimpleSpsso, )
    CONTENT_TYPE = AttributeConsumingService

    security = ClassSecurityInfo()

    security.declareObjectProtected(manage_saml)
    security.declarePublic("authenticate")

    # as we only support http-post and this requires signing
    wants_assertions_signed = True

    # newly introduced
    nameid_formats = ()
    allow_create = True

    def __init__(self, **kw):
        SchemaConfigured.__init__(self, **kw)
        Sso.__init__(self)
        self.__keys = PersistentList()
        self.new_key()

    def authenticate(self,
                     idp,
                     ok,
                     fail,
                     authn_context_class=None,
                     passive=False,
                     force=False,
                     acs_index=None,
                     REQUEST=None):
        """authenticate via *idp*."""
        r = REQUEST or self.REQUEST
        R = r.response
        if authn_context_class is None:
            authn_context_class = self.default_authn_context_class
        if authn_context_class is not None:
            authn_context_class = normalize_class(authn_context_class)
        if not force:
            # see whether we have a valid authentication satisfying the requirements
            session = self.get_authentication_session(r)
            if session:
                comparison = (authn_context_class is None and -1
                              or compare_classes(
                                  authn_context_class,
                                  session["authn_context_class"]))
                if comparison is not None and comparison <= 0:
                    return R.redirect(ok)
        # must authenticate
        from dm.saml2.pyxb.protocol import AuthnRequest, RequestedAuthnContext, \
             NameIDPolicy
        from dm.saml2.pyxb.assertion import AuthnContextClassRef
        req = AuthnRequest(ForceAuthn=force, IsPassive=passive)
        if authn_context_class is not None:
            req.RequestedAuthnContext = RequestedAuthnContext(
                AuthnContextClassRef(authn_context_class))
        if acs_index is not None:
            req.AttributeConsumingServiceIndex = acs_index
        self.customize_authn_request(req)
        relay_state = self.store((req.ID, ok, fail))
        nip = NameIDPolicy(AllowCreate=self.allow_create)
        nifs = INameidFormatSupport(self).supported
        if len(nifs) == 1: nip.Format = nifs[0]
        req.NameIDPolicy = nip
        return self.deliver(
            Target(
                eid=idp,
                role="idpsso",
                endpoint="SingleSignOnService",
                sign_msg_attr="WantAuthnRequestsSigned",
                binding="urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST",
            ), None, req, relay_state)

    # optionally overridden by derived classes to customize the request
    def customize_authn_request(self, req):
        pass

    def _process_AuthnStatement(self, subject, s):
        info = dict(
            name_qualifier=subject.NameQualifier,
            nameid_format=subject.Format,
            sp_name_qualifier=subject.SPNameQualifier,
            nameid=subject.value(),
            authn_time=pyxb_to_datetime(as_utc(s.AuthnInstant)),
            valid_until=pyxb_to_datetime(as_utc(s.SessionNotOnOrAfter)),
            session_id=s.SessionIndex,
            authn_context_class=s.AuthnContext.AuthnContextClassRef,
        )
        info["user_id"] = self.format_user_id(info)
        self._set_cookie(self.session_cookie_name, info)

    def _process_AttributeStatement(self, subject, s):
        # build map of known attributes -- we might want to cache this information
        attrs = {}
        for acs in self.objectValues():
            for att in acs.objectValues():
                attrs[(att.format, att.title)] = att
        # now process the attributes
        info = {}
        for att in s.Attribute:
            d = attrs.get(
                (att.NameFormat
                 or normalize_attrname_format("unspecified"), att.Name))
            if d is None: continue  # do not know this attribute
            info[d.getId()] = xs_convert_from_xml(att.AttributeValue,
                                                  d.is_sequence, d.type)
        self._set_cookie(self.attribute_cookie_name, info)
        ## XXX: This currently throws a TypeError because the user_info thing is missing.  Why?  Who knows?
        # user_info = self._get_cookie(self.REQUEST, self.session_cookie_name)
        # notify(SamlUserAuthenticated(user_info['user_id']))

    def get_authentication_session(self, request):
        info = self._get_cookie(request, self.session_cookie_name)
        if info is None: return
        if info["valid_until"] is not None:
            if utcnow() >= info["valid_until"]: return  # no longer valid
        return info

    def invalidate_authentication_session(self, request):
        expire = request.response.expireCookie
        params = self._cookie_params()
        expire(self.session_cookie_name, **params)
        expire(self.attribute_cookie_name, **params)

    def get_attributes(self, request):
        # only called when there is an auth session
        #auths = self.get_authentication_session()
        #if auths is None: return
        return self._get_cookie(request, self.attribute_cookie_name)

    def format_user_id(self, info):
        """format user id from dict *info*.

    Almost surely, the `nameid` key will be used.
    To ensure uniqueness, you might need the `name_qualifier`.
    `nameid_format` may allow specific decisions about
    the user id format.

    The primary purpose of this method is customization.
    Be warned however, that changing the format will invalidate
    existing user references.
    """
        return "%(name_qualifier)s::%(nameid)s" % info

    def _cookie_params(self):
        params = dict(path=self.cookie_path, httpOnly=True)
        if self.cookie_domain: params["domain"] = self.cookie_domain
        return params

    def _set_cookie(self, name, info):
        self.REQUEST.response.setCookie(name, self._encode(info),
                                        **self._cookie_params())

    def _get_cookie(self, req, name):
        return self._decode(req.cookies.get(name))

    def _encode(self, info):
        """encode *info* for use as integrity protected cookie value."""
        s = self._serialize(info)
        key = self.__keys[0]
        if self.encrypt_cookies:
            value = getUtility(IEncryption).encrypt(key, s)
        else:
            value = hmac.new(key, s, digest_module).digest() + s
        return value.encode("base64").replace("\n", "").replace(" ", "")

    def _decode(self, v):
        """decode cookie value *v* into an info object (or `None`)."""
        if not v: return
        v = v.decode("base64")
        deserialize = self._deserialize
        if self.encrypt_cookies:
            decrypt = getUtility(IEncryption).decrypt
            for k in self.__keys:
                # try to decrypt and deserialize
                try:
                    return deserialize(decrypt(k, v))
                except:
                    # probably a wrong key
                    pass
            return None  # unable to understand the cookie
        else:
            dl = hmac.new("", "", digest_module).digest_size
            digest = v[:dl]
            v = v[dl:]
            for k in self.__keys:
                if digest == hmac.new(k, v, digest_module).digest():
                    return deserialize(v)
            return None  # unable to authenticate

    def _serialize(self, obj):
        return compress(dumps(obj, -1), 9)

    def _deserialize(self, s):
        return loads(decompress(s))

    def add_attribute_metadata(self, descriptor):
        from dm.saml2.pyxb.metadata import AttributeConsumingService, \
             RequestedAttribute, ServiceName, ServiceDescription
        from dm.saml2.pyxb.assertion import Attribute
        # check uniqueness of "index" -- should probably happen earlier
        seen_indexes = set()
        for acs in self.objectValues():
            ads = acs.objectValues()
            if not ads: continue
            index = acs.index
            if index in seen_indexes:
                raise ValueError("duplicate acs index %d for sp %s" %
                                 (index, descriptor.getId()))
            seen_indexes.add(index)
            acs_md = AttributeConsumingService(
                ServiceName(acs.title, lang=acs.language),
                index=index,
                isDefault=acs.is_default,
            )
            if acs.description:
                acs_md.ServiceDescription.append(
                    ServiceDescription(acs.description, lang=acs.language))
            for ad in ads:
                acs_md.RequestedAttribute.append(
                    RequestedAttribute(
                        NameFormat=ad.format,
                        Name=ad.title,
                        FriendlyName=ad.id,
                        isRequired=ad.required,
                    ))
            descriptor.AttributeConsumingService.append(acs_md)

    ## key handling
    KEY_LENGTH = 32
    KEY_NUMBER = 5

    def _create_key(self):
        kl = self.KEY_LENGTH
        try:
            from os import urandom
            bytes = urandom(kl)
        except ImportError:  # no "urandom"
            from random import randrange
            bytes = [chr(randrange(256)) for i in range(kl)]
        return "".join(bytes)

    def clear_keys(self):
        del self.__keys[:]

    def new_key(self):
        self.__keys.insert(0, self._create_key())
        del self.__keys[self.KEY_NUMBER:]
Exemplo n.º 14
0
    def checkTheWorld(self):
        # Test constructors
        u = PersistentList()
        u0 = PersistentList(l0)
        u1 = PersistentList(l1)
        u2 = PersistentList(l2)

        uu = PersistentList(u)
        uu0 = PersistentList(u0)
        uu1 = PersistentList(u1)
        uu2 = PersistentList(u2)

        v = PersistentList(tuple(u))
        class OtherList(object):
            def __init__(self, initlist):
                self.__data = initlist
            def __len__(self):
                return len(self.__data)
            def __getitem__(self, i):
                return self.__data[i]
        v0 = PersistentList(OtherList(u0))
        vv = PersistentList("this is also a sequence")

        # Test __repr__
        eq = self.assertEqual

        eq(str(u0), str(l0), "str(u0) == str(l0)")
        eq(repr(u1), repr(l1), "repr(u1) == repr(l1)")
        eq(repr(u2), repr(l2), "repr(u2) == repr(l2)")

        # Test __cmp__ and __len__

        # Py3: No cmp() or __cmp__ anymore.
        if PY2:
            def mycmp(a, b):
                r = cmp(a, b)
                if r < 0: return -1
                if r > 0: return 1
                return r

            all = [l0, l1, l2, u, u0, u1, u2, uu, uu0, uu1, uu2]
            for a in all:
                for b in all:
                    eq(mycmp(a, b), mycmp(len(a), len(b)),
                          "mycmp(a, b) == mycmp(len(a), len(b))")

        # Test __getitem__

        for i in range(len(u2)):
            eq(u2[i], i, "u2[i] == i")

        # Test __setitem__

        uu2[0] = 0
        uu2[1] = 100
        try:
            uu2[2] = 200
        except IndexError:
            pass
        else:
            self.fail("uu2[2] shouldn't be assignable")

        # Test __delitem__

        del uu2[1]
        del uu2[0]
        try:
            del uu2[0]
        except IndexError:
            pass
        else:
            self.fail("uu2[0] shouldn't be deletable")

        # Test __getslice__

        for i in range(-3, 4):
            eq(u2[:i], l2[:i], "u2[:i] == l2[:i]")
            eq(u2[i:], l2[i:], "u2[i:] == l2[i:]")
            for j in range(-3, 4):
                eq(u2[i:j], l2[i:j], "u2[i:j] == l2[i:j]")

        # Test __setslice__

        for i in range(-3, 4):
            u2[:i] = l2[:i]
            eq(u2, l2, "u2 == l2")
            u2[i:] = l2[i:]
            eq(u2, l2, "u2 == l2")
            for j in range(-3, 4):
                u2[i:j] = l2[i:j]
                eq(u2, l2, "u2 == l2")

        uu2 = u2[:]
        uu2[:0] = [-2, -1]
        eq(uu2, [-2, -1, 0, 1], "uu2 == [-2, -1, 0, 1]")
        uu2[0:] = []
        eq(uu2, [], "uu2 == []")

        # Test __contains__
        for i in u2:
            self.assertTrue(i in u2, "i in u2")
        for i in min(u2)-1, max(u2)+1:
            self.assertTrue(i not in u2, "i not in u2")

        # Test __delslice__

        uu2 = u2[:]
        del uu2[1:2]
        del uu2[0:1]
        eq(uu2, [], "uu2 == []")

        uu2 = u2[:]
        del uu2[1:]
        del uu2[:1]
        eq(uu2, [], "uu2 == []")

        # Test __add__, __radd__, __mul__ and __rmul__

        #self.assertTrue(u1 + [] == [] + u1 == u1, "u1 + [] == [] + u1 == u1")
        self.assertTrue(u1 + [1] == u2, "u1 + [1] == u2")
        #self.assertTrue([-1] + u1 == [-1, 0], "[-1] + u1 == [-1, 0]")
        self.assertTrue(u2 == u2*1 == 1*u2, "u2 == u2*1 == 1*u2")
        self.assertTrue(u2+u2 == u2*2 == 2*u2, "u2+u2 == u2*2 == 2*u2")
        self.assertTrue(u2+u2+u2 == u2*3 == 3*u2, "u2+u2+u2 == u2*3 == 3*u2")

        # Test append

        u = u1[:]
        u.append(1)
        eq(u, u2, "u == u2")

        # Test insert

        u = u2[:]
        u.insert(0, -1)
        eq(u, [-1, 0, 1], "u == [-1, 0, 1]")

        # Test pop

        u = PersistentList([0, -1, 1])
        u.pop()
        eq(u, [0, -1], "u == [0, -1]")
        u.pop(0)
        eq(u, [-1], "u == [-1]")

        # Test remove

        u = u2[:]
        u.remove(1)
        eq(u, u1, "u == u1")

        # Test count
        u = u2*3
        eq(u.count(0), 3, "u.count(0) == 3")
        eq(u.count(1), 3, "u.count(1) == 3")
        eq(u.count(2), 0, "u.count(2) == 0")


        # Test index

        eq(u2.index(0), 0, "u2.index(0) == 0")
        eq(u2.index(1), 1, "u2.index(1) == 1")
        try:
            u2.index(2)
        except ValueError:
            pass
        else:
            self.fail("expected ValueError")

        # Test reverse

        u = u2[:]
        u.reverse()
        eq(u, [1, 0], "u == [1, 0]")
        u.reverse()
        eq(u, u2, "u == u2")

        # Test sort

        u = PersistentList([1, 0])
        u.sort()
        eq(u, u2, "u == u2")

        # Test extend

        u = u1[:]
        u.extend(u2)
        eq(u, u1 + u2, "u == u1 + u2")
Exemplo n.º 15
0
class NyPortalLanguageManager(Persistent):
    """
    Portal_i18n has an instance of this type, accessible by *get_lang_manager()*
    method. It supplies add/edit/remove/set_default operations with languages
    available in portal and it is also used to get current available
    languages, default language and manage display order.

    """
    implements(INyLanguageManagement)

    # by default, the display order is the creation order, default lang first
    custom_display_order = None

    def __init__(self, default_langs=[('en', 'English')]):
        if not isinstance(default_langs, list):
            raise ValueError("Default languages must be a list of touples"
                             " (code, name)")
        self.portal_languages = PersistentList(default_langs)

    def getAvailableLanguages(self):
        """Return a sequence of language tags/codes for available languages
        """
        if self.custom_display_order is None:
            return tuple([ x[0] for x in self.portal_languages ])
        else:
            return tuple(self.custom_display_order)

    def addAvailableLanguage(self, lang_code, lang_name=None):
        """Adds available language in portal"""
        lang_code = normalize_code(lang_code)
        if not lang_name:
            lang_name = get_iso639_name(lang_code)
        if lang_code not in self.getAvailableLanguages():
            self.portal_languages.append((lang_code, lang_name))
            self.set_display_order()

    def delAvailableLanguage(self, lang):
        """
        Deletes specified language from available languages list in portal

        """
        lang = normalize_code(lang)
        available = [x[0] for x in self.portal_languages]
        if lang in available:
            if len(available) == 1:
                raise ValueError("Can not delete the only available language")
            else:
                pos = available.index(lang)
                self.portal_languages.pop(pos)
                self.set_display_order()

    # MORE:
    def set_display_order(self, operation=None):
        """
        `operation` is 'x-y' where x and y are consecutive indices
        about to be switched.
        If operation is None, custom display order is "refreshed" if it is
        defined and if there were any changes in available portal languages.

        """
        if self.custom_display_order is None and operation is None:
            # no operation, no custom order
            return

        creation_order = [x[0] for x in self.portal_languages]

        if operation is None:
            # explore for changes - new added lang or removed language
            # custom_display_order obviously not None
            added = set(creation_order) - set(self.custom_display_order)
            rmed = set(self.custom_display_order) - set(creation_order)
            self.custom_display_order.extend(list(added))
            for r in rmed:
                self.custom_display_order.remove(r)
        else:
            # we have a "move operation" request
            if self.custom_display_order is None:
                self.custom_display_order = PersistentList(creation_order)
            switch = map(int, operation.split("-"))
            assert((switch[0]-switch[1])**2 == 1)
            acc = self.custom_display_order.pop(switch[0])
            self.custom_display_order.insert(switch[1], acc)

    def set_default_language(self, lang):
        """
        Sets default language in language manager. Default language
        is mainly used in negotiation. Also rearranges langs order: first
        is default, the rest are sorted alphabetically.

        """
        lang = normalize_code(lang)
        if lang not in self.getAvailableLanguages():
            raise ValueError("Language %s is not provided by portal" % lang)
        available = [x[0] for x in self.portal_languages]
        if len(available)==1:
            return
        pos = available.index(lang)
        new_default = self.portal_languages.pop(pos)
        self.portal_languages.insert(0, new_default)
        self.set_display_order()

    def get_default_language(self):
        """ Returns default language """
        return self.portal_languages[0][0]

    def get_language_name(self, code):
        """
        Returns the name of a language available in portal, '???' otherwise.

        """
        available = list(self.getAvailableLanguages())
        if code in available:
            pos = [lcode for lcode, name in self.portal_languages].index(code)
            return self.portal_languages[pos][1]
        else:
            return "???"
Exemplo n.º 16
0
class PersistentOrderedContainer(PersistentContainer):
    def __init__(self, name, parent=None):
        PersistentContainer.__init__(self, name, parent)
        self.__children = PersistentList()

    def index(self, item):
        return self.__children.index(item)

    def get_children(self, index):
        return self.__children[index]

    @property
    def children(self):
        return list(self.__children)

    @children.setter
    def children(self, children):
        if len(children) != len(self):
            raise ValueError('len(children) and len(self) must be equal')
        for child in children:
            if not child.name in self:
                raise ValueError('children and self must ' \
                                 'contain the same objects')
        self.__children = PersistentList(children)

    def add(self, item):
        if item.__name__ not in self:
            self.__children.append(item)
        else:
            raise ValueError('The container already contains this item')
        PersistentContainer.add(self, item)

    append = add

    def insert(self, index, item):
        if item.__name__ not in self:
            self.__children.insert(index, item)
        else:
            raise ValueError('The container already contains this item')
        PersistentContainer.append(self, item)

    def __setitem__(self, name, item):
        already_in_children = name in self
        PersistentContainer.__setitem__(self, name, item)
        if not already_in_children:
            self.__children.append(item)

    def __delitem__(self, name):
        if name in self:
            self.__children.remove(self[name])
        PersistentContainer.__delitem__(self, name)

    def __iter__(self):
        return self.iterkeys()

    def keys(self):
        return [child.name for child in self.__children]

    def values(self):
        return [child for child in self.__children]

    def items(self):
        return [(child.name, child) for child in self.__children]

    def iterkeys(self):
        for child in self.__children:
            yield child.name

    def itervalues(self):
        for child in self.__children:
            yield child

    def iteritems(self):
        for child in self.__children:
            yield child.name, child
Exemplo n.º 17
0
class PersistentOrderedContainer(PersistentContainer):
    def __init__(self, name, parent=None):
        PersistentContainer.__init__(self, name, parent)
        self.__children = PersistentList()

    def index(self, item):
        return self.__children.index(item)

    def get_children(self, index):
        return self.__children[index]

    @property
    def children(self):
        return list(self.__children)

    @children.setter
    def children(self, children):
        if len(children) != len(self):
            raise ValueError('len(children) and len(self) must be equal')
        for child in children:
            if not child.name in self:
                raise ValueError('children and self must ' \
                                 'contain the same objects')
        self.__children = PersistentList(children)

    def add(self, item):
        if item.__name__ not in self:
            self.__children.append(item)
        else:
            raise ValueError('The container already contains this item')
        PersistentContainer.add(self, item)
    append = add

    def insert(self, index, item):
        if item.__name__ not in self:
            self.__children.insert(index, item)
        else:
            raise ValueError('The container already contains this item')
        PersistentContainer.append(self, item)

    def __setitem__(self, name, item):
        already_in_children = name in self
        PersistentContainer.__setitem__(self, name, item)
        if not already_in_children:
            self.__children.append(item)

    def __delitem__(self, name):
        if name in self:
            self.__children.remove(self[name])
        PersistentContainer.__delitem__(self, name)

    def __iter__(self):
        return self.iterkeys()

    def keys(self):
        return [child.name for child in self.__children]

    def values(self):
        return [child for child in self.__children]

    def items(self):
        return [(child.name, child) for child in self.__children]

    def iterkeys(self):
        for child in self.__children:
            yield child.name

    def itervalues(self):
        for child in self.__children:
            yield child

    def iteritems(self):
        for child in self.__children:
            yield child.name, child
Exemplo n.º 18
0
class RecordContainer(Persistent):
    """
    Base/default record container uses PersistentDict for entry storage
    and PersistentList to store ordered keys.  This base container class
    does not advocate one place of storage for the container in a ZODB
    over another, so subclass implementations may choose to implement a
    container within a placeful (e.g. OFS or CMF Content item) or placeless
    (local utility) storage context.  Only a placeless context is supported
    by direct users of this class (without subclassing).

    For a container with hundreds of items or more, consider using instead
    BTreeRecordContainer as an implementation or base class, as it should
    handle memory usage and insert performance much better for larger sets
    of records.

    Usage
    -----

    RecordContainer acts as CRUD controller for working with records.

    The RecordContainer is an addressible object in the system, either as a
    registered utility (or with a subclass as "contentish" (CMF) content).

    Records themselves are not content, but data that are possibly
    non-atomic elements of an atomic content item (if the container is
    implemented in a subclass of RecordContainer as contentish).

    Usage:
    ------

    We need a record container object:

    >>> from uu.record.base import Record, RecordContainer
    >>> container = RecordContainer()
    >>> from uu.record.interfaces import IRecordContainer
    >>> assert IRecordContainer.providedBy(container)

    Record containers have length and containment checks:

    >>> assert len(container) == 0
    >>> import uuid  # keys for entries are stringified UUIDs
    >>> randomuid = str(uuid.uuid4())
    >>> assert randomuid not in container
    >>> assert container.get(randomuid, None) is None

    And they have keys/values/items methods like a mapping:

    >>> assert container.keys() == ()
    >>> assert container.values() == ()
    >>> assert container.items() == ()  # of course, these are empty now.

    Before we add records to a container, we need to create them; there are
    two possible ways to do this:

    >>> from uu.record.base import Record
    >>> entry1 = Record()
    >>> entry2 = container.create()  # preferred factory

    Both factory mechanisms create an entry item with a record_uid attribute:

    >>> from uu.record.interfaces import IRecord
    >>> assert IRecord.providedBy(entry1)
    >>> assert IRecord.providedBy(entry2)
    >>> is_uuid = lambda u: isinstance(u, str) and len(u) == 36
    >>> assert is_uuid(entry1.record_uid)
    >>> assert is_uuid(entry2.record_uid)

    And, these are RFC 4122 UUIDs, so even randomly generated 128-bit ids
    have near zero chance of collision:

    >>> assert entry1.record_uid != entry2.record_uid
    >>> assert entry2.record_uid != randomuid

    The record objects provide plone.uuid.interfaces.IAttributeUUID as an
    alternative way to get the UUID value (string representation) by
    adapting to IUUID:

    >>> from zope.configuration import xmlconfig
    >>> import plone.uuid
    >>> c = xmlconfig.file('configure.zcml', plone.uuid)  # load registrations
    >>> from plone.uuid.interfaces import IUUID, IAttributeUUID
    >>> from zope.component import queryAdapter
    >>> assert IAttributeUUID.providedBy(entry1)
    >>> assert queryAdapter(entry1, IUUID) is not None
    >>> assert queryAdapter(entry1, IUUID) == entry1.record_uid

    Now when we have a parent context with a schema, the created entries will
    be signed with the schema and provide it.

    RecordContainer.create() is the preferred factory when processing data.
    This is because it can take a mapping of keys/values, and copy each
    field name/value onto object attributes -- if and only if the attribute
    in question matches a type whitelist and a name blacklist filter.

    >>> entry4 = container.create(data={'record_uid':randomuid})
    >>> assert entry4.record_uid == randomuid
    >>> entry5 = container.create(data={'count':5})
    >>> assert entry5.count == 5
    >>> entry6 = container.create(data={'_bad_name'    : True,
    ...                                  'count'        : 2,
    ...                                  'bad_value'    : lambda x: x })
    >>> assert not hasattr(entry6, '_bad_name')  # no leading underscores
    >>> assert entry6.count == 2
    >>> assert not hasattr(entry6, 'bad_value')  # function not copied!

    Of course, merely using the record container object as a factory for
    new records does not mean they are stored within (yet):

    >>> assert entry4.record_uid not in container
    >>> assert entry4.record_uid not in container.keys()

    Let's add an item:

    >>> container.add(entry4)

    There are two ways to check for containment, by either key or value:

    >>> assert entry4 in container
    >>> assert entry4.record_uid in container

    We can get records using a (limited, read) mapping-like interface:

    >>> assert len(container) == 1  # we just added the first entry
    >>> assert container.values()[0] is entry4
    >>> assert container.get(entry4.record_uid) is entry4
    >>> assert container[entry4.record_uid] is entry4

    We can deal with references to entries also NOT in the container:

    >>> import uuid
    >>> randomuid = str(uuid.uuid4())
    >>> assert randomuid not in container
    >>> assert container.get(str(uuid.uuid4()), None) is None
    >>> assert entry1.record_uid not in container

    And we can check containment on either an instance or a UID; checking on
    an instance is just a convenience that uses its UID (record_uid) field
    to check for actual containment:

    >>> assert entry4.record_uid in container
    >>> assert entry4 in container  # shortcut!

    However, it should be noted for good measure:

    >>> assert entry4 in container.values()
    >>> assert entry4.record_uid in container.keys()
    >>> assert entry4 not in container.keys()  # of course!
    >>> assert (entry4.record_uid, entry4) in container.items()

    We can modify a record contained directly; this is the most direct and
    low-level update interface for any entry:

    >>> _marker = object()
    >>> assert getattr(entry4, 'title', _marker) is _marker
    >>> entry4.title = u'Curious George'
    >>> assert container.get(entry4.record_uid).title == u'Curious George'

    We can add another record:

    >>> container.add(entry6)
    >>> assert entry6 in container
    >>> assert entry6.record_uid in container
    >>> assert len(container) == 2

    Keys, values, items are always ordered; since we added entry4, then
    entry6 previously, they will return in that order:

    >>> expected_order = (entry4, entry6)
    >>> expected_uid_order = tuple([e.record_uid for e in expected_order])
    >>> expected_items_order = tuple(zip(expected_uid_order, expected_order))
    >>> assert tuple(container.keys()) == expected_uid_order
    >>> assert tuple(container.values()) == expected_order
    >>> assert tuple(container.items()) == expected_items_order

    We can re-order this; let's move entry6 up to position 0 (first):

    >>> container.reorder(entry6, offset=0)
    >>> expected_order = (entry6, entry4)
    >>> expected_uid_order = tuple([e.record_uid for e in expected_order])
    >>> expected_items_order = tuple(zip(expected_uid_order, expected_order))
    >>> assert tuple(container.keys()) == expected_uid_order
    >>> assert tuple(container.values()) == expected_order
    >>> assert tuple(container.items()) == expected_items_order

    We can also re-order by UID instead of record/entry reference:

    >>> container.reorder(entry6.record_uid, offset=1)  # where it was before
    >>> expected_order = (entry4, entry6)
    >>> expected_uid_order = tuple([e.record_uid for e in expected_order])
    >>> expected_items_order = tuple(zip(expected_uid_order, expected_order))
    >>> assert tuple(container.keys()) == expected_uid_order
    >>> assert tuple(container.values()) == expected_order
    >>> assert tuple(container.items()) == expected_items_order

    And we can remove records from containment by UID or by reference (note,
    del(container[key]) uses __delitem__ since a container is a writable
    mapping):

    >>> del(container[entry6])
    >>> assert entry6 not in container
    >>> assert entry6.record_uid not in container
    >>> assert len(container) == 1
    >>> assert entry4 in container
    >>> del(container[entry4.record_uid])
    >>> assert entry4 not in container
    >>> assert len(container) == 0

    Earlier, direct update of objects was demonstrated: get an object and
    modify its properties.  This attribute-setting mechanism is the best
    low-level interface, but it does not (a) support a wholesale update
    from either a field dictionary/mapping nor another object providing
    IRecord needing its data to be copied; nor (b) support notification
    of zope.lifecycle object events.

    Given these needs, a high level interface for update exists, with the
    record object acting as a controller for updating contained entries.
    This provides for update via another entry (a field-by-field copy) or
    from a data dictionary/mapping.

    >>> newuid = str(uuid.uuid4())
    >>> data = {    'record_uid' : newuid,
    ...             'title'      : u'George',
    ...             'count'      : 9,
    ...        }
    >>> assert len(container) == 0  # empty, nothing in there yet!
    >>> assert newuid not in container

    Note, update() returns an entry; return value can be ignored if caller
    deems it not useful.

    >>> entry = container.update(data)
    >>> assert newuid in container  # update implies adding!
    >>> assert entry is container.get(newuid)
    >>> assert entry.title == data['title']
    >>> assert entry.count == data['count']

    Now, the entry we just modified was also added.  We can modify it again:

    >>> data = {    'record_uid' : newuid,
    ...             'title'      : u'Curious George',
    ...             'count'      : 2,
    ...        }
    >>> entry = container.update(data)
    >>> assert newuid in container     # same uid
    >>> entry.title
    u'Curious George'
    >>> entry.count
    2
    >>> assert len(container) == 1     # same length, nothing new was added.

    We could also create a stand-in entry for which data is copied to the
    permanent entry with the same UUID on update:

    >>> temp_entry = container.create()
    >>> temp_entry.record_uid = newuid      # overwrite with the uid of entry
    >>> temp_entry.title = u'Monkey jumping on the bed'
    >>> temp_entry.count = 0

    temp_entry is a stand-in which we will pass to update(), when we really
    intend to modify entry (they have the same UID):

    >>> real_entry = container.update(temp_entry)
    >>> assert container.get(newuid) is not temp_entry
    >>> assert container.get(newuid) is entry  # still the same object...
    >>> assert container.get(newuid) is real_entry
    >>> entry.title                             # ...but data is modified!
    u'Monkey jumping on the bed'
    >>> entry.count
    0
    >>> assert len(container) == 1     # same length, nothing new was added.


    JSON integration
    ----------------

    As a convenience, update_all() parses JSON into a data dict for use by
    update(), using the Python 2.6 json library (aka/was: simplejson):

    >>> party_form = RecordContainer()
    >>> entry = party_form.create()
    >>> party_form.add(entry)
    >>> data = {  # mock data we'll serialize to JSON
    ...     'record_uid': entry.record_uid,  # which record to update
    ...     'name'      : 'Me',
    ...     'birthday'  : u'77/06/01',
    ...     'party_time': u'11/06/05 12:00',
    ...     }
    >>> import json  # requires Python >= 2.6
    >>> data['name'] = 'Chunky monkey'
    >>> serialized = json.dumps([data,], indent=2)  # JSON array of one item...
    >>> print serialized  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
    [
      {
        "party_time": "11/06/05 12:00",
        "birthday": "77/06/01",
        "name": "Chunky monkey",
        "record_uid": "..."
      }
    ]

    The JSON created above is useful enough for demonstration, despite being
    only a single-item list.

    >>> assert getattr(entry, 'name', _marker) is _marker  # before, no attr
    >>> party_form.update_all(serialized)
    >>> entry.name  # after update
    u'Chunky monkey'

    update_all() also takes a singular record, not just a JSON array:

    >>> data['name'] = 'Curious George'
    >>> serialized = json.dumps(data, indent=2)  # JSON object, not array.
    >>> print serialized  # doctest: +ELLIPSIS, +NORMALIZE_WHITESPACE
    {
      "party_time": "11/06/05 12:00",
      "birthday": "77/06/01",
      "name": "Curious George",
      "record_uid": "..."
    }
    >>> entry.name  # before
    u'Chunky monkey'
    >>> party_form.update_all(serialized)
    >>> entry.name  # after update
    u'Curious George'

    JSON parsing also supports a "bundle" or wrapper object around a list of
    entries, where the wrapper contains metadata about the form itself, not
    its entries (currently, this is just the process_changes field, which
    is sourced from the JSON bundle/wrapper object field called 'notes').
    When wrapped, the list of entries is named 'entries' inside the wrapper.

    >>> data['name'] = u'Party monkey'
    >>> serialized = json.dumps({'notes'    : 'something changed',
    ...                          'entries'  : [data,]},
    ...                         indent=2)  # JSON array of one item...
    >>> entry.name  # before
    u'Curious George'
    >>> party_form.update_all(serialized)
    >>> entry.name  # after
    u'Party monkey'

    It should be noted that update_all() removes entries not in the data
    payload, and it preserves the order contained in the JSON entries.

    Object events
    -------------

    CRUD methods on a controlling object should have some means of extension,
    pluggable to code that should subscribe to CRUD (object lifecycle) events.
    We notify four distinct zope.lifecycleevent object event types:

    1. Object created (zope.lifecycleevent.interfaces.IObjectCreatedEvent)

    2. Object addded to container:
        (zope.lifecycleevent.interfaces.IObjectAddedEvent).

    3. Object modified (zope.lifecycleevent.interfaces.IObjectModifiedEvent)

    4. Object removed (zope.lifecycleevent.interfaces.IObjectRemovedEvent)

    Note: the create() operation both creates and modifies: as such, both
    created and modified events are fired off, and since most creations also
    are followed by an add() to a container, you may have three events to
    subscribe to early in a new entry's lifecycle.

    First, some necessary imports of events and the @adapter decorator:

    >>> from zope.component import adapter
    >>> from zope.lifecycleevent import IObjectCreatedEvent
    >>> from zope.lifecycleevent import IObjectModifiedEvent
    >>> from zope.lifecycleevent import IObjectRemovedEvent
    >>> from zope.lifecycleevent import IObjectAddedEvent

    Let's define dummy handlers:

    >>> @adapter(IRecord, IObjectCreatedEvent)
    ... def handle_create(context, event):
    ...     print 'object created'
    ...
    >>> @adapter(IRecord, IObjectModifiedEvent)
    ... def handle_modify(context, event):
    ...     print 'object modified'
    ...
    >>> @adapter(IRecord, IObjectRemovedEvent)
    ... def handle_remove(context, event):
    ...     print 'object removed'
    ...
    >>> @adapter(IRecord, IObjectAddedEvent)
    ... def handle_add(context, event):
    ...     print 'object added'
    ...

    Next, let's configure zope.event to use zope.component event
    subscribers; most frameworks using zope.lifecycleevent already do
    this, but we will configure this explicitly for documentation
    and testing purposes, only if not already enabled:

    >>> import zope.event
    >>> from zope.component import getGlobalSiteManager
    >>> gsm = getGlobalSiteManager()

    Importing zope.component.event puts dispatch() in zope.event.subscribers:

    >>> from zope.component import event
    >>> assert event.dispatch in zope.event.subscribers
    
    Now, let's register the handlers:

    >>> for h in (handle_create, handle_modify, handle_remove, handle_add):
    ...     gsm.registerHandler(h)
    ...

    Usually, these handlers will be registered in the global site manager
    via ZCML and zope.configuration, but they are registered in Python
    above for documentation/testing purposes.

    We can watch these event handlers get fired when CRUD methods are called.

    Object creation, with and without data:

    >>> newentry = container.create()      # should print 'object created'
    object created
    >>> another_uid = str(uuid.uuid4())
    >>> newentry = container.create({'count':88})
    object modified
    object created

    Object addition:

    >>> container.add(newentry)
    object added
    >>>

    Object removal:

    >>> del(container[newentry.record_uid])  # via __delitem__()
    object removed

    Object update (existing object):

    >>> entry = container.values()[0]
    >>> entry = container.update({'record_uid' : entry.record_uid,
    ...                            'title'      : u'Me'})
    object modified

    Object modified (new object or not contained):

    >>> random_uid = str(uuid.uuid4())
    >>> entry = container.update({'record_uid' : random_uid,
    ...                            'title'      : u'Bananas'})
    object modified
    object created
    object added

    Event handlers for modification can know what fields are modified; let's
    create a more interesting modification handler that prints the names of
    changed fields.

    >>> from zope.lifecycleevent.interfaces import IAttributes
    >>> unregistered = gsm.unregisterHandler(handle_modify)
    >>> @adapter(IRecord, IObjectModifiedEvent)
    ... def handle_modify(context, event):
    ...     if event.descriptions:
    ...         attr_desc = [d for d in event.descriptions
    ...                         if (IAttributes.providedBy(d))]
    ...         if attr_desc:
    ...             field_names = attr_desc[0].attributes
    ...         print tuple(field_names)
    >>> gsm.registerHandler(handle_modify)

    >>> entry = container.values()[0]
    >>> entry = container.update({'record_uid' : entry.record_uid,
    ...                            'title'      : u'Hello'})
    ('title',)

    Finally, clean up and remove all the dummy handlers:
    >>> for h in (handle_create, handle_modify, handle_remove, handle_add):
    ...     success = gsm.unregisterHandler(h)
    ...

    """

    implements(IRecordContainer)

    # whitelist types of objects to copy on data update:

    TYPE_WHITELIST = (int,
                      long,
                      str,
                      unicode,
                      bool,
                      float,
                      time.time,
                      datetime,
                      date,
                      timedelta,
                      decimal.Decimal,)

    SEQUENCE_WHITELIST = (list, tuple, set, frozenset, PersistentList,)

    MAPPING_WHITELIST = (dict, PersistentDict,)

    RECORD_INTERFACE = IRecord

    factory = Record

    def __init__(self, factory=Record, _impl=PersistentDict):
        self._entries = _impl()
        self._order = PersistentList()
        self.factory = factory

    # IWriteContainer methods:

    def _update_size(self):
        self._size = len(self._order)
        self._p_changed = True

    def __setitem__(self, key, value):
        if isinstance(key, uuid.UUID) or isinstance(key, unicode):
            key = str(key)
        elif not (isinstance(key, str) and len(key) == 36):
            raise KeyError('key does not appear to be string UUID: %s', key)
        if not self.RECORD_INTERFACE.providedBy(value):
            raise ValueError('Record value must provide %s' % (
                self.RECORD_INTERFACE.__identifier__))
        self._entries[key] = value
        if key not in self._order:
            self._order.append(key)
            self._update_size()

    def __delitem__(self, record):
        uid = record
        if self.RECORD_INTERFACE.providedBy(record):
            uid = str(record.record_uid)
        elif isinstance(record, uuid.UUID):
            uid = str(record)
        if not (isinstance(uid, str) and len(uid) == 36):
            raise ValueError('record neither record object nor UUID')
        if uid not in self._entries:
            raise ValueError('record not found contained within')
        if uid in self._order:
            self._order.remove(uid)
            self._update_size()
        if not self.RECORD_INTERFACE.providedBy(record):
            record = self._entries.get(uid)  # need ref for event notify below
        del(self._entries[uid])
        notify(ObjectRemovedEvent(record, self, uid))

    # IRecordContainer and IOrdered re-ordering methods:

    def reorder(self, record, offset):
        """
        Reorder a record (either UUID or object with record_uid attribute)
        in self._order, if record exists.  If no UUID exists in self._order,
        raise a ValueError.  Offset must be non-negative integer.
        """
        uid = record
        offset = abs(int(offset))
        if self.RECORD_INTERFACE.providedBy(record):
            uid = record.record_uid
        if not uid or uid not in self._order:
            raise ValueError('cannot find record to move for id %s' % uid)
        self._order.insert(offset, self._order.pop(self._order.index(uid)))

    def updateOrder(self, order):
        """Provides zope.container.interfaces.IOrdered.updateOrder"""
        if len(order) != len(self._order):
            raise ValueError('invalid number of keys')
        s_order = set(order)
        if len(order) != len(s_order):
            raise ValueError('duplicate keys in order')
        if s_order - set(self._order):
            raise ValueError('unknown key(s) provided in order')
        if not isinstance(order, PersistentList):
            order = PersistentList(order)
        self._order = order

    # IReadContainer interface methods:

    def get(self, uid, default=None):
        """
        Get object providing IRecord for given UUID uid or return None
        """
        if self.RECORD_INTERFACE.providedBy(uid):
            uid = uid.record_uid   # special case to support __contains__() impl
        v = self._entries.get(str(uid), default)
        if v and getattr(v, '_v_parent', None) is None:
            v._v_parent = self  # container marks item with itself as context
        return v

    def __contains__(self, record):
        """
        Given record as either IRecord object or UUID, is record contained?
        """
        if self.RECORD_INTERFACE.providedBy(record):
            return self.get(record, None) is not None
        return str(record) in self._entries

    def __len__(self):
        """
        return length of record entries
        """
        size = getattr(aq_base(self), '_size', None)
        return size if size is not None else len(self._order)

    def __getitem__(self, key):
        """Get item by UID key"""
        v = self.get(key, None)
        if v is None:
            raise KeyError('unknown UID for record entry')
        return v

    def keys(self):
        """return tuple with elements ordered"""
        return tuple(self._order)

    def values(self):
        """return tuple of records in order"""
        return tuple([t[1] for t in self.items()])

    def items(self):
        """return ordered pairs of key/values"""
        return tuple([(uid, self.get(uid)) for uid in self._order])

    def __iter__(self):
        return self._order.__iter__()

    # IRecordContainer-specific CRUD methods:

    def _type_whitelist_validation(self, value):
        vtype = type(value)
        if vtype in self.MAPPING_WHITELIST:
            for k, v in value.items():
                if not (k in self.TYPE_WHITELIST and v in self.TYPE_WHITELIST):
                    raise ValueError('Unsupported mapping key/value type')
        elif vtype in self.SEQUENCE_WHITELIST:
            for v in value:
                if v not in self.TYPE_WHITELIST:
                    raise ValueError('Unsupported sequence value type')
        else:
            if vtype not in self.TYPE_WHITELIST:
                raise ValueError('Unsupported data type')

    def _populate_record(self, record, data):
        """
        Given mapping of data, copy values to attributes on record.

        Subclasses may override to provide schema validation, selective
        copy of names, and normalization of values if/as necessary.
        """
        changelog = []
        for key, value in data.items():
            if key.startswith('_'):
                continue  # invalid key
            if key == 'record_uid':
                self.record_uid = str(value)
                continue
            try:
                self._type_whitelist_validation(value)
            except ValueError:
                continue  # skip problem name!
            existing_value = getattr(self, key, None)
            if value != existing_value:
                changelog.append(key)
                setattr(record, key, value)
        if changelog:
            record._p_changed = True
            changelog = [
                Attributes(self.RECORD_INTERFACE, name)
                for name in changelog
                ]
            notify(ObjectModifiedEvent(record, *changelog))

    def create(self, data=None):
        """
        Alternative factory for an IRecord object, does not store object.
        If data is not None, copy fields from data.
        """
        if data is None:
            data = {}
        uid = data.get('record_uid', str(uuid.uuid4()))  # get or random uuid
        record = self.factory(context=self, uid=uid)
        if data and (hasattr(data, 'get') and
                     hasattr(data, 'items')):
            self._before_populate(record, data)
            self._populate_record(record, data)
        notify(ObjectCreatedEvent(record))
        return record

    def add(self, record):
        """
        Add a record to container, append UUID to end of order; over-
        write existing entry if already exists for a UUID (in such case
        leave order as-is).
        """
        uid = str(record.record_uid)
        if not uid:
            raise ValueError('record has empty UUID')
        self._entries[uid] = record
        if uid not in self._order:
            self._order.append(uid)
            self._update_size()
        notify(ObjectAddedEvent(record, self, uid))

    def _ad_hoc_fieldlist(self, record):
        attrs = [name for name in dir(record) if not name.startswith('_')]
        fieldnames = []
        for name in attrs:
            v = getattr(record, name)
            try:
                self._type_whitelist_validation(v)
                fieldnames.append(name)
            except ValueError:
                pass  # ignore name
        return fieldnames

    def _filtered_data(self, data):
        fieldnames = self._ad_hoc_fieldlist(data)
        if IRecord.providedBy(data):
            return dict([(k, getattr(data, k, None)) for k in fieldnames])
        return dict([(k, data.get(k, None)) for k in fieldnames])

    def _before_populate(self, record, data):
        pass  # hook for subclasses

    def _before_update_notification(self, record, data):
        pass  # hook for subclasses

    def notify_data_changed(self):
        notify(
            ObjectModifiedEvent(self, Attributes(IRecordContainer, 'items'))
            )

    def update(self, data, suppress_notify=False):
        """
        Given data, which may be a dict of field key/values or an actual
        IRecord providing object, update existing entry given a UUID, or
        add the entry if an entry for that UUID does not yet exist.  The
        update should copy all values for every key provided.  Specialized
        or schema-bound subclasses of this interface may execute more
        elaborate rules on what data is copied and how it is normalized.

        Pre-condition:

          * All new (added) entries updated this way must contain a record_uid
            field with a string UUID.

        Post-condition:

          * New items should always be handled through self.create() and then
            self.add().

          * Method returns modified record.

          * Should notify at least zope.lifecycleevent.IObjectModifiedEvent,
            (if changes, detection of which is left up to implementation).

          * On creation of new records, should notify both
            IObjectCreatedEvent and IObjectAddedEvent (the record container
            is the context of record).

        """
        if self.RECORD_INTERFACE.providedBy(data):
            uid = data.record_uid
            data = self._filtered_data(data)
        else:
            uid = data.get('record_uid', None)
        if uid is None:
            raise ValueError('empty record UID on update')
        uid = str(uid)
        record = self.get(uid, None)
        if record is not None:
            # existing record, already known/saved
            self._before_populate(record, data)
            self._populate_record(record, data)  # also notifies modified event
        else:
            # new, create, then add
            record = self.create(data)  # notifies created, modified for record
            self.add(record)            # notified added event
        self._before_update_notification(record, data)
        if (not suppress_notify) and getattr(record, '_p_changed', None):
            self.notify_data_changed()
        return record

    def _process_container_metadata(self, data):
        return False  # hook for subclasses

    def update_all(self, data):
        """
        Given sequence of data dictionaries or a JSON serialization
        thereof, update each item.  Raises ValueError on missing UID of
        any item/entry.  Also supports JSON serialization of a single
        record/entry dict.
        """
        _modified = False
        if isinstance(data, basestring):
            _data = json.loads(data)
            if isinstance(_data, dict):
                # dict might be singluar item, or wrapping object; a wrapping
                # object would have a list called 'entries'
                if 'entries' in _data and isinstance(_data['entries'], list):
                    _modified = self._process_container_metadata(_data)
                    # wrapper, get entries from within.
                    _data = _data['entries']
                else:
                    # singular record, not a wrapper
                    _data = [_data]  # wrap singular item update in list
            _keynorm = lambda o: dict([(str(k), v) for k, v in o.items()])
            data = [_keynorm(o) for o in _data]
        uids = [str(o['record_uid']) for o in data]
        existing_uids = set(self.keys())
        added_uids = set(uids) - existing_uids
        modified_uids = set(uids).intersection(existing_uids)
        for entry_data in data:
            if 'record_uid' not in entry_data:
                raise ValueError('record missing UID')
            record = self.update(entry_data, suppress_notify=True)
            if not _modified and getattr(record, '_p_changed', None):
                _modified = True
        remove_uids = existing_uids - set(uids)
        for deluid in remove_uids:
            del(self[deluid])  # remove any previous entries not in the form
        self._order = PersistentList(uids)  # replace old with new uid order
        if added_uids or modified_uids:
            _modified = True
        if data and _modified:
            self.notify_data_changed()  # notify just once