예제 #1
0
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()
예제 #2
0
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)
예제 #3
0
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
예제 #4
0
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
예제 #5
0
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
예제 #6
0
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
예제 #7
0
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)