class Event(Document): members_order = [ "event_start", "event_end", "event_location", "image", "summary", "blocks" ] event_start = schema.DateTime(member_group="content", indexed=True) event_end = schema.DateTime(member_group="content", min=event_start, indexed=True) event_location = schema.String(edit_control="cocktail.html.TextArea", translated=True, member_group="content") image = schema.Reference(type=File, relation_constraints={"resource_type": "image"}, listed_by_default=False, member_group="content") summary = schema.String(translated=True, edit_control="woost.views.RichTextEditor", listed_by_default=False, member_group="content") blocks = Slot()
class ChangeSet(PersistentObject): """A persistent record of a set of L{changes<Change>} performed on one or more CMS items.""" members_order = "id", "author", "date", "changes" indexed = True changes = schema.Mapping( searchable=False, get_item_key=lambda change: change.target and change.target.id) author = schema.Reference(required=True, type="woost.models.User") date = schema.DateTime(required=True, default=schema.DynamicDefault(datetime.now)) _thread_data = local() @classgetter def current(cls): return getattr(cls._thread_data, "current", None) @classgetter def current_author(cls): cs = cls.current return cs and cs.author def begin(self): if self.current: raise TypeError("Can't begin a new changeset, another changeset " "is already in place") self._thread_data.current = self def end(self): try: del self._thread_data.current except AttributeError: raise TypeError("Can't finalize the current changeset, there's no " "changeset in place") def get_searchable_text(self, languages, visited_objects=None): if visited_objects is None: visited_objects = set() elif self in visited_objects: return visited_objects.add(self) # Concatenate the descriptions of change authors and targets for language in languages: if self.author: yield translations(self.author, language) for change in self.changes.itervalues(): yield translations(change.target, language)
class Publishable(Item): """Base class for all site elements suitable for publication.""" instantiable = False cacheable = True edit_view = "woost.views.PublishableFieldsView" backoffice_heading_view = "woost.views.BackOfficePublishableHeading" type_group = "publishable" groups_order = [ "navigation", "presentation", "presentation.behavior", "presentation.format", "publication" ] members_order = [ "controller", "mime_type", "resource_type", "encoding", "parent", "path", "full_path", "hidden", "login_page", "per_language_publication", "enabled", "translation_enabled", "websites", "start_date", "end_date", "requires_https", "caching_policies" ] mime_type = schema.String(required=True, default="text/html", text_search=False, format=r"^[^/]+/[^/]+$", listed_by_default=False, member_group="presentation.format") resource_type = schema.String( indexed=True, text_search=False, editable=False, enumeration=("document", "image", "audio", "video", "package", "html_resource", "other"), translate_value=lambda value, language=None, **kwargs: u"" if not value else translations( "woost.models.Publishable.resource_type " + value, language, ** kwargs), listed_by_default=False, member_group="presentation.format") encoding = schema.String(listed_by_default=False, text_search=False, default="utf-8", member_group="presentation.format") controller = schema.Reference(type="woost.models.Controller", indexed=True, bidirectional=True, listed_by_default=False, member_group="presentation.behavior") def resolve_controller(self): if self.controller and self.controller.python_name: return import_object(self.controller.python_name) parent = schema.Reference(type="woost.models.Document", bidirectional=True, related_key="children", listed_by_default=False, member_group="navigation") path = schema.String(max=1024, indexed=True, listed_by_default=False, text_search=False, member_group="navigation") full_path = schema.String(indexed=True, unique=True, editable=False, text_search=False, listed_by_default=False, member_group="navigation") hidden = schema.Boolean(required=True, default=False, listed_by_default=False, member_group="navigation") login_page = schema.Reference(listed_by_default=False, member_group="navigation") per_language_publication = schema.Boolean(required=True, default=False, indexed=True, listed_by_default=False, member_group="publication") enabled = schema.Boolean(required=True, default=True, indexed=True, listed_by_default=False, member_group="publication") translation_enabled = schema.Boolean(required=True, default=True, translated=True, indexed=True, listed_by_default=False, member_group="publication") websites = schema.Collection(items="woost.models.Website", bidirectional=True, related_key="specific_content", member_group="publication") start_date = schema.DateTime(indexed=True, listed_by_default=False, affects_cache_expiration=True, member_group="publication") end_date = schema.DateTime(indexed=True, min=start_date, listed_by_default=False, affects_cache_expiration=True, member_group="publication") requires_https = schema.Boolean(required=True, default=False, listed_by_default=False, member_group="publication") caching_policies = schema.Collection( items=schema.Reference(type=CachingPolicy), bidirectional=True, integral=True, related_end=schema.Reference(), member_group="publication") def get_effective_caching_policy(self, **context): from woost.models import Configuration policies = [((-policy.important, 1), policy) for policy in self.caching_policies] policies.extend(((-policy.important, 2), policy) for policy in Configuration.instance.caching_policies) policies.sort() for criteria, policy in policies: if policy.applies_to(self, **context): return policy @event_handler def handle_changed(cls, event): member = event.member publishable = event.source if member.name == "path": publishable._update_path(publishable.parent, event.value) elif member.name == "parent": publishable._update_path(event.value, publishable.path) # If the parent element is specific to one or more websites, its # descendants will automatically share that specificity if event.value: publishable.websites = list(event.value.websites) else: publishable.websites = [] elif member.name == "mime_type": if event.value is None: publishable.resource_type = None else: publishable.resource_type = \ get_category_from_mime_type(event.value) @event_handler def handle_related(cls, event): if event.member is cls.websites: publishable = event.source website = event.related_object # Update the index if publishable.is_inserted and website.is_inserted: index = cls.per_website_publication_index # No longer available to any website if len(publishable.websites) == 1: index.remove(None, publishable.id) index.add(website.id, publishable.id) @event_handler def handle_unrelated(cls, event): if event.member is cls.websites: publishable = event.source website = event.related_object index = cls.per_website_publication_index index.remove(website.id, publishable.id) # Now available to any website if publishable.is_inserted and not publishable.websites: index.add(None, publishable.id) @event_handler def handle_inserted(cls, event): event.source.__insert_into_per_website_publication_index() @event_handler def handle_deleted(cls, event): cls.per_website_publication_index.remove(None, event.source.id) def __insert_into_per_website_publication_index(self): index = self.__class__.per_website_publication_index # Available to any website if not self.websites: index.add(None, self.id) # Restricted to a subset of websites else: for website in self.websites: if website.is_inserted: index.add(website.id, self.id) @classgetter def per_website_publication_index(cls): """A database index that enumerates content exclusive to one or more websites. """ index = datastore.root.get(WEBSITE_PUB_INDEX_KEY) if index is None: index = datastore.root.get(WEBSITE_PUB_INDEX_KEY) if index is None: index = MultipleValuesIndex() datastore.root[WEBSITE_PUB_INDEX_KEY] = index return index @event_handler def handle_rebuilding_indexes(cls, e): cls.rebuild_per_website_publication_index(verbose=e.verbose) @classmethod def rebuild_per_website_publication_index(cls, verbose=False): if verbose: print "Rebuilding the Publishable/Website index" del datastore.root[WEBSITE_PUB_INDEX_KEY] for publishable in cls.select(): publishable.__insert_into_per_website_publication_index() @classgetter def per_language_publication_index(cls): return datastore.root[WEBSITE_PUB_INDEX_KEY] def _update_path(self, parent, path): parent_path = parent and parent.full_path if parent_path and path: self.full_path = parent_path + "/" + path else: self.full_path = path def get_ancestor(self, depth): """Obtain one of the item's ancestors, given its depth in the document tree. @param depth: The depth level of the ancestor to obtain, with 0 indicating the root of the tree. Negative indices are accepted, and they reverse the traversal order (-1 will point to the item itself, -2 to its parent, and so on). @type depth: int @return: The requested ancestor, or None if there is no ancestor with the indicated depth. @rtype: L{Publishable} """ tree_line = list(self.ascend_tree(include_self=True)) tree_line.reverse() try: return tree_line[depth] except IndexError: return None def ascend_tree(self, include_self=False): """Iterate over the item's ancestors, moving towards the root of the document tree. @param include_self: Indicates if the object itself should be included in the iteration. @type include_self: bool @return: An iterable sequence of pages. @rtype: L{Document} iterable sequence """ publishable = self if include_self else self.parent while publishable is not None: yield publishable publishable = publishable.parent def descend_tree(self, include_self=False): """Iterate over the item's descendants. @param include_self: Indicates if the object itself should be included in the iteration. @type include_self: bool @return: An iterable sequence of publishable elements. @rtype: L{Publishable} iterable sequence """ if include_self: yield self def descends_from(self, page): """Indicates if the object descends from the given document. @param page: The hypothetical ancestor of the page. @type page: L{Document<woost.models.document.Document>} @return: True if the object is contained inside the given document or one of its descendants, or if it *is* the given document. False in any other case. @rtype: bool """ ancestor = self while ancestor is not None: if ancestor is page: return True ancestor = ancestor.parent return False def is_home_page(self): """Indicates if the object is the home page for any website. @rtype: bool """ from woost.models import Website return bool(self.get(Website.home.related_end)) def is_current(self): now = datetime.now() return (self.start_date is None or self.start_date <= now) \ and (self.end_date is None or self.end_date > now) def is_published(self, language=None, website=None): if self.per_language_publication: language = require_language(language) if not self.get("translation_enabled", language): return False from woost.models import Configuration if not Configuration.instance.language_is_enabled(language): return False elif not self.enabled: return False if not self.is_current(): return False websites_subset = self.websites if websites_subset and website != "any": if website is None: website = get_current_website() if website is None or website not in websites_subset: return False return True def is_accessible(self, user=None, language=None, website=None): if user is None: user = get_current_user() return (self.is_published(language, website) and user.has_permission(ReadPermission, target=self) and (not self.per_language_publication or user.has_permission(ReadTranslationPermission, language=require_language(language)))) @classmethod def select_published(cls, *args, **kwargs): return cls.select(filters=[IsPublishedExpression()]).select( *args, **kwargs) @classmethod def select_accessible(cls, *args, **kwargs): return cls.select( filters=[IsAccessibleExpression(get_current_user())]).select( *args, **kwargs) def get_uri(self, path=None, parameters=None, language=None, host=None, encode=True): uri = app.url_resolver.get_path(self, language=language) if uri is not None: if self.per_language_publication: uri = app.language.translate_uri( path=uri, language=require_language(language)) if path: uri = make_uri(uri, *path) if parameters: uri = make_uri(uri, **parameters) if host == "?": websites = self.websites if websites and get_current_website() not in websites: host = websites[0].hosts[0] else: host = None elif host == "!": if self.websites: host = self.websites[0].hosts[0] else: from woost.models import Configuration website = (get_current_website() or Configuration.instances.websites[0]) host = website.hosts[0] uri = self._fix_uri(uri, host, encode) return uri def translate_file_type(self, language=None): trans = "" mime_type = self.mime_type if mime_type: trans = translations("mime " + mime_type, language=language) if not trans: res_type = self.resource_type if res_type: trans = self.__class__.resource_type.translate_value( res_type, language=language) if trans and res_type != "other": ext = self.file_extension if ext: trans += " " + ext.upper().lstrip(".") return trans def get_cache_expiration(self): now = datetime.now() start = self.start_date if start is not None and start > now: return start end = self.end_date if end is not None and end > now: return end
class Block(Item): instantiable = False visible_from_root = False type_group = "blocks.content" type_groups_order = [ "blocks.content", "blocks.layout", "blocks.listings", "blocks.social", "blocks.forms", "blocks.custom" ] view_class = None block_display = "woost.views.BlockDisplay" backoffice_heading_view = "woost.views.BackOfficeBlockHeading" groups_order = ["content", "behavior", "html", "administration"] members_order = [ "heading", "heading_type", "enabled", "start_date", "end_date", "controller", "styles", "inline_css_styles", "html_attributes" ] heading = schema.String(descriptive=True, translated=True, member_group="content") heading_type = schema.String(default="hidden", enumeration=[ "hidden", "hidden_h1", "generic", "h1", "h2", "h3", "h4", "h5", "h6", "dt", "figcaption" ], required=heading, member_group="content") enabled = schema.Boolean(required=True, default=True, member_group="behavior") start_date = schema.DateTime(indexed=True, affects_cache_expiration=True, member_group="behavior") end_date = schema.DateTime(indexed=True, min=start_date, affects_cache_expiration=True, member_group="behavior") controller = schema.String(member_group="behavior") styles = schema.Collection( items=schema.Reference(type=Style), relation_constraints={"applicable_to_blocks": True}, related_end=schema.Collection(), member_group="html") inline_css_styles = schema.String(edit_control="cocktail.html.TextArea", member_group="html") html_attributes = schema.String(listed_by_default=False, edit_control="cocktail.html.TextArea", member_group="html") def get_block_image(self): return self def create_view(self): if self.view_class is None: raise ValueError("No view specified for block %s" % self) view = templates.new(self.view_class) self.init_view(view) if self.controller: controller_class = import_object(self.controller) controller = controller_class() controller.block = self controller.view = view controller() for key, value in controller.output.iteritems(): setattr(view, key, value) return view def init_view(self, view): view.block = self block_proxy = self.get_block_proxy(view) block_proxy.set_client_param("blockId", self.id) block_proxy.add_class("block") if self.html_attributes: for line in self.html_attributes.split("\n"): try: pos = line.find("=") key = line[:pos] value = line[pos + 1:] except: pass else: block_proxy[key.strip()] = value.strip() if self.inline_css_styles: for line in self.inline_css_styles.split(";"): try: key, value = line.split(":") except: pass else: block_proxy.set_style(key.strip(), value.strip()) for style in self.styles: block_proxy.add_class(style.class_name) block_proxy.add_class("block%d" % self.id) if self.qname: block_proxy.add_class(self.qname.replace(".", "-")) if self.heading: self.add_heading(view) view.depends_on(self) def get_block_proxy(self, view): return view def add_heading(self, view): if self.heading_type != "hidden": if hasattr(view, "heading"): if isinstance(view.heading, Element): if self.heading_type == "hidden_h1": view.heading.tag = "h1" view.heading.set_style("display", "none") elif self.heading_type == "generic": view.heading.tag = "div" else: view.heading.tag = self.heading_type view.heading.append(self.heading) else: view.heading = self.heading else: insert_heading = getattr(view, "insert_heading", None) view.heading = self.create_heading() if insert_heading: insert_heading(view.heading) else: view.insert(0, view.heading) def create_heading(self): if self.heading_type == "hidden_h1": heading = Element("h1") heading.set_style("display", "none") elif self.heading_type == "generic": heading = Element() else: heading = Element(self.heading_type) heading.add_class("heading") heading.append(self.heading) return heading def is_common_block(self): from .configuration import Configuration return bool(self.get(Configuration.common_blocks.related_end)) def is_published(self): # Time based publication window if self.start_date or self.end_date: now = datetime.now() # Not published yet if self.start_date and now < self.start_date: return False # Expired if self.end_date and now >= self.end_date: return False return self.enabled def get_member_copy_mode(self, member): mode = Item.get_member_copy_mode(self, member) if (mode and mode != schema.DEEP_COPY and isinstance(member, schema.RelationMember) and member.is_persistent_relation and issubclass(member.related_type, Block)): mode = lambda block, member, value: not value.is_common_block() return mode def _included_in_cascade_delete(self, parent, member): if isinstance(parent, Block) and self.is_common_block(): return False return Item._included_in_cascade_delete(self, parent, member) def find_publication_slots(self): """Iterates over the different slots of publishable elements that contain the block. @return: An iterable sequence of the slots that contain the block. Each slot is represented by a tuple consisting of a L{Publishable} and a L{Member<cocktail.schema.member>}. """ visited = set() def iter_slots(block): for member in block.__class__.members().itervalues(): if ((block, member) not in visited and isinstance(member, schema.RelationMember) and member.related_type): value = block.get(member) if value is not None: # Yield relations to publishable elements if issubclass(member.related_type, Publishable): if isinstance(member, schema.Collection): for publishable in value: yield (publishable, member) else: yield (value, member) # Recurse into relations to other blocks elif issubclass(member.related_type, Block): visited.add((block, member)) if member.related_end: visited.add((block, member.related_end)) if isinstance(member, schema.Collection): for child in value: for slot in iter_slots(child): yield slot else: for slot in iter_slots(value): yield slot return iter_slots(self) def find_paths(self): """Iterates over the different sequences of slots that contain the block. @return: A list of lists, where each list represents one of the paths that the block descends from. Each entry in a path consists of container, slot pair. @rtype: list of (L{Item<woost.models.item.Item>}, L{Slot<woost.models.slot.Slot>}) lists """ def visit(block, followed_path): paths = [] for member in block.__class__.members().itervalues(): related_end = getattr(member, "related_end", None) if isinstance(related_end, Slot): parents = block.get(member) if parents: if isinstance(parents, Item): parents = (parents, ) for parent in parents: location = (parent, related_end) if location not in followed_path: paths.extend( visit(parent, [location] + followed_path)) # End of the line if not paths and followed_path: paths.append(followed_path) return paths return visit(self, []) @property def name_prefix(self): return "block%d." % self.id @property def name_suffix(self): return None def replace_with(self, replacement): """Removes this block from all slots, putting another block in the same position. @param replacement: The block to insert. @type replacement: L{Block} """ for member in self.__class__.members().itervalues(): related_end = getattr(member, "related_end", None) if isinstance(related_end, Slot): for container in self.get(member): slot_content = container.get(related_end) slot_content[slot_content.index(self)] = replacement
class Item(PersistentObject): """Base class for all CMS items. Provides basic functionality such as authorship, modification timestamps, versioning and synchronization. """ type_group = "setup" instantiable = False members_order = [ "id", "qname", "global_id", "synchronizable", "author", "creation_time", "last_update_time" ] # Enable full text indexing for all items (although the Item class itself # doesn't provide any searchable text field by default, its subclasses may, # or it may be extended; by enabling full text indexing at the root class, # heterogeneous queries on the whole Item class will use available # indexes). full_text_indexed = True # Extension property that indicates if content types should be visible from # the backoffice root view visible_from_root = True # Extension property that indicates if the backoffice should show child # entries for this content type in the type selector collapsed_backoffice_menu = False # Customization of the heading for BackOfficeItemView backoffice_heading_view = "woost.views.BackOfficeItemHeading" # Customization of the backoffice preview action preview_view = "woost.views.BackOfficePreviewView" preview_controller = "woost.controllers.backoffice." \ "previewcontroller.PreviewController" def __init__(self, *args, **kwargs): PersistentObject.__init__(self, *args, **kwargs) # Assign a global ID for the object (unless one was passed in as a # keyword parameter) if not self.global_id: if not app.installation_id: raise ValueError( "No value set for woost.app.installation_id; " "make sure your settings file specifies a unique " "identifier for this installation of the site.") self.global_id = "%s-%d" % (app.installation_id, self.id) @event_handler def handle_inherited(cls, e): if (isinstance(e.schema, schema.SchemaClass) and "instantiable" not in e.schema.__dict__): e.schema.instantiable = True # Unique qualified name #-------------------------------------------------------------------------- qname = schema.String(unique=True, indexed=True, text_search=False, listed_by_default=False, member_group="administration") # Synchronization #------------------------------------------------------------------------------ global_id = schema.String(required=True, unique=True, indexed=True, normalized_index=False, synchronizable=False, invalidates_cache=False, listed_by_default=False, member_group="administration") synchronizable = schema.Boolean(required=True, indexed=True, synchronizable=False, default=True, shadows_attribute=True, invalidates_cache=False, listed_by_default=False, member_group="administration") # Backoffice customization #-------------------------------------------------------------------------- show_detail_view = "woost.views.BackOfficeShowDetailView" show_detail_controller = \ "woost.controllers.backoffice.showdetailcontroller." \ "ShowDetailController" collection_view = "woost.views.BackOfficeCollectionView" edit_node_class = "woost.controllers.backoffice.editstack.EditNode" edit_view = "woost.views.BackOfficeFieldsView" edit_form = "woost.views.ContentForm" edit_controller = \ "woost.controllers.backoffice.itemfieldscontroller." \ "ItemFieldsController" __deleted = False @getter def is_deleted(self): return self.__deleted # Last change timestamp #-------------------------------------------------------------------------- @classmethod def get_last_instance_change(cls): max_value = datastore.root.get(cls.full_name + ".last_instance_change") return None if max_value is None else max_value.value @classmethod def set_last_instance_change(cls, last_change): for cls in cls.__mro__: if hasattr(cls, "set_last_instance_change"): key = cls.full_name + ".last_instance_change" max_value = datastore.root.get(key) if max_value is None: datastore.root[key] = max_value = MaxValue(last_change) else: max_value.value = last_change # Versioning #-------------------------------------------------------------------------- versioned = True changes = schema.Collection(required=True, versioned=False, editable=False, synchronizable=False, items="woost.models.Change", bidirectional=True, invalidates_cache=False, visible=False, affects_last_update_time=False) creation_time = schema.DateTime(versioned=False, indexed=True, editable=False, synchronizable=False, invalidates_cache=False, member_group="administration") last_update_time = schema.DateTime(indexed=True, versioned=False, editable=False, synchronizable=False, invalidates_cache=False, affects_last_update_time=False, member_group="administration") @classmethod def _create_translation_schema(cls, members): members["versioned"] = False PersistentObject._create_translation_schema.im_func(cls, members) @classmethod def _add_member(cls, member): if member.name == "translations": member.editable = False member.searchable = False member.synchronizable = False PersistentClass._add_member(cls, member) def _get_revision_state(self): """Produces a dictionary with the values for the item's versioned members. The value of translated members is represented using a (language, translated value) mapping. @return: The item's current state. @rtype: dict """ # Store the item state for the revision state = PersistentMapping() for key, member in self.__class__.members().iteritems(): if not member.versioned: continue if member.translated: value = dict( (language, translation.get(key)) for language, translation in self.translations.iteritems()) else: value = self.get(key) # Make a copy of mutable objects if isinstance(value, (list, set, ListWrapper, SetWrapper)): value = list(value) state[key] = value return state # Item insertion overriden to make it versioning aware @event_handler def handle_inserting(cls, event): item = event.source now = datetime.now() item.creation_time = now item.last_update_time = now item.set_last_instance_change(now) item.__deleted = False if item.__class__.versioned: changeset = ChangeSet.current if changeset: change = Change() change.action = "create" change.target = item change.changed_members = set( member.name for member in item.__class__.members().itervalues() if member.versioned) change.item_state = item._get_revision_state() change.changeset = changeset changeset.changes[item.id] = change if item.author is None: item.author = changeset.author change.insert(event.inserted_objects) # Extend item modification to make it versioning aware @event_handler def handle_changed(cls, event): item = event.source now = None update_timestamp = ( item.is_inserted and event.member.affects_last_update_time and not getattr(item, "_v_is_producing_default", False)) if update_timestamp: now = datetime.now() item.set_last_instance_change(now) if getattr(item, "_v_initializing", False) \ or not event.member.versioned \ or not item.is_inserted \ or not item.__class__.versioned: return changeset = ChangeSet.current if changeset: member_name = event.member.name language = event.language change = changeset.changes.get(item.id) if change is None: action = "modify" change = Change() change.action = action change.target = item change.changed_members = set() change.item_state = item._get_revision_state() change.changeset = changeset changeset.changes[item.id] = change if update_timestamp: item.last_update_time = now change.insert() else: action = change.action if action == "modify": change.changed_members.add(member_name) if action in ("create", "modify"): value = event.value # Make a copy of mutable objects if isinstance(value, (list, set, ListWrapper, SetWrapper)): value = list(value) if language: change.item_state[member_name][language] = value else: change.item_state[member_name] = value elif update_timestamp: item.last_update_time = now @event_handler def handle_deleting(cls, event): item = event.source # Update the last time of modification for the item now = datetime.now() item.set_last_instance_change(now) item.last_update_time = now if item.__class__.versioned: changeset = ChangeSet.current # Add a revision for the delete operation if changeset: change = changeset.changes.get(item.id) if change and change.action != "delete": del changeset.changes[item.id] if change is None \ or change.action not in ("create", "delete"): change = Change() change.action = "delete" change.target = item change.changeset = changeset changeset.changes[item.id] = change change.insert() item.__deleted = True _preserved_members = frozenset([changes]) def _should_erase_member(self, member): return (PersistentObject._should_erase_member(self, member) and member not in self._preserved_members) # Authorship #-------------------------------------------------------------------------- author = schema.Reference(indexed=True, editable=False, type="woost.models.User", listed_by_default=False, invalidates_cache=False, member_group="administration") # URLs #-------------------------------------------------------------------------- def get_image_uri( self, image_factory=None, parameters=None, encode=True, include_extension=True, host=None, ): uri = make_uri("/images", self.id) ext = None if image_factory: if isinstance(image_factory, basestring): pos = image_factory.rfind(".") if pos != -1: ext = image_factory[pos + 1:] image_factory = image_factory[:pos] from woost.models.rendering import ImageFactory image_factory = \ ImageFactory.require_instance(identifier = image_factory) uri = make_uri( uri, image_factory.identifier or "factory%d" % image_factory.id) if include_extension: from woost.models.rendering.formats import (formats_by_extension, extensions_by_format, default_format) if not ext and image_factory and image_factory.default_format: ext = extensions_by_format[image_factory.default_format] if not ext: ext = getattr(self, "file_extension", None) if ext: ext = ext.lower().lstrip(".") if not ext or ext not in formats_by_extension: ext = extensions_by_format[default_format] uri += "." + ext if parameters: uri = make_uri(uri, **parameters) return self._fix_uri(uri, host, encode) def _fix_uri(self, uri, host, encode): if encode: uri = percent_encode_uri(uri) if "://" in uri: host = None if host: website = get_current_website() policy = website and website.https_policy if (policy == "always" or (policy == "per_page" and (getattr(self, 'requires_https', False) or not get_current_user().anonymous))): scheme = "https" else: scheme = "http" if host == ".": location = Location.get_current_host() location.scheme = scheme host = str(location) elif not "://" in host: host = "%s://%s" % (scheme, host) uri = make_uri(host, uri) elif "://" not in uri: uri = make_uri("/", uri) return uri copy_excluded_members = set( [changes, author, creation_time, last_update_time, global_id]) # Caching and invalidation #-------------------------------------------------------------------------- cacheable = False @property def main_cache_tag(self): """Obtains a cache tag that can be used to match all cache entries related to this item. """ return "%s-%d" % (self.__class__.__name__, self.id) def get_cache_tags(self, language=None, cache_part=None): """Obtains the list of cache tags that apply to this item. :param language: Indicates the language for which the cache invalidation is being requested. If not set, the returned tags will match all entries related to this item, regardless of the language they are in. :param cache_part: If given, the returned tags will only match cache entries qualified with the specified identifier. These qualifiers are tipically attached by specifying the homonimous parameter of the `~woost.views.depends_on` extension method. """ main_tag = self.main_cache_tag if cache_part: main_tag += "-" + cache_part tags = set([main_tag]) if language: tags.add("lang-" + language) return tags def get_cache_expiration(self): expiration = None for member in self.__class__.iter_members(): if member.affects_cache_expiration: expiration = nearest_expiration(expiration, self.get(member)) return expiration @classmethod def get_cache_expiration_for_type(cls): expiration = None for member in cls.iter_members(): if member.affects_cache_expiration: if isinstance(member, schema.Date): threshold = date.today() else: threshold = datetime.now() instance = first( cls.select(member.greater(threshold), order=member, cached=False)) if instance is not None: expiration = nearest_expiration(expiration, instance.get(member)) return expiration def clear_cache(self, language=None, cache_part=None): """Remove all the cached pages that are based on this item. :param language: Indicates the language for which the cache invalidation is being requested. If not set, the invalidation will affect all entries related to this item, regardless of the language they are in. :param cache_part: If given, only cache entries qualified with the specified identifier will be cleared. These qualifiers are tipically attached by specifying the homonimous parameter of the `~woost.views.depends_on` extension method. """ app.cache.clear(scope=self.get_cache_invalidation_scope( language=language, cache_part=cache_part)) def clear_cache_after_commit(self, language=None, cache_part=None): """Remove all the cached pages that are based on this item, as soon as the current database transaction is committed. This method can be called multiple times during a single transaction. All the resulting invalidation targets will be removed from the cache once the transaction is committed. :param language: Indicates the language for which the cache invalidation is being requested. If not set, the invalidation will affect all entries related to this item, regardless of the language they are in. :param cache_part: If given, only cache entries qualified with the specified identifier will be cleared. These qualifiers are tipically attached by specifying the homonimous parameter of the `~woost.views.depends_on` extension method. """ app.cache.clear_after_commit(scope=self.get_cache_invalidation_scope( language=language, cache_part=cache_part)) def get_cache_invalidation_scope(self, language=None, cache_part=None): """Determine the scope of a cache invalidation request for this item. :param language: Indicates the language for which the cache invalidation is being requested. If not set, the scope will include all entries related to this item, regardless of the language they are in. :param cache_part: If given, only cache entries qualified with the specified identifier will be included. These qualifiers are tipically attached by specifying the homonimous parameter of the `~woost.views.depends_on` extension method. :return: A cache invalidation scope. See the `cocktail.caching.cache.Cache.clear` method for details on its format. """ selectors = set() # Tags per type for cls in \ self.__class__.ascend_inheritance(include_self = True): selector = cls.full_name if cache_part: selector += "-" + cache_part if language: selector = (selector, "lang-" + language) selectors.add(selector) if cls is Item: break # Tags per instance selector = self.main_cache_tag if cache_part: selector += "-" + cache_part if language: selector = (selector, "lang-" + language) selectors.add(selector) return selectors
class PricingPolicy(Item): visible_from_root = False integral = True instantiable = False edit_controller = \ "woost.extensions.shop.pricingpolicyfieldscontroller." \ "PricingPolicyFieldsController" edit_view = "woost.extensions.shop.PricingPolicyFields" members_order = [ "title", "enabled", "start_date", "end_date" ] title = schema.String( translated = True, descriptive = True ) enabled = schema.Boolean( required = True, default = True ) start_date = schema.DateTime( indexed = True ) end_date = schema.DateTime( indexed = True, min = start_date ) matching_items = schema.Mapping() # TODO: Validate issubclass(matching_items["type"], (ShopOrder, Product)) def is_current(self): return (self.start_date is None or self.start_date <= datetime.now()) \ and (self.end_date is None or self.end_date > datetime.now()) def select_matching_items(self, *args, **kwargs): user_collection = UserCollection(Item) user_collection.allow_paging = False user_collection.allow_member_selection = False user_collection.allow_language_selection = False user_collection.params.source = self.matching_items.get #user_collection.available_languages = Configuration.instance.languages # <- required? return user_collection.subset def match_item(self, item): if self.matching_items: for filter in self.select_matching_items().filters: if not filter.eval(item): return False return True def applies_to(self, item): return self.enabled and self.is_current() and self.match_item(item) def apply(self, item, costs): pass
class ECommerceBillingConcept(Item): members_order = [ "title", "enabled", "start_date", "end_date", "hidden", "scope", "eligible_countries", "eligible_products", "eligible_roles", "condition", "implementation" ] visible_from_root = False title = schema.String(translated=True, descriptive=True, required=True) enabled = schema.Boolean(required=True, default=True) start_date = schema.DateTime(indexed=True) end_date = schema.DateTime(indexed=True, min=start_date) hidden = schema.Boolean(default=False, required=True) scope = schema.String( required=True, enumeration=["order", "purchase"], default="order", edit_control="cocktail.html.RadioSelector", translate_value=lambda value, language=None, **kwargs: "" if not value else translations( "ECommerceBillingConcept.scope-" + value, language, **kwargs)) condition = schema.CodeBlock(language="python") eligible_countries = schema.Collection(items=schema.Reference( type=Location, relation_constraints=[Location.location_type.equal("country")]), related_end=schema.Collection()) eligible_products = schema.Collection( items=schema.Reference(type=ECommerceProduct), related_end=schema.Collection()) eligible_roles = schema.Collection(items=schema.Reference(type=Role), related_end=schema.Collection()) implementation = schema.CodeBlock(language="python") def is_current(self): return (self.start_date is None or self.start_date <= datetime.now()) \ and (self.end_date is None or self.end_date > datetime.now()) def applies_to(self, item, costs=None): if not self.enabled: return False if not self.is_current(): return False from woost.extensions.ecommerce.ecommerceproduct \ import ECommerceProduct order = None purchase = None product = None if isinstance(item, ECommerceProduct): if self.eligible_products and item not in self.eligible_products: return False product = item elif self.scope == "order": from woost.extensions.ecommerce.ecommerceorder \ import ECommerceOrder if not isinstance(item, ECommerceOrder): return False if self.eligible_products and not any( purchase.product in self.eligible_products for purchase in item.purchases): return False order = item elif self.scope == "purchase": from woost.extensions.ecommerce.ecommercepurchase \ import ECommercePurchase if not isinstance(item, ECommercePurchase): return False if self.eligible_products \ and item.product not in self.eligible_products: return False order = item.order purchase = item product = item.product if self.eligible_countries and ( order is None or order.country is None or not any( order.country.descends_from(region) for region in self.eligible_countries)): return False # Eligible roles if self.eligible_roles and ( order is None or order.customer is None or not any(role in self.eligible_roles for role in order.customer.iter_roles())): return False # Custom condition if self.condition: context = { "self": self, "order": order, "purchase": purchase, "product": product, "costs": costs, "applies": True } exec self.condition in context if not context["applies"]: return False return True def apply(self, item, costs): costs["concepts"].append(self) kind, value = self.parse_implementation() if kind == "override": applicable_concepts = [] for concept in costs["concepts"]: concept_kind, concept_value = concept.parse_implementation() if concept_kind not in ("add", "override") or concept is self: applicable_concepts.append(concept) costs["concepts"] = applicable_concepts costs["cost"] = value elif kind == "add": costs["cost"] += value elif kind == "override_percentage": applicable_concepts = [] for concept in costs["concepts"]: concept_kind, concept_value = concept.parse_implementation() if concept_kind not in ("add_percentage", "override_percentage" ) or concept is self: applicable_concepts.append(concept) costs["concepts"] = applicable_concepts costs["percentage"] = value elif kind == "add_percentage": costs["percentage"] += value elif kind == "free_units": delivered, paid = value q, r = divmod(item.quantity, delivered) costs["paid_units"] = q * paid + r elif kind == "custom": context = {"self": self, "item": item, "costs": costs} exec value in context def parse_implementation(self): value = self.implementation # Cost override match = override_regexp.match(value) if match: return ("override", Decimal(match.group(1))) # Additive cost match = add_regexp.match(value) if match: return ("add", Decimal(match.group(1))) # Override percentage match = override_percentage_regexp.match(value) if match: return ("override_percentage", Decimal(match.group(1))) # Override percentage match = add_percentage_regexp.match(value) if match: return ("add_percentage", Decimal(match.group(1))) # Free units discount ("3x2", "2x1", etc) match = free_units_regexp.match(value) if match: return ("free_units", (int(match.group(1)), int(match.group(2)))) return ("custom", value)