Exemplo n.º 1
0
class Change(PersistentObject):
    """A persistent record of an action performed on a CMS item."""

    indexed = True

    changeset = schema.Reference(required=True, type="woost.models.ChangeSet")

    action = schema.String(required=True,
                           indexed=True,
                           enumeration=["create", "modify", "delete"])

    target = schema.Reference(required=True,
                              type="woost.models.Item",
                              bidirectional=True)

    changed_members = schema.Collection(type=set, items=schema.String())

    item_state = schema.Mapping(required=False)

    def __translate__(self, language, **kwargs):
        return translations(
            "woost.models.changesets.Change description",
            action=self.action,
            target=self.target) or PersistentObject.__translate__(
                self, language, **kwargs)
Exemplo n.º 2
0
class ContentPermission(Permission):
    """Base class for permissions restricted to a subset of a content type."""

    edit_controller = \
        "woost.controllers.backoffice.contentpermissionfieldscontroller." \
        "ContentPermissionFieldsController"
    edit_view = "woost.views.ContentPermissionFields"

    matching_items = schema.Mapping(
        translate_value=lambda value, language=None, **kwargs: ""
        if not value else translations(
            ContentPermission._get_user_collection(value).subset))

    def match(self, target, verbose=False):

        query = self.select_items()

        if isinstance(target, type):
            if not issubclass(target, query.type):
                if verbose:
                    print permission_doesnt_match_style("type doesn't match"),
                return False
            elif not self.authorized and "filter" in self.matching_items:
                if verbose:
                    print permission_doesnt_match_style("partial restriction")
                return False
        else:
            if not issubclass(target.__class__, query.type):
                if verbose:
                    print permission_doesnt_match_style("type doesn't match"),
                return False

            for filter in query.filters:
                if not filter.eval(target):
                    if verbose:
                        print permission_doesnt_match_style(
                            "filter %s doesn't match" % filter),
                    return False

        return True

    def select_items(self, *args, **kwargs):

        subset = self._get_user_collection(self.matching_items).subset

        if args or kwargs:
            subset = subset.select(*args, **kwargs)

        return subset

    @classmethod
    def _get_user_collection(self, matching_items):
        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 = matching_items.get
        from woost.models.configuration import Configuration
        user_collection.available_languages = Configuration.instance.languages
        return user_collection
Exemplo n.º 3
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)
Exemplo n.º 4
0
class ContentTrigger(Trigger):
    """Base class for triggers based on content type instances."""

    edit_controller = \
        "woost.controllers.backoffice.triggerfieldscontroller." \
        "TriggerFieldsController"

    edit_view = "woost.views.TriggerFields"

    matching_items = schema.Mapping()

    def select_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
        from woost.models import Configuration
        user_collection.available_languages = Configuration.instance.languages
        return user_collection.subset

    def match(self, user, target = None, verbose = False, **context):

        # Check the target
        query = self.select_items()

        if not isinstance(target, query.type):
            if verbose:
                print trigger_doesnt_match_style("type doesn't match")
            return False
        else:
            for filter in query.filters:
                if not filter.eval(target):
                    if verbose:
                        print trigger_doesnt_match_style(
                            "filter doesn't match"
                        )
                    return False

        if not Trigger.match(
            self,
            user,
            target = target,
            verbose = verbose,
            **context
        ):
            return False

        return True
Exemplo n.º 5
0
class Feed(Publishable):

    type_group = "setup"
    instantiable = True

    groups_order = ["meta", "feed_items"]

    members_order = [
        "title", "ttl", "image", "description", "limit", "query_parameters",
        "item_title_expression", "item_link_expression",
        "item_publication_date_expression", "item_description_expression"
    ]

    default_mime_type = u"application/rss+xml"

    default_controller = schema.DynamicDefault(
        lambda: Controller.get_instance(qname="woost.feed_controller"))

    edit_controller = \
        "woost.controllers.backoffice.feedfieldscontroller." \
        "FeedFieldsController"
    edit_view = "woost.views.FeedFields"

    title = schema.String(indexed=True,
                          normalized_index=True,
                          full_text_indexed=True,
                          descriptive=True,
                          translated=True,
                          member_group="meta")

    ttl = schema.Integer(listed_by_default=False, member_group="meta")

    image = schema.Reference(
        type=Publishable,
        related_end=schema.Collection(),
        relation_constraints=Publishable.resource_type.equal("image"),
        member_group="meta")

    description = schema.String(required=True,
                                translated=True,
                                listed_by_default=False,
                                edit_control="cocktail.html.TextArea",
                                member_group="meta")

    limit = schema.Integer(min=1,
                           listed_by_default=False,
                           member_group="feed_items")

    query_parameters = schema.Mapping(keys=schema.String(),
                                      required=True,
                                      listed_by_default=False,
                                      member_group="feed_items")

    item_title_expression = schema.CodeBlock(language="python",
                                             required=True,
                                             default="translations(item)",
                                             member_group="feed_items")

    item_link_expression = schema.CodeBlock(language="python",
                                            required=True,
                                            default="cms.uri(item)",
                                            member_group="feed_items")

    item_publication_date_expression = schema.CodeBlock(
        language="python",
        required=True,
        default="item.start_date or item.creation_time",
        member_group="feed_items")

    item_description_expression = schema.CodeBlock(language="python",
                                                   required=True,
                                                   default="item.description",
                                                   member_group="feed_items")

    def select_items(self):

        user_collection = UserCollection(Publishable)
        user_collection.allow_paging = False
        user_collection.allow_member_selection = False
        user_collection.allow_language_selection = False
        user_collection.params.source = self.query_parameters.get
        user_collection.available_languages = \
            Configuration.instance.get_enabled_languages()
        items = user_collection.subset

        if self.limit:
            items.range = (0, self.limit)

        return items
class Export(Item):

    type_group = "staticpub"
    instantiable = False

    members_order = ["user", "destination", "state", "tasks"]

    user = schema.Reference(editable=schema.READ_ONLY,
                            type=User,
                            related_end=schema.Collection())

    destination = schema.Reference(
        editable=schema.READ_ONLY,
        type="woost.extensions.staticpub.destination.Destination",
        bidirectional=True,
        required=True)

    state = schema.String(editable=schema.READ_ONLY,
                          default="idle",
                          enumeration=["idle", "running", "completed"],
                          indexed=True,
                          ui_read_only_form_control=JS("""
            (binding) => binding.object.destination._class.state_ui_component
        """))

    tasks = schema.Mapping(editable=schema.NOT_EDITABLE,
                           searchable=False,
                           type=OOBTree,
                           keys=schema.Tuple(items=(schema.Integer(),
                                                    LocaleMember())),
                           values=export_task_schema)

    auth_token = None

    def renew_auth_token(self):
        if self.user:
            self.auth_token = app.authentication.create_auth_token(
                self.user, expiration=timedelta(hours=2))
        return self.auth_token

    def add_task(self, action, item, language):

        valid_actions = ("post", "delete")
        if action not in valid_actions:
            raise ValueError(f"Invalid export action ({action}); "
                             f"should be one of {valid_actions}")

        if not isinstance(item, Publishable):
            raise ValueError(
                f"Can't export ({item:r}); expected an instance of "
                "woost.models.Publishable")

        key = (item.id, language)
        task = self.tasks.get(key)
        if task is None:
            task = PersistentMapping({"item": item, "language": language})
            self.tasks[key] = task

        task["action"] = action
        task["state"] = "pending"
        task["error_message"] = None
        return task

    @property
    def progress(self):

        total = 0
        completed = 0

        for task in self.tasks.itervalues():
            total += 1
            if task["state"] != "pending":
                completed += 1

        if not total:
            return 0.0

        return float(completed) / total

    def create_export_job(self):
        return self.destination.export_job_class(self)

    def execute_in_subprocess(self):
        script = app.path("scripts", "staticpub.py")
        return subprocess.Popen([
            os.path.join(sys.prefix, "bin", "python"), script, "export",
            f"export:{self.id}"
        ])

    @event_handler
    def handle_changed(e):
        if e.member is Export.state:
            if e.value == "running":
                if e.source.user and not e.source.auth_token:
                    e.source.renew_auth_token()
            elif e.value == "completed":
                if e.source.auth_token:
                    app.authentication.revoke_auth_token(e.source.auth_token)
                    e.source.auth_token = None
Exemplo n.º 7
0
class UserView(Item):

    type_group = "users"
    members_order = ["title", "parameters", "roles"]
    edit_controller = \
        "woost.controllers.backoffice.userviewfieldscontroller." \
        "UserViewFieldsController"
    edit_view = "woost.views.UserViewFields"

    title = schema.String(
        descriptive = True,
        translated = True,
        required = True,
        unique = True
    )

    def _parse_parameters(form_reader, data):

        if data is None:
            return None

        params = {}

        for param in data.split("\n"):
            param = param.strip()
            if param:
                parts = param.split("=")
                if len(parts) != 2:
                    return data # Parse error
                key, value = parts
                if key in params:
                    prev_value = params[key]
                    if isinstance(prev_value, basestring):
                        params[key] = [prev_value, value]
                    else:
                        prev_value.append(value)
                else:
                    params[key] = value
        
        return params

    def _serialize_parameters(value):
        if value:
            return "\n".join(
                "%s=%s" % (key, value)
                if isinstance(value, basestring)
                else ("\n".join("%s=%s" % (key, x) for x in value))
                for key, value in value.iteritems()
            )
        else:
            return ""

    parameters = schema.Mapping(
        keys = schema.String()
    )

    del _parse_parameters
    del _serialize_parameters

    roles = schema.Collection(
        items = "woost.models.Role",
        bidirectional = True
    )

    def uri(self, **kwargs):        
        params = self.parameters.copy()
        params.update(kwargs)
        params = dict((str(key), value) for key, value in params.iteritems())
        return "?" + view_state(**params)
Exemplo n.º 8
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
class Destination(Item):

    type_group = "staticpub"
    export_file_extension = ".html"
    export_job_class = ExportJob
    exporter_class = None
    instantiable = False
    state_ui_component = (
        "woost.extensions.staticpub.admin.ui."
        "PublicationState"
    )

    resolving_export_path = Event()

    members_order = [
        "title",
        "url",
        "website_prefixes",
        "exports"
    ]

    title = schema.String(
        required=True,
        unique=True,
        indexed=True,
        translated=True,
        descriptive=True
    )

    url = schema.URL()

    website_prefixes = schema.Mapping(
        keys=schema.Reference(
            type=Website,
            ui_form_control="cocktail.ui.DropdownSelector"
        ),
        values=schema.String()
    )

    exports = schema.Collection(
        items="woost.extensions.staticpub.export.Export",
        bidirectional=True,
        integral=True,
        editable=schema.NOT_EDITABLE
    )

    def __init__(self, *args, **kwargs):
        Item.__init__(self, *args, **kwargs)
        self._pending_tasks = IOBTree()
        self._entries_by_tag = OOBTree()
        self._entry_tags = OOBTree()

    def create_exporter(self, **kwargs):
        if self.exporter_class is None:
            raise ValueError(f"No exporter class defined for {self}")
        return self.exporter_class(**kwargs)

    def get_export_url(
            self,
            url: URL,
            resolution: URLResolution = None,
            content_type: str = None) -> URL:

        root_url = URL(self.url)
        return root_url.copy(
            path=root_url.path.append(
                self.get_export_path(
                    url,
                    resolution=resolution,
                    content_type=content_type
                )
            )
        )

    def get_export_path(
            self,
            url: Union[URL, str],
            resolution: URLResolution = None,
            content_type: str = None,
            add_file_extension: bool = True) -> Sequence[str]:

        url = URL(url)

        if resolution is None:
            resolution = app.url_mapping.resolve(url)

        export_path = []

        # Add per-website prefixes
        if self.website_prefixes:
            website = (
                resolution
                and resolution.publishable
                and resolution.publishable.websites
                and len(resolution.publishable.websites) == 1
                and iter(resolution.publishable.websites).next()
            )
            if website:
                prefix = self.website_prefixes.get(website)
                if prefix:
                    export_path.extend(prefix.split("/"))

        # Path
        export_path.extend(url.path.segments)

        # Query string
        if url.query:
            export_path.append(url.query.replace("=", "-").replace("&", "."))

        # File extension
        if (
            add_file_extension
            and url.path.segments
            and "." not in url.path.segments[-1]
        ):
            ext = self.get_export_file_extension(
                url,
                content_type
            )
            if ext:
                export_path[-1] += ext

        # Customization
        e = self.resolving_export_path(
            url=url,
            resolution=resolution,
            export_path=export_path
        )

        return e.export_path

    def get_export_file_extension(
            self,
            url: URL,
            content_type: str = None) -> str:

        if content_type == "text/html" or not content_type:
            return self.export_file_extension

        return guess_extension(content_type)

    def iter_pending_tasks(self, publishable=None, languages=None):
        if publishable:
            pub_tasks = self._pending_tasks.get(publishable.id)
            if pub_tasks:
                for lang, action in pub_tasks.iteritems():
                    if languages is None or lang in languages:
                        yield action, publishable.id, lang
        else:
            for pub_id, pub_tasks in self._pending_tasks.iteritems():
                for lang, action in pub_tasks.iteritems():
                    if languages is None or lang in languages:
                        yield action, pub_id, lang

    def has_pending_tasks(self, publishable=None, languages=None):
        for task in self.iter_pending_tasks(publishable, languages):
            return True
        else:
            return False

    def clear_pending_tasks(self, publishable=None, languages=None):
        if publishable:
            if languages:
                pub_tasks = self._pending_tasks.get(publishable.id)
                for lang in languages:
                    try:
                        del pub_tasks[language]
                    except KeyError:
                        pass
                if not pub_tasks:
                    del self._pending_tasks[publishable.id]
            else:
                del self._pending_tasks[publishable.id]
        else:
            for pub_id, pub_tasks in list(self._pending_tasks.iteritems()):
                if languages:
                    pub_tasks = self._pending_tasks.get(pub_id)
                    for lang in languages:
                        try:
                            del pub_tasks[language]
                        except KeyError:
                            pass
                    if not pub_tasks:
                        del self._pending_tasks[pub_id]
                else:
                    del self._pending_tasks[pub_id]

    def get_pending_task(self, publishable, language):
        pub_tasks = self._pending_tasks.get(publishable.id)
        if pub_tasks is not None:
            return pub_tasks.get(language)
        return None

    def set_pending_task(self, publishable, language, task):
        if task is None:
            pub_tasks = self._pending_tasks.get(publishable.id)
            if pub_tasks is not None:
                try:
                    del pub_tasks[language]
                except KeyError:
                    pass
                else:
                    if not pub_tasks:
                        del self._pending_tasks[publishable.id]
        else:
            pub_tasks = self._require_pub_tasks(publishable.id)
            pub_tasks[language] = task

    def _require_pub_tasks(self, publishable_id):
        pub_tasks = self._pending_tasks.get(publishable_id)
        if pub_tasks is None:
            pub_tasks = OOBucket()
            self._pending_tasks[publishable_id] = pub_tasks
        return pub_tasks

    def set_exported_content_tags(self, item, language, tags):

        entry = (item.id, language)
        prev_tags = self._entry_tags.get(entry)
        self._entry_tags[entry] = tags

        if prev_tags:
            for tag in prev_tags:
                tag_entries = self._entries_by_tag.get(tag)
                if tag_entries:
                    tag_entries.remove(entry)

        for tag in tags:
            tag_entries = self._entries_by_tag.get(tag)
            if tag_entries is None:
                tag_entries = OOTreeSet()
                self._entries_by_tag[tag] = tag_entries
            tag_entries.insert(entry)

    def invalidate_exported_content(
        self,
        item,
        language = None,
        cache_part = None
    ):
        scope = normalize_scope(
            item.get_cache_invalidation_scope(
                language=language,
                cache_part=cache_part
            )
        )
        self._invalidate_exported_scope(scope)

    def _invalidate_exported_scope(self, scope, task="mod"):

        # Invalidate everything
        if scope is whole_cache:
            for publishable, language in iter_all_exportable_content():
                pub_tasks = self._require_pub_tasks(publishable.id)
                pub_tasks.setdefault(language, task)

        # Invalidate a single tag
        elif isinstance(scope, basestring):
            for publishable_id, language in self._entries_by_tag.get(scope, ()):
                pub_tasks = self._require_pub_tasks(publishable_id)
                pub_tasks.setdefault(language, task)

        # Invalidate an intersection of tags
        elif isinstance(scope, tuple):

            matching_entries = None

            for tag in scope:
                tagged_entries = self._entries_by_tag.get(tag, ())

                if matching_entries is None:
                    matching_entries = set(tagged_entries)
                else:
                    matching_entries.intersection_update(tagged_entries)

                if not matching_entries:
                    break

            for publishable_id, language in matching_entries:
                pub_tasks = self._require_pub_tasks(publishable_id)
                pub_tasks.setdefault(language, task)

        # Invalidate a collection of scopes
        elif isinstance(scope, Iterable):
            for subscope in scope:
                self._invalidate_exported_scope(subscope)

        # Invalid scope
        else:
            raise TypeError(
               f"Invalid scope ({scope}). "
                "Expected whole_cache, a string, a tuple of strings or a "
                "collection of any of those elements."
            )