""" open/dulcinea/lib/category.py """ from dulcinea.base import DulcineaPersistent from durus.persistent import ComputedAttribute from durus.persistent_dict import PersistentDict from durus.persistent_list import PersistentList from qp.lib.spec import add_getters_and_setters, Mixin from qp.lib.spec import instance, sequence, specify, mapping from qp.lib.spec import spec, require, string, pattern, both class Category (DulcineaPersistent): """ A category is a node in a partial ordering. """ name_is = spec( (None, both(string, pattern('[a-zA-Z0-9_-]*$'))), "unique, URL-friendly identifier for this category") label_is = spec( (string, None), "human-readable string that describes this category; probably but " "not necessarily unique") description_is = spec( (string, None), "paragraph-length, human-readable prose description of this category") children_is = sequence(instance('Category'), PersistentList) parents_is = sequence(instance('Category'), PersistentList) _v_expansion_is = ComputedAttribute def __init__(self, name=None, label=None, description=None): specify(self, name=name, label=label, description=description, children=PersistentList(), parents=PersistentList(), _v_expansion=ComputedAttribute()) def __str__(self): return self.name or "*unnamed*" def add_child(self, child): require(child, Category) if child.is_ancestor_of(self): raise ValueError("Attempt to create circular relationship") if child not in self.children: self.children.append(child) child.parents.append(self) self._invalidate_expansion() def remove_child(self, child): require(child, Category) self.children.remove(child) child.parents.remove(self) self._invalidate_expansion() def set_children(self, children): require(children, list) for child in self.children[:]: # remove_child() modifies self.children self.remove_child(child) for child in children: self.add_child(child) def set_parents(self, parents): require(parents, list) for parent in self.parents[:]: # remove_child() modifies self.parents parent.remove_child(self) for parent in parents or []: parent.add_child(self) def _invalidate_expansion(self): self._v_expansion.invalidate() for parent in self.parents: parent._invalidate_expansion() def expand(self): """() -> { Category : bool } Return a set containing this category and its (recursive) children. """ def compute(): expansion = {self: 1} for child in self.children: if child not in expansion: expansion.update(child.expand()) return expansion return self._v_expansion.get(compute) def get_descendants(self): """() -> [ Category ] Return list containing this category's (recursive) children. """ descendants = [] for child in self.children: if child not in descendants: descendants.append(child) descendants += [descendant for descendant in child.get_descendants() if descendant not in descendants] return descendants def get_leaves(self): """() -> [ Category ] Return list containing this category or this category's (recursive) children that have no children. """ if self.children: leaves = {} for child in self.children: for leaf in child.get_leaves(): leaves[leaf] = 1 return list(leaves.keys()) else: return [self] def is_descendant_of(self, other_category): """(Category) -> bool Is the other category the same as this one or one of the descendant categories? """ return self in other_category.expand() def is_ancestor_of(self, other_category): """(Category) -> bool Is this category the same as or a proper descendant of other_category? """ return other_category in self.expand() def get_label(self): return self.label or self.name add_getters_and_setters(Category) class Categorized (Mixin): categories_is = sequence(Category, PersistentList) def __init__(self): self.categories = PersistentList() def add_category(self, category): if category not in self.categories: self.set_categories(self.categories + [category]) def set_categories(self, categories): """(categories : [Category]) """ specify(self, categories=PersistentList(categories)) def get_categories(self): """() -> [Category] """ return list(self.categories) def get_descendants_of(self, ancestor): return [category for category in self.categories if ancestor.is_ancestor_of(category)] def in_category(self, other_category): if other_category is None: return 0 require(other_category, Category) other_categories = other_category.expand() for category in self.categories: if category in other_categories: return 1 return 0 def in_categories(self, categories): """Is self in all of the categories? """ require(categories, [Category]) for category in categories: if not self.in_category(category): return 0 return 1 def remove_ancestors(categories): require(categories, [Category]) no_ancestors = categories[:] for category in categories or []: for descendant in category.get_descendants(): if descendant in categories and category in no_ancestors: no_ancestors.remove(category) return no_ancestors class CategoryDatabase (Mixin): categories_is = mapping({Category.name_is:Category}, PersistentDict) def __init__(self): assert self.__class__ is not CategoryDatabase, ( "CategoryDatabase should be a mixin in a persistent subclass") self.categories = PersistentDict() def add_category(self, category): require(category, Category) if self.categories.has_key(category.get_name()): raise ValueError("Category %r already exists." % category.get_name()) self.categories[category.get_name()] = category def rename_category(self, category, new_name): """(category:Category, new_name:string) """ old_name = category.get_name() if not self.categories.has_key(old_name): raise ValueError("Category %r not found." % old_name) if self.categories.has_key(new_name): raise ValueError("Category %r already exists." % new_name) category.set_name(new_name) self.add_category(category) del self.categories[old_name] def get_category(self, name): """(name:string) -> Category Retrieve the named category, or None. """ return self.categories.get(name) def get_categories(self): """() -> [Category]""" return list(self.categories.values()) def find_category(self, name): for category in self.get_categories(): if category.get_name() == name: return category for category in self.get_categories(): for descendant in category.get_descendants(): if descendant.get_name() == name: return descendant