Exemple #1
0
class RequestHandler:

    exposed = True

    traversed = Event(doc="""
        An event triggered when the
        L{dispatcher<cocktail.controllers.dispatcher.Dispatcher>} passes
        through or reaches the controller while looking for a request handler.

        @ivar path: A list-like object containing the remaining path components
            that haven't been consumed by the dispatcher yet.
        @type path: L{PathProcessor
                      <cocktail.controllers.dispatcher.PathProcessor>}

        @ivar config: The configuration dictionary with all entries that apply
            to the controller. Note that this dictionary can change as the
            dispatcher traverses through the handler hierarchy. Event handlers
            are free to modify it to change configuration as required.
        @type config: dict
        """)

    before_request = Event(doc="""
        An event triggered before the controller (or one of its nested
        handlers) has been executed.
        """)

    after_request = Event(doc="""
        An event triggered after the controller (or one of its nested handlers)
        has been executed. This is guaranteed to run, regardless of errors.
        """)

    exception_raised = Event(doc="""
        An event triggered when an exception is raised by the controller or one
        of its descendants. The event is propagated up through the handler
        chain, until it reaches the top or it is stoped by setting its
        L{handled} attribute to True.

        @param exception: The exception being reported.
        @type exception: L{Exception}

        @param handled: A boolean indicating if the exception has been dealt
            with. Setting this to True will stop the propagation of the event
            up the handler chain, while a False value will cause the exception
            to continue to be bubbled up towards the application root and,
            eventually, the user.
        @type handled: bool
        """)

    @cherrypy.expose
    def default(self, *args, **kwargs):
        # Compatibility with the standard CherryPy dispatcher
        return self.__call__(*args, **kwargs)
class StatusTracker(object):
    """An object that allows clients to be kept up to date of the progress of
    an L{export operation<StaticSiteDestination.export>}.
    """
    __metaclass__ = EventHub

    beginning = Event(
        """An event triggered after an exporter has finished its preparations
        and is ready to start the export operation.
        
        @ivar context: The L{context<StaticSiteDestination.setup.context>}
            used by the export operation to maintain its state.
        @type context: dict
        """)

    file_processed = Event("""An event triggered after file has been processed.

        This event will be triggered at least once for each generated file on
        the exportation process. 

        The event will be triggered regardless of the outcome of the exporting
        process, and even if the item is discarded and not exported. Clients
        can differentiate between these cases using the L{status} attribute.

        @ivar file: The generated file path.
        @type file: str

        @ivar status: The status for the processed file. Will be one of the
            following:
                - ignored: The file is not published or marked as not able
                  to be exported, and has been skipped.
                - not-modified: The file hasn't been modified since the last
                  time it was exported, and the operation has been flagged as
                  "L{update only<StaticSiteDestination.export.update_only>}".
                - exported: The file has been correctly exported.
                - failed: The export attempt raised an exception.
        @type status: str

        @ivar error: Only relevant if L{status} is 'failed'. A reference
            to the exception raised while trying to export the item.
        @type error: L{Exception}

        @ivar error_handled: Only relevant if L{status} is 'failed'. Allows
            the event response code to capture an export error. If set to True,
            the exporter will continue 
        @type error_handled: bool

        @ivar context: The L{context<StaticSiteDestination.setup.context>}
            used by the export operation to maintain its state.
        @type context: dict
        """)
Exemple #3
0
        class Foo:

            spammed = Event()

            @event_handler
            def handle_spammed(e):
                pass
Exemple #4
0
class Node(Handler):
    """Base class for nodes in a RESTful API.

    .. attribute:: children

        The child nodes of this node.

    .. attribute:: wildcards

        The list of wildcard nodes (anonymous, argument based nodes) of this
        node.

    .. attribute:: arguments

        The positional arguments expected by the node.
    """

    children: Mapping[str, Type["Node"]] = {}
    wildcards: Sequence[Type["Node"]] = []
    arguments: Sequence[schema.Member] = []
    response_type: str = None
    private: bool = False
    open_api: yaml_template = None

    traversed = Event(doc = """
        An event triggered when the CherryPy dispatcher passes through or
        reaches this node while dispatching a request.

        .. attribute:: path

            A list-like object containing the remaining path components
            that haven't been consumed by the dispatcher yet.
        """)

    def __init__(self):

        super().__init__()

        children = self.children
        wildcards = self.wildcards
        arguments = self.arguments

        self.__name = None
        self.__parent = None
        self.children = {}
        self.wildcards = []
        self.arguments = []
        self.__methods = {}

        # Instantiate child nodes
        for name, node_cls in children.items():
            self.add(node_cls(), name)

        # Instantiate wildcard nodes
        for node_cls in wildcards:
            self.add(node_cls())

        # Add positional arguments
        for arg in arguments:
            self.add_argument(arg)

        # Instantiate methods
        for key in dir(self):
            value = getattr(self, key, None)
            if isinstance(value, type) and issubclass(value, Method):
                assert key == key.upper(), \
                    "HTTP methods must be all in uppercase"
                method = self.create_method(value, key)
                self.__methods[key] = method

    def __repr__(self):
        return f"{self.__class__.__name__} {self.get_path()}"

    def get_path(self, arguments: Mapping[str, Any] = None) -> str:
        """Gets the string path for the node"""
        return "/" + "/".join(self.get_path_components(arguments))

    def get_path_components(
            self,
            arguments: Mapping[str, Any] = None) -> List[str]:
        """Gets a list containing the distinct components of the node's
        path.
        """
        if self.__parent:
            components = self.__parent.get_path_components()
        else:
            components = []

        if self.__name:
            components.append(self.__name)

        if self.arguments:
            for arg in self.arguments:

                if arguments:
                    try:
                        value = arguments[arg.name]
                    except KeyError:
                        if arg.required:
                            raise
                        else:
                            pass
                    else:
                        if arg.serialize_request_value:
                            value = arg.serialize_request_value(value)

                        components.append(value)

                elif arg.required:
                    components.append("{%s}" % arg.name)

        return components

    def add(self, node: "Node", name: str = None):
        """Add a nested node."""

        if node.__parent:
            raise NodeRelocationError(node, node.__parent, self, name)

        node.__parent = self

        if name:
            node.__name = name
            self.children[node.__name] = node
        else:
            self.wildcards.append(node)

    def add_argument(self, argument: schema.Member):
        """Declare a position argument on the node."""
        self.arguments.append(argument)

    @property
    def name(self) -> Optional[str]:
        """The name of the node."""
        return self.__name

    @property
    def parent(self) -> Optional["Node"]:
        """The parent node that contains the node."""
        return self.__parent

    def iter_nodes(
            self,
            include_self: bool = False,
            recursive: bool = False) -> Iterable["Node"]:
        """Iterates over the node contained within the node."""

        if include_self:
            yield self

        if recursive:
            for child in self.children.values():
                yield from child.iter_nodes(
                    include_self=True,
                    recursive=True
                )

            for wildcard in self.wildcards:
                yield from wildcard.iter_nodes(
                    include_self=True,
                    recursive=True
                )
        else:
            yield from self.children.values()

    @property
    def methods(self) -> Dict[str, "Method"]:
        """The methods implemented by this node."""
        return self.__methods

    def create_method(
            self,
            method_class: Type[Method],
            method_name: str) -> Method:
        """Creates the implementation of a given HTTP method for this node.

        This method allows nodes to apply common initialization to their
        methods.
        """
        return method_class(self, method_name)

    def _cp_dispatch(self, vpath):

        # Override the default CherryPy dispatching algorithm

        self.traversed(path = vpath)

        if vpath:
            child = self.children.get(vpath[0])
            if child:
                vpath.pop(0)
                return child

        if (
            self.arguments
            and getattr(cherrypy.request, "wildcard", None) is not self
            and not self.process_arguments(vpath)
        ):
            return None

        if vpath:
            for wildcard in self.wildcards:
                if wildcard.process_arguments(vpath):
                    cherrypy.request.wildcard = wildcard
                    return wildcard

            return None

        return self

    def process_arguments(
            self,
            path: Sequence[str]) -> Optional[Dict[str, Any]]:
        """Extract the positional arguments for the node for the current
        request.

        If the path for the current request doesn't match the expected
        arguments (either not enough path components are supplied, or one of
        the extracted values doesn't pass validation) the method will leave
        `path` untouched and return 'None'.

        If all expected values can be correctly extracted and validated, the
        resulting values are merged into the 'cherrypy.request.arguments'
        mapping, along with any preceding arguments previously loaded by an
        ancestor node. The method will pop all extracted values from `path`
        (to indicate the matching path components have been consumed), and
        return a mapping with the values extracted by this node.
        """
        arg_values = {}

        for n, arg in enumerate(self.arguments):
            try:
                value = path[n]
            except IndexError:
                if arg.required:
                    return None
                value = arg.produce_default()
            else:
                try:
                    value = arg.parse(value)
                except ValueError:
                    return None

            if not arg.validate(value):
                return None

            arg_values[arg.name] = value

        for v in arg_values:
            path.pop(0)

        # Store values in cherrypy.request.arguments
        try:
            request_arguments = cherrypy.request.arguments
        except AttributeError:
            cherrypy.request.arguments = arg_values
        else:
            request_arguments.update(arg_values)

        return arg_values

    def handle_request(self):
        try:
            method = self.__methods[cherrypy.request.method]
        except KeyError:
            raise cherrypy.HTTPError(405, "Not Implemented") from None

        return method()

    class OPTIONS(Method):
        """Default response for CORS pre-flight requests.

        See `cocktail.web.Handler.handle_cors` for the actual implementation of
        CORS headers.
        """

        responses = {
            204: {}
        }

        def respond(self):
            cherrypy.response.status = 204
Exemple #5
0
class SASSCompilation(object):

    validating_theme = Event()
    custom_functions = {}

    class ResolvingImportEventInfo(EventInfo):

        uri = None
        path = None
        code = None
        source_map = None

        def add_code(self, code):
            if self.code is None:
                self.code = [code]
            else:
                self.code.append(code)

        @property
        def resolution(self):

            if self.path:
                resolution = [self.path]
            elif self.code:
                resolution = [self.uri]
            else:
                return None

            if self.code is not None:
                resolution.append("".join(self.code))

                if self.source_map:
                    resolution.append(self.source_map)

            return [tuple(resolution)]

    resolving_import = Event(event_info_class=ResolvingImportEventInfo)

    def __init__(self, theme=None):

        if theme is None:
            theme = get_theme()

        if theme != "default":
            theme_is_valid = self.validating_theme(theme=theme,
                                                   valid=False).valid
            if not theme_is_valid:
                raise InvalidSASSTheme(theme)

        self.__imported_uris = set()
        self.__theme = theme

    @property
    def theme(self):
        return self.__theme

    def compile(self, **kwargs):

        if sass is None:
            raise sass_import_error

        importers = kwargs.get("importers")
        if importers is None:
            importers = []
            kwargs["importers"] = importers

        importers.insert(0, (0, self.resolve_import))

        custom_functions = self.custom_functions
        extra_custom_functions = kwargs.get("custom_functions")
        if extra_custom_functions is not None:
            custom_functions = custom_functions.copy()
            custom_functions.update(extra_custom_functions)

        kwargs["custom_functions"] = custom_functions
        return sass.compile(**kwargs)

    def resolve_import(self, uri):

        # Prevent importing dependencies more than once
        if uri in self.__imported_uris:
            return ((uri, ""), )
        else:
            self.__imported_uris.add(uri)

        e = self.resolving_import(uri=uri, theme=self.__theme)
        resolution = e.resolution

        if resolution is None:
            if resource_repositories.is_repository_uri(uri):

                if not uri.endswith(".scss"):
                    uri += ".scss"

                location = resource_repositories.locate(uri)
                if os.path.exists(location):
                    resolution = ((location, ), )
                else:
                    path, file_name = os.path.split(location)
                    resolution = ((os.path.join(path, "_" + file_name), ), )

        return resolution
Exemple #6
0
class Controller(RequestHandler):

    context = context

    # Default configuration
    #------------------------------------------------------------------------------
    default_rendering_engine = "cocktail"
    default_rendering_format = "html"

    # Execution and lifecycle
    #------------------------------------------------------------------------------
    exposed = True

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

        if args:
            raise cherrypy.NotFound()

        try:
            if self.ready:
                self.submit()
                self.successful = True
            self.processed()
        except Exception as ex:
            self.handle_error(ex)

        return self.render()

    def submit(self):
        pass

    @cached_getter
    def submitted(self):
        return True

    @cached_getter
    def valid(self):
        return True

    @cached_getter
    def ready(self):
        return self.submitted and self.valid

    successful = False

    handled_errors = ()

    def handle_error(self, error):
        if isinstance(error, self.handled_errors):
            self.output["error"] = error
        else:
            raise

    processed = Event(doc="""
        An event triggered after the controller's logic has been invoked.
        """)

    # Input / Output
    #------------------------------------------------------------------------------
    @cached_getter
    def params(self):
        return FormSchemaReader()

    @cached_getter
    def output(self):
        return {}

    # Rendering
    #------------------------------------------------------------------------------
    rendering_format_param = "format"
    allowed_rendering_formats = frozenset(
        ["html", "html4", "html5", "xhtml", "json"])

    def _get_rendering_engine(self, engine_name):
        warn(
            "Controller._get_rendering_engine() is deprecated, use "
            "cocktail.controllers.renderingengines.get_rendering_engine() instead",
            DeprecationWarning,
            stacklevel=2)
        return get_rendering_engine(
            engine_name,
            cherrypy.request.config.get("rendering.engine_options"))

    @cached_getter
    def rendering_format(self):

        format = None

        if self.rendering_format_param:
            format = cherrypy.request.params.get(self.rendering_format_param)

        if format is None:
            format = cherrypy.request.config.get("rendering.format",
                                                 self.default_rendering_format)

        return format

    @cached_getter
    def rendering_engine(self):
        engine_name = cherrypy.request.config.get(
            "rendering.engine", self.default_rendering_engine)
        return get_rendering_engine(
            engine_name,
            cherrypy.request.config.get("rendering.engine_options"))

    @cached_getter
    def view_class(self):
        return None

    def render(self):

        format = self.rendering_format

        if format and format in self.allowed_rendering_formats:
            renderer = getattr(self, "render_" + format, None)
        else:
            renderer = None

        if renderer is None:
            raise ValueError("%s can't render its response in '%s' format" %
                             (self, format))

        return renderer()

    render_as_fragment = False

    def _render_template(self):

        view_class = self.view_class

        if view_class:
            output = self.output
            output["submitted"] = self.submitted
            output["successful"] = self.successful
            return self.rendering_engine.render(
                output,
                format=self.rendering_format,
                fragment=self.render_as_fragment,
                template=view_class)
        else:
            return ""

    def render_html(self):
        return self._render_template()

    def render_html4(self):
        return self._render_template()

    def render_html5(self):
        return self._render_template()

    def render_xhtml(self):
        return self._render_template()

    def render_json(self):
        cherrypy.response.headers["Content-Type"] = "text/plain"
        return dumps(self.output)

    @classmethod
    def copy_config(cls, **kwargs):
        config = getattr(cls, "_cp_config", None)
        config = {} if config is None else config.copy()
        config.update(kwargs)
        return config
Exemple #7
0
class CMSController(BaseCMSController):

    application_settings = None

    # Application events
    item_saved = Event(doc="""
        An event triggered after an item is inserted or modified.

        @ivar item: The saved item.
        @type item: L{Item<woost.models.Item>}

        @ivar user: The user who saved the item.
        @type user: L{User<woost.models.user.User>}

        @ivar is_new: True for an insertion, False for a modification.
        @type is_new: bool

        @ivar change: The revision describing the changes to the item.
        @type change: L{Change<woost.models.Change>}
        """)

    item_deleted = Event(doc="""
        An event triggered after an item is deleted.

        @ivar item: The deleted item.
        @type item: L{Item<woost.models.Item>}

        @ivar user: The user who deleted the item.
        @type user: L{User<woost.models.user.User>}

        @ivar change: The revision describing the changes to the item.
        @type change: L{Change<woost.models.Change>}
        """)

    producing_output = Event(doc="""
        An event triggered to allow setting site-wide output for controllers.

        @ivar controller: The controller that is producing the output.
        @type controller: L{BaseCMSController
                            <woost.controllers.basecmscontroller
                            .BaseCMSController>}

        @ivar output: The output for the controller. Event handlers can modify
            it as required.
        @type output: dict
        """)

    # Application modules
    LanguageModule = None
    AuthenticationModule = None

    # Webserver configuration
    virtual_path = "/"

    # Enable / disable confirmation dialogs when closing an edit session. This
    # setting exists mainly to disable the dialogs on selenium test runs.
    closing_item_requires_confirmation = True

    # A dummy controller for CherryPy, that triggers the cocktail dispatcher.
    # This is done so dynamic dispatching (using the resolve() method of
    # request handlers) can depend on session setup and other requirements
    # being available beforehand.
    class ApplicationContainer(object):
        def __init__(self, cms):
            self.__cms = cms
            self.__dispatcher = Dispatcher()

            # Set the default location for file-based sessions
            sconf = session.config
            using_file_sessions = (sconf.get("session.type") == "file")
            using_dbm_sessions = (sconf.get("session.type") == "dbm")
            lock_dir_missing = (not using_file_sessions
                                and not sconf.get("session.lock_dir"))
            data_dir_missing = (
                (using_file_sessions or
                 (using_dbm_sessions and not sconf.get("session.dbm_dir")))
                and not sconf.get("session.data_dir"))

            if lock_dir_missing or data_dir_missing:

                session_path = app.path("sessions")
                if not os.path.exists(session_path):
                    os.mkdir(session_path)

                if lock_dir_missing:
                    sconf["session.lock_dir"] = session_path

                if data_dir_missing:
                    sconf["session.data_dir"] = session_path

            if not sconf.get("session.secret"):

                session_key_path = app.path(".session_key")
                if os.path.exists(session_key_path):
                    with open(session_key_path, "r") as session_key_file:
                        session_key = session_key_file.readline()
                else:
                    with open(session_key_path, "w") as session_key_file:
                        session_key = sha("".join(
                            choice(ascii_letters)
                            for i in range(10))).hexdigest()
                        session_key_file.write(session_key)

                sconf["session.secret"] = session_key

            # Create the folders for uploaded files
            upload_path = app.path("upload")
            if not os.path.exists(upload_path):
                os.mkdir(upload_path)

            temp_path = app.path("upload", "temp")
            if not os.path.exists(temp_path):
                os.mkdir(temp_path)

            async_uploader.temp_folder = temp_path

        @cherrypy.expose
        def default(self, *args, **kwargs):
            # All requests are forwarded to the nested dispatcher:
            return self.__dispatcher.respond(args, self.__cms)

        # Static resources
        resources = folder_publisher(
            resource_filename("woost.views", "resources"))

        cocktail = folder_publisher(
            resource_filename("cocktail.html", "resources"))

    def session_middleware(self, app):
        return SessionMiddleware(app, session.config)

    @property
    def language(self):
        warn("CMSController.language is deprecated, use app.language instead",
             DeprecationWarning,
             stacklevel=2)
        return app.language

    @property
    def authentication(self):
        warn(
            "CMSController.authentication is deprecated, use "
            "app.authentication instead",
            DeprecationWarning,
            stacklevel=2)
        return app.authentication

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

        BaseCMSController.__init__(self, *args, **kwargs)

        if self.LanguageModule:
            warn(
                "CMSController.LanguageModule is deprecated, use "
                "app.language instead", DeprecationWarning)
            app.language = self.LanguageModule()

        if self.AuthenticationModule:
            warn(
                "CMSController.AuthenticationModule is deprecated, use "
                "app.authentication instead", DeprecationWarning)
            app.authentication = self.AuthenticationModule()

    def run(self, block=True):

        self.mount()

        if hasattr(cherrypy.engine, "signal_handler"):
            cherrypy.engine.signal_handler.subscribe()

        if hasattr(cherrypy.engine, "console_control_handler"):
            cherrypy.engine.console_control_handler.subscribe()

        cherrypy.engine.start()

        if block:
            cherrypy.engine.block()
        else:
            cherrypy.engine.wait(cherrypy.engine.states.STARTED)

    def mount(self):

        app = cherrypy.Application(self.ApplicationContainer(self))

        # Session middleware
        app.wsgiapp.pipeline.append(
            ("beaker_session", self.session_middleware))

        return cherrypy.tree.mount(app, self.virtual_path,
                                   self.application_settings)

    def resolve(self, path):

        # Allow application modules (ie. language) to process the URI before
        # resolving the requested publishable item
        self._process_path(path)

        request = cherrypy.request

        # Item resolution
        publishable = self._resolve_path(path)
        self.context["publishable"] = publishable

        # HTTP/HTTPS check
        self._apply_https_policy(publishable)

        # Check maintenance mode
        self._maintenance_check(publishable)

        # Controller resolution
        controller = publishable.resolve_controller()

        if controller is None:
            raise cherrypy.NotFound()

        # Add the selected language to the current URI
        if publishable.per_language_publication:
            if not request.language_specified:
                location = Location.get_current()
                location.path_info = app.language.translate_uri()
                location.go()

        # Remove the language selection from the current URI
        elif request.language_specified:
            location = Location.get_current()
            location.path_info = \
                "/" + "/".join(location.path_info.strip("/").split("/")[1:])
            location.go()

        return controller

    def canonical_redirection(self, path_resolution):
        """Redirect the current request to the canonical URL for the selected
        publishable element.
        """
        publishable = path_resolution.item

        # Find the canonical path for the element
        canonical_path = app.url_resolver.get_path(publishable)

        if canonical_path is None:
            return

        canonical_path = canonical_path.strip("/")
        canonical_path = (canonical_path.split("/") if canonical_path else [])

        # The current request matches the canonical path, do nothing
        if canonical_path == path_resolution.matching_path:
            return

        canonical_uri = "".join(
            percent_encode(c) for c in "/" + "/".join(
                step
                for step in (canonical_path + path_resolution.extra_path)))

        if publishable.per_language_publication:
            canonical_uri = \
                app.language.translate_uri(canonical_uri)

        if cherrypy.request.query_string:
            canonical_uri = canonical_uri + \
                "?" + cherrypy.request.query_string

        raise cherrypy.HTTPRedirect(canonical_uri)

    def _process_path(self, path):

        # Invoke the language module to set the active language
        app.language.process_request(path)

    def _maintenance_check(self, publishable):

        if isinstance(publishable, File):
            return

        config = Configuration.instance
        website = get_current_website()

        if (config.down_for_maintenance
                or (website and website.down_for_maintenance)):
            headers = cherrypy.request.headers
            client_ip = headers.get("X-Forwarded-For") \
                     or headers.get("Remote-Addr")

            if client_ip not in config.maintenance_addresses:
                raise cherrypy.HTTPError(503, "Site down for maintenance")

    def _resolve_path(self, path):

        unicode_path = [try_decode(step) for step in path]
        path_resolution = app.url_resolver.resolve_path(unicode_path)

        if path_resolution:
            publishable = path_resolution.item

            for step in path_resolution.matching_path:
                path.pop(0)

            self.canonical_redirection(path_resolution)
        else:
            website = get_current_website()
            publishable = website.home if website else None

        return publishable

    def uri(self, publishable, *args, **kwargs):
        """Obtains the canonical absolute URI for the given item.

        @param publishable: The item to obtain the canonical URI for.
        @type publishable: L{Publishable<woost.models.publishable.Publishable>}

        @param args: Additional path components to append to the produced URI.
        @type args: unicode

        @param kwargs: Key/value pairs to append to the produced URI as query
            string parameters.
        @type kwargs: (unicode, unicode)

        @return: The item's canonical URI, or None if no matching URI can be
            constructed.
        @rtype: unicode
        """
        warn("CMS.uri() is deprecated, use Publishable.get_uri() instead",
             DeprecationWarning,
             stacklevel=2)

        # User defined URIs
        if isinstance(publishable, URI):
            uri = publishable.uri

        # Regular elements
        else:
            uri = app.url_resolver.get_path(publishable)

            if uri is not None:

                if publishable.per_language_publication:
                    uri = app.language.translate_uri(uri)

                uri = self.application_uri(uri, *args, **kwargs)

        if uri:
            uri = "".join(percent_encode(c) for c in uri)

        return uri

    def translate_uri(self, path=None, language=None):
        return self.application_uri(
            app.language.translate_uri(path=path, language=language))

    def image_uri(self, element, factory="default"):

        if not isinstance(element, (int, basestring)):
            if isinstance(element, type) \
            or not getattr(element, "is_inserted", False):
                element = element.full_name
            elif hasattr(element, "id"):
                element = element.id

        return self.application_uri("images", element, factory)

    def validate_publishable(self, publishable):

        if not publishable.is_published():
            raise cherrypy.NotFound()

        user = get_current_user()

        user.require_permission(ReadPermission, target=publishable)
        user.require_permission(ReadTranslationPermission,
                                language=get_language())

    def _establish_active_website(self):
        location = Location.get_current_host()
        website = Configuration.instance.get_website_by_host(location.host)
        set_current_website(website)

        if website is None:
            raise cherrypy.HTTPError(404, "Unknown hostname: " + location.host)

    @event_handler
    def handle_traversed(cls, event):

        datastore.sync()

        cms = event.source

        cms.context.update(cms=cms, publishable=None)

        # Determine the active website
        cms._establish_active_website()

        # Set the default language
        language = app.language.infer_language()
        set_language(language)

        # Invoke the authentication module
        app.authentication.process_request()

    @event_handler
    def handle_before_request(cls, event):

        cms = event.source

        publishable = cms.context.get("publishable")

        if publishable is not None:

            # Possibly redirect to another website, if the selected publishable is
            # specific to another website
            cms._apply_website_exclusiveness(publishable)

            # Validate access to the requested item
            cms.validate_publishable(publishable)

            # Set the content type and encoding
            content_type = publishable.mime_type
            if content_type:
                encoding = publishable.encoding
                if encoding:
                    content_type += ";charset=" + encoding
                cherrypy.response.headers["Content-Type"] = content_type

            # TODO: encode / decode the request based on the 'encoding' member?

    @event_handler
    def handle_producing_output(cls, event):
        # Set application wide output parameters
        cms = event.source
        event.output.update(
            cms=cms,
            user=get_current_user(),
            publishable=event.controller.context.get("publishable"))

    @event_handler
    def handle_exception_raised(cls, event):

        # Couldn't establish the active website: show a generic error
        if get_current_website() is None:
            return

        error = event.exception
        controller = event.source

        content_type = cherrypy.response.headers.get("Content-Type")
        pos = content_type.find(";")

        if pos != -1:
            content_type = content_type[:pos]

        if content_type in ("text/html", "text/xhtml"):

            error_page, status = event.source.get_error_page(error)
            response = cherrypy.response

            if status:
                response.status = status

            if error_page:
                event.handled = True

                # HTTP/HTTPS check
                controller._apply_https_policy(error_page)

                controller.context.update(
                    original_publishable=controller.context["publishable"],
                    publishable=error_page)

                error_controller = error_page.resolve_controller()

                # Instantiate class based controllers
                if isinstance(error_controller, type):
                    error_controller = error_controller()
                    error_controller._rendering_format = "html"

                response.body = error_controller()

    def get_error_page(self, error):
        """Produces a custom error page for the indicated exception.

        @param error: The exception to describe.
        @type error: Exception

        @return: A tuple comprised of a publishable item to delegate to and an
            HTTP status to set on the response. Either component can be None,
            so that no custom error page is shown, or that the status is not
            changed, respectively.
        @rtype: (L{Document<woost.models.Document>}, int)
        """
        is_http_error = isinstance(error, cherrypy.HTTPError)
        config = Configuration.instance
        page = None
        page_name = None
        status = None

        # Page not found
        if is_http_error and error.status == 404:
            return config.get_setting("not_found_error_page"), 404

        # Service unavailable
        elif is_http_error and error.status == 503:
            return config.get_setting("maintenance_page"), 503

        # Access forbidden:
        # The default behavior is to show a login page for anonymous users, and
        # a 403 error message for authenticated users.
        elif ((is_http_error and error.status == 403)
              or isinstance(error,
                            (AuthorizationError, AuthenticationFailedError))):
            if get_current_user().anonymous:
                publishable = self.context["publishable"]

                while publishable is not None:
                    login_page = publishable.login_page
                    if login_page is not None:
                        return login_page, 403
                    publishable = publishable.parent

                return config.get_setting("login_page"), 403
            else:
                return config.get_setting("forbidden_error_page"), 403

        # Generic error
        elif (is_http_error and error.status == 500) or not is_http_error:
            return config.get_setting("generic_error_page"), 500

        return None, None

    def _apply_website_exclusiveness(self, publishable):
        if (publishable.websites
                and get_current_website() not in publishable.websites):
            raise cherrypy.HTTPRedirect(
                publishable.get_uri(host=publishable.websites[0].hosts[0]))

    def _apply_https_policy(self, publishable):

        policy = Configuration.instance.get_setting("https_policy")
        website = get_current_website()

        if policy == "always":
            Location.require_https()
        elif policy == "never":
            Location.require_http()
        elif policy == "per_page":
            if publishable.requires_https or not get_current_user().anonymous:
                Location.require_https()
            elif not website.https_persistence:
                Location.require_http()

    @event_handler
    def handle_after_request(cls, event):
        datastore.abort()

    images = ImagesController

    async_upload = AsyncUploadController()
    async_upload.uploader = async_uploader

    @cherrypy.expose
    def current_user(self):
        cherrypy.response.headers["Content-Type"] = "text/javascript"
        user = get_current_user()
        return "cocktail.declare('woost'); woost.user = %s;" % dumps(
            {
                "id": user.id,
                "label": translations(user),
                "identifier": user.get(app.authentication.identifier_field),
                "anonymous": user.anonymous
            } if user else None)
Exemple #8
0
class Tracker:

    declaring_tracker = Event()

    inclusion_code = """
        <script type="text/javascript">
          (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
          (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
          m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
          })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
          %(create_tracker_command)s
          %(initialization)s
          %(commands)s
        </script>
        """

    @classmethod
    def get_analytics_script(cls, publishable=None, commands=None):

        config = Configuration.instance

        if publishable is None:
            publishable = app.publishable

        event = cls.declaring_tracker(
            publishable=publishable,
            account=get_setting("x_googleanalytics_account"),
            tracker_parameters={},
            domain=get_setting("x_googleanalytics_domain"),
            template=cls.inclusion_code,
            values={},
            commands=commands or [])

        if not event.account:
            return ""

        commands = event.commands
        parameters = {}

        if event.domain:
            event.tracker_parameters["cookieDomain"] = event.domain

        parameters["create_tracker_command"] = \
            cls._serialize_commands([(
                "create",
                event.account,
                event.tracker_parameters
            )])

        if event.values:
            commands.insert(0, ("set", event.values))

        parameters["initialization"] = (
            "woost.ga.setCustomDefinitions(%s);\n"
            "ga('set', woost.ga.getEventData(document.documentElement));" %
            (json.dumps(
                dict((custom_def.identifier, {
                    "index": i + 1,
                    "type": custom_def.definition_type
                }) for i, custom_def in enumerate(
                    config.x_googleanalytics_custom_definitions)
                     if custom_def.identifier))))
        parameters["commands"] = cls._serialize_commands(commands)
        return event.template % parameters

    @classmethod
    def get_analytics_page_hit_script(cls, publishable=None):
        return cls.get_analytics_script(publishable=publishable,
                                        commands=[("send", "pageview")])

    @classmethod
    def _serialize_commands(cls, commands):
        return "\n".join("ga(%s);" %
                         (", ".join(json.dumps(arg) for arg in cmd))
                         for cmd in commands)
Exemple #9
0
 class Foo(object):
     spammed = Event()
Exemple #10
0
class Basket(object):

    session_key = "woost.extensions.ecommerce.basket"

    created = Event(doc="""
        An event triggered on the class when a new instance is created.

        @ivar basket: A reference to the new instance.
        @type basket: L{Basket}
        """)

    @classmethod
    def get(cls, create_new=True):
        """Obtains the shop order for the current user session.

        If the user had not started an order yet, a new one is created.
        
        :rtype: `~woost.extensions.ecommerce.ecommerceorder.ECommerceOrder`
        """
        order = getattr(cherrypy.request, "woost_ecommerce_order", None)

        if order is None:
            order = cls.restore()

            if order is None and create_new:
                order = ECommerceOrder()
                cls.created(basket=order)

            cherrypy.request.woost_ecommerce_order = order

        return order

    @classmethod
    def pop(cls):
        """Remove the current shop order from the session."""
        order = cls.get(create_new=False)

        if order is not None:
            session.pop(cls.session_key, None)
            del cherrypy.request.woost_ecommerce_order

        return order

    @classmethod
    def empty(cls):
        """Removes all products from the basket."""
        order = cls.get(create_new=False)

        if order is not None:
            for purchase in list(order.purchases):
                purchase.delete()
            datastore.commit()

    @classmethod
    def store(cls):

        order = cls.get()
        order.insert()

        for purchase in order.purchases:
            purchase.insert()

        datastore.commit()
        session[cls.session_key] = order.id

    @classmethod
    def restore(cls):
        session_data = session.get(cls.session_key)

        if session_data is None:
            return None
        else:
            return ECommerceOrder.get_instance(session_data)
Exemple #11
0
class Form:
    """A description of a form based on a schema."""

    controller = None
    actions = (None, )
    process_after = ()

    declared = Event()

    def __init__(self, controller):
        self.controller = controller

    def __str__(self):
        return "%s in %s" % (self.form_id, self.controller)

    @cached_getter
    def model(self):

        source_instance = self.source_instance

        if source_instance is not None \
        and isinstance(source_instance, schema.SchemaObject):
            return source_instance.__class__

        return schema.Schema(get_full_name(self.__class__))

    @cached_getter
    def form_id(self):
        """The name given to the form by its `controller`."""
        return camel_to_underscore(self.__class__.__name__)

    @cached_getter
    def submitted(self):
        """Indicates if this particular form has been submitted in the current
        request.
        """
        return self.controller.submitted \
           and self.controller.action in self.actions

    def submit(self):
        """Load and handle form data when the form is submitted."""
        self.apply_form_data()

    def after_submit(self):
        """Perform additional actions after all forms have been submitted."""
        pass

    @cached_getter
    def valid(self):
        """Indicates if the form is valid and can be submitted."""
        return not self.errors

    @cached_getter
    def errors(self):
        """The `list of validation errors<cocktail.schema.errorlist.ErrorList>`
        for the current form state.
        """
        return schema.ErrorList(
            self.schema.get_errors(self.data, **self.validation_parameters
                                   ) if self.submitted else ())

    @cached_getter
    def adapter(self):
        """The adapter used to obtain the form's customized `schema` from the
        form's `model`.
        """
        return schema.Adapter()

    @cached_getter
    def schema(self):
        """A schema describing the fields and validation logic that make up the
        form.
        """
        if self.model is None:
            raise ValueError("No form model specified for %s" % self)

        adapted_schema = schema.Schema(
            name=self.get_schema_name(),
            schema_aliases=self.get_schema_aliases())
        adapted_schema.is_form = True
        return self.adapter.export_schema(self.model, adapted_schema)

    def get_schema_name(self):
        if self.controller:
            return (get_full_name(self.controller.__class__) + "." +
                    self.__class__.__name__)
        else:
            return get_full_name(self.__class__)

    def get_schema_aliases(self):

        if not self.controller:
            return []

        return [
            get_full_name(cls) + "." + self.__class__.__name__
            for cls in self.controller.__class__.__mro__
            if cls is not FormProcessor and issubclass(cls, FormProcessor)
        ]

    @cached_getter
    def data(self):
        """A dictionary containing the user's input."""

        data = {}

        # First load: set the initial state for the form
        if not self.submitted:
            self.init_data(data)

        # Form submitted: read request data
        # - Start by exporting data from the source instance; this takes care
        #   of read only members
        # - Then, overwrite all editable parameters by reading from the request
        else:
            self.apply_instance_data(data)
            self.read_data(data)

        return data

    def init_data(self, data):
        """Set the form state on first load.

        :param data: The dictionary representing the form state.
        :type data: dict
        """
        if self.source_instance is None:
            # First, apply defaults specified by the form schema
            self.apply_defaults(data)

            # Then, selectively override these with any pre-supplied parameter
            # (useful to pass parameters to a form page)
            get_parameter(self.schema,
                          target=data,
                          undefined="skip",
                          errors="ignore")
        else:
            self.apply_instance_data(data)

    def apply_defaults(self, data):
        """Initialize the form state with defaults supplied by the form's
        schema.

        :param data: The dictionary representing the form state.
        :type data: dict
        """
        self.schema.init_instance(data)

    def apply_instance_data(self, data):
        """Initialize the form state with data from the edited instance.

        :param data: The dictionary representing the form state. The method
            should update it with data from the source instance.
        :type data: dict
        """
        self.adapter.export_object(self.instance,
                                   data,
                                   source_schema=self.model,
                                   target_schema=self.schema)

    def read_data(self, data):
        """Update the form state with data read from the request.

        :param data: The dictionary that request data is read into.
        :type data: dict
        """
        get_parameter(self.schema, target=data, **self.reader_params)

    @cached_getter
    def reader_params(self):
        """The set of parameters to pass to
        `get_parameter <cocktail.controllers.parameters.get_parameter>` when
        reading data from the form.
        """
        return {"errors": "ignore", "undefined": "set_none"}

    @cached_getter
    def instance(self):
        """The instance produced by the form.

        Depending on the form's model, it will be a dictionary or a
        `SchemaObject <cocktail.schema.schemaobject.SchemaObject>` instance.
        """
        return self.source_instance or self.create_instance()

    @cached_getter
    def source_instance(self):
        """An existing instance that should be edited by the form."""
        return None

    def create_instance(self):
        if isinstance(self.model, type):
            return self.model()
        else:
            return {}

    def apply_form_data(self):
        """Fill the edited instance with data from the form."""
        self.adapter.import_object(self.data,
                                   self.instance,
                                   source_schema=self.schema,
                                   target_schema=self.model)

    @cached_getter
    def validation_parameters(self):
        """Context supplied to the form validation process."""
        context = {}

        if isinstance(self.model, PersistentClass):
            context["persistent_object"] = self.instance

        return context
Exemple #12
0
class Extension(Item):
    """Base model for Woost extensions."""

    instantiable = False
    type_group = "setup"
    collapsed_backoffice_menu = True
    edit_node_class = "woost.controllers.backoffice.extensioneditnode." \
                      "ExtensionEditNode"

    installed = False

    @property
    def loaded(self):
        return self.__class__ in _loaded_extensions

    member_order = ("extension_author", "license", "web_page", "description",
                    "enabled")

    extension_author = schema.String(editable=False, listed_by_default=False)

    license = schema.String(editable=False, listed_by_default=False)

    web_page = schema.String(editable=False)

    description = schema.String(editable=False, translated=True)

    enabled = schema.Boolean(required=True, default=False)

    loading = Event("""An event triggered during application start up.""")

    installing = Event(
        """An event triggered when an extension creates its assets, the first
        time it is loaded.
        """)

    @classgetter
    def instance(cls):
        return first(cls.select())

    def __translate__(self, language, **kwargs):
        return translations(self.__class__.name)

    def load(self):
        with _extensions_lock:
            if self.__class__ not in _loaded_extensions:
                _loaded_extensions.add(self.__class__)
                self._load()
                self.loading()

    def _load(self):
        pass

    def install(self):
        if not self.installed:

            def install_extension():
                self._install()
                self.installing()
                self.installed = True

            transaction(install_extension, desist=lambda: self.installed)

    def _install(self):
        pass

    def _create_asset(self, cls, id, **values):
        """Convenience method for creating content when installing an
        extension.        
        """
        asset = cls()
        asset.qname = qname = self.full_name.rsplit(".", 1)[0] + "." + id

        if values:
            for key, value in values.iteritems():
                if value is extension_translations:
                    for language in Configuration.instance.languages:
                        value = translations(qname + "." + key, language)
                        if value:
                            asset.set(key, value, language)
                else:
                    asset.set(key, value)

        asset.insert()
        return asset
class CSRFProtection(object):
    """A class that provides protection against CSRF attacks.

    For background on Cross Site Request Forgery attacks, see the
    `corresponding entry on the OWASP website
    <https://www.owasp.org/index.php/Cross-Site_Request_Forgery_(CSRF)>`.

    The class implements a Synchronizer Token pattern to disallow POST requests
    that don't include a previously supplied token. Tokens are generated on a
    per session basis, and can also be explicitly renewed by calling the
    `renew_session_token` method.

    Tokens are sent to the client using a cookie and injected into form
    submissions and Ajax requests via Javascript. This approach prevents
    interference with response caching, at the cost of preventing POST requests
    from user agents without javascript enabled.

    Only a single instance of this class can be enabled at any one time. To
    install the protection scheme, create an instance of the class and pass it
    to `set_csrf_protection`. Note that the module automatically installs an
    instance of the class by default, so unless the default behavior needs to
    be changed, users don't need to do anything to enable the protection.
    """
    session_key = "cocktail.csrf_protection_token"
    cookie_name = session_key
    token_characters = ascii_letters + digits
    token_length = 40
    field = "__cocktail_csrf_token"
    header = "X-Cocktail-CSRF-Token"

    deciding_injection = Event()
    deciding_validation = Event()

    def should_inject(self):
        """Specifies wether the protection scheme should be applied to the
        current request.

        All requests are protected by default.

        :return: True if the scheme should be enabled, False otherwise.
        :rtype: True
        """
        if not getattr(cherrypy.request, "csrf_token_injection", True):
            return False

        try:
            self.deciding_injection()
        except CSRFProtectionExemption:
            return False
        else:
            return True

    def should_require_token(self):
        """Specifies wether the current request should be protected.

        By default, all POST, PUT and DELETE requests are protected.

        :return: True if the request should be protected, False otherwise.
        :rtype: True
        """
        try:
            self.deciding_validation()
        except CSRFProtectionExemption:
            return False
        else:
            return cherrypy.request.method in ("POST", "PUT", "DELETE")

    def generate_token(self):
        """Generate a Synchronizer Token.

        The default implementation generates a random string containing as many
        characters from `token_characters` as indicated by `token_length`.

        :return: The generated token.
        :rtype: str
        """
        return random_string(self.token_length, self.token_characters)

    def renew_session_token(self):
        """Generate a new token for the active session."""
        session.pop(self.session_key, None)
        return self.get_session_token()

    def get_session_token(self):
        """Get (and set, if new) the token for the active session.

        :return: The token assigned to the active session.
        :rtype: str
        """
        token = session.get(self.session_key)

        if not token:
            token = self.generate_token()
            session[self.session_key] = token

        return token

    def get_request_token(self):
        """Get the token included in the present request.

        The method will check for either an HTTP header (as specified by the
        `header` property) and a POST data field (as specified by the `field`
        property).

        :return: The token included in the request, or None if the request
            contains no Synchronizer token.
        :rtype: str
        """
        return (cherrypy.request.headers.get(self.header)
                or cherrypy.request.params.get(self.field))

    def handle_token_error(self, session_token, received_token):
        """Specifies what should happen when a non authenticated request is
        received.

        Note that a request without a valid token doesn't necessarily equate
        with an attack:
            - Any user agent without javascript enabled will produce this.
            - If the session is lost, existing clients may send tokens that are
              no longer valid.

        The default behavior will raise a `CSRFTokenError` exception (which
        will typically end up producing a 403 HTTP error).

        :param session_token: The token that should have been included in the
            request for it to be considered valid.
        :type session_token: str

        :param received_token: The invalid token that was received instead, or
            None if no token was included.
        :type received_token: str
        """
        raise CSRFTokenError()
Exemple #14
0
class Handler(metaclass=ABCMeta):
    """Base class for request handlers.

    This is an abstract class providing common functionality to
    `~cocktail.web.node.Node` and `~cocktail.web.method.Method`.
    """
    error_responses: ErrorResponseMap = default_error_responses
    allowed_origins: Union[Inherit, AnyOrigin, Set[str]] = inherit
    allowed_headers: Set[str] = {"Content-Type", "Authorization"}

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)

        # Subclasses create their own Handler.error_responses, chained to the
        # one on its parent
        custom_responses = cls.__dict__.get("error_responses", None)
        if custom_responses is not None:
            del cls.error_responses
            cls.error_responses = cls.error_responses.new_child(
                custom_responses)
        else:
            cls.error_responses = cls.error_responses.new_child()

    before_request = Event(doc="""
        An event triggered before the handler (or one of its nested handlers)
        has been executed.
        """)

    before_reading_input = Event(doc="""
        An event triggered just before a method starts parsing request
        parameters.

        Arguments, parameters and request bodies are not yet available at this
        point this point.
        """)

    responding = Event(doc="""
        An event triggered just before a method starts producing the response
        for the current HTTP request.

        Arguments, parameters and request bodies have already been processed at
        this point.
        """)

    after_request = Event(doc="""
        An event triggered after the controller (or one of its nested handlers)
        has been executed. This is guaranteed to run, regardless of errors.
        """)

    exception_raised = Event(doc="""
        An event triggered when an exception is raised by the handler or one
        of its descendants.

        The event is propagated up through the handler chain, until it reaches
        the top or it is stoped by setting its `handled` attribute to True.

        .. attribute:: exception

            The exception being reported.

        .. attribute:: handled

            A boolean indicating if the exception has been dealt with. Setting
            this to True will stop the propagation of the event up the handler
            chain, while a False value will cause the exception to continue to
            be bubbled up towards the application root and, eventually, the
            user.
        """)

    def ascend_handlers(self) -> Iterable["Handler"]:
        """Produces the iterable sequence of `Handler` objects for this handler
        and its ancestors.

        The method yields the handler itself first, and iterates upwards
        towards the root handler, which is returned in last place.
        """
        handler = self
        while handler:
            yield handler
            handler = handler.parent

    def iter_handlers_from_root(self) -> Iterable["Handler"]:
        """Produces the iterable sequence of `Handler` objects for this handler
        and its ancestors, starting from the root.

        The method yields the root handler first, and iterates inwards towards
        the handler itself, which is returned in last place.
        """
        parent = self.parent
        if parent:
            yield from parent.iter_handlers_from_root()
        yield self

    @property
    @abstractmethod
    def parent(self) -> "Handler":
        """Returns the parent handler for this handler."""
        pass

    def allows_origin(self, origin: str) -> bool:
        """Determine if the given origin is acceptable."""
        return origin in self.resolve_allowed_origins()

    def resolve_allowed_origins(self) -> Union[AnyOrigin, Set[str]]:
        """Resolves the value of the `allowed_origins` property, recursing to
        parent handlers when the value is set to `inherit`.

        If the root of the handler tree is reached and the value is still
        `inherit`, the method returns an empty set.
        """
        if self.allowed_origins is inherit:
            parent = self.parent
            return (parent and parent.resolve_allowed_origins() or set())

        return self.allowed_origins

    def __call__(self) -> Any:
        """Handle the current request, producing the appropiate response."""

        try:
            for handler in self.iter_handlers_from_root():
                handler.before_request(handler=self)

            return self.handle_request()

        except Exception as error:

            # Redirections skip error handling
            if (isinstance(error, cherrypy.HTTPError)
                    and error.status >= 300 <= 400):
                raise

            return self.handle_error(error)

        finally:
            for handler in self.iter_handlers_from_root():
                handler.after_request(handler=self)

    @cherrypy.expose
    def index(self, **kwargs):
        return self()

    @abstractmethod
    def handle_request(self):
        pass

    def handle_error(self, error: Exception) -> ErrorResponse:
        """Handle an exception raised during the invocation of this handler or
        one of its nested handlers.
        """
        # Fire an event on the handler and each of its ancestors
        for handler in self.ascend_handlers():
            handler.exception_raised(error=error)

        # Find a suitable response for the exception
        error_response = self.get_error_response(error)
        return error_response.respond(error)

    def get_error_response(self, error: Exception) -> ErrorResponse:
        """Gets the error response that should handle the given exception."""
        for handler in self.ascend_handlers():
            error_response = self.error_responses.get(error.__class__)
            if error_response is not None:
                return error_response

        return None

    def apply_error_response(self, error: Exception,
                             error_response: ErrorResponse) -> Any:
        """Applies the given response to an exception."""
        error_response.applying(error=error)
        cherrypy.response.status = error_response.status
Exemple #15
0
class PaymentGateway(Item):

    instantiable = False
    visible_from_root = False
    payment_gateway_controller_class = \
        "woost.extensions.payments.paymentgatewaycontroller.PaymentGatewayController"

    transaction_notified = Event(
        """An event triggered when the payment gateway notifies the application
        of the outcome of a payment transaction.

        @param payment: The payment that the notification is sent for.
        @type payment: L{Payment<tpv.payment.Payment>}
        """)

    members_order = ["label", "test_mode", "currency"]

    label = schema.String(required=True, translated=True)

    test_mode = schema.Boolean(required=True, default=True)

    currency = schema.String(required=True,
                             enumeration=currency_alpha,
                             translatable_enumeration=False,
                             text_search=False)

    def __translate__(self, language, **kwargs):
        return translations(self.__class__.name, language)

    def initiate_payment(self, payment_id):
        """Begin a payment transaction, redirecting the user to the payment
        gateway.
        
        @param payment_id: The identifier of the payment to execute.
        """
        url, params = self.get_payment_form_data(payment_id, get_language())

        location = Location(url)
        location.method = "POST"
        location.form_data = OrderedDict(params)
        location.go()

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

        website = get_current_website()
        location = Location()
        location.relative = False

        if website.https_policy != "never":
            location.scheme = "https"

        location.host = website.hosts[0]
        location.path_info = "payments/" + str(self.id)
        location.join_path(*args)
        location.query_string.update(kwargs)

        return unicode(location)

    @property
    def handshake_url(self):
        return self.get_payment_url("handshake")

    @property
    def notification_url(self):
        return self.get_payment_url("notification")
Exemple #16
0
class Adapter(object):

    member_created = Event(
        """An event triggered when the adapter generates a new member on a
        target schema.

        :param member: The new member.
        :type member: `~cocktail.schema.Member`
        """)

    def __init__(self,
                 source_accessor=None,
                 target_accessor=None,
                 implicit_copy=IMPLICIT_COPY_DEFAULT,
                 preserve_order=PRESERVE_ORDER_DEFAULT,
                 copy_validations=COPY_VALIDATIONS_DEFAULT,
                 copy_mode=COPY_MODE_DEFAULT,
                 collection_copy_mode=COLLECTION_COPY_MODE_DEFAULT):

        self.import_rules = RuleSet()
        self.import_rules.adapter = self

        self.export_rules = RuleSet()
        self.export_rules.adapter = self

        self.source_accessor = source_accessor
        self.target_accessor = target_accessor

        self.implicit_copy = implicit_copy
        self.preserve_order = preserve_order
        self.copy_validations = copy_validations
        self.copy_mode = copy_mode
        self.collection_copy_mode = collection_copy_mode

    def import_schema(self,
                      source_schema,
                      target_schema=None,
                      parent_context=None):
        if target_schema is None:
            target_schema = Schema()

        self.import_rules.adapt_schema(source_schema,
                                       target_schema,
                                       parent_context=parent_context)
        return target_schema

    def export_schema(self,
                      source_schema,
                      target_schema=None,
                      parent_context=None):
        if target_schema is None:
            target_schema = Schema()

        self.export_rules.adapt_schema(source_schema,
                                       target_schema,
                                       parent_context=parent_context)
        return target_schema

    def import_object(self,
                      source_object,
                      target_object,
                      source_schema=None,
                      target_schema=None,
                      source_accessor=None,
                      target_accessor=None,
                      languages=None,
                      parent_context=None):
        self.import_rules.adapt_object(source_object,
                                       target_object,
                                       source_accessor or self.source_accessor,
                                       target_accessor or self.target_accessor,
                                       source_schema,
                                       target_schema,
                                       languages=languages,
                                       parent_context=parent_context)

    def export_object(self,
                      source_object,
                      target_object,
                      source_schema=None,
                      target_schema=None,
                      source_accessor=None,
                      target_accessor=None,
                      languages=None,
                      parent_context=None):
        self.export_rules.adapt_object(source_object,
                                       target_object,
                                       source_accessor or self.source_accessor,
                                       target_accessor or self.target_accessor,
                                       source_schema,
                                       target_schema,
                                       languages=languages,
                                       parent_context=parent_context)

    def _get_implicit_copy(self):
        return self.__implicit_copy

    def _set_implicit_copy(self, value):
        self.__implicit_copy = value
        self.import_rules.implicit_copy = value
        self.export_rules.implicit_copy = value

    implicit_copy = property(
        _get_implicit_copy,
        _set_implicit_copy,
        doc="""Indicates if members of the source schema that are not
        covered by any adaptation rule should be implicitly copied.

        Note that setting this property will alter the analogous attribute on
        both of the adapter's import and export rule sets (but the opposite
        isn't true; setting X{collection_copy_mode} on either rule set won't
        affect the adapter).

        @type: bool
        """)

    def _get_preserve_order(self):
        return self.__preserve_order

    def _set_preserve_order(self, value):
        self.__preserve_order = value
        self.import_rules.preserve_order = value
        self.export_rules.preserve_order = value

    preserve_order = property(
        _get_preserve_order,
        _set_preserve_order,
        doc="""Indicates if the schemas exported by the adapter attempt to
        preserve the same relative ordering for their members and groups of
        members defined by the source schema.

        Note that setting this property will alter the analogous attribute on
        both of the adapter's import and export rule sets (but the opposite
        isn't true; setting X{preserve_order} on either rule set won't affect
        the adapter).

        @type: bool
        """)

    def _get_copy_validations(self):
        return self.__copy_validations

    def _set_copy_validations(self, copy_validations):
        self.__copy_validations = copy_validations
        self.import_rules.copy_validations = copy_validations
        self.export_rules.copy_validations = copy_validations

    copy_validations = property(
        _get_copy_validations,
        _set_copy_validations,
        doc="""Indicates if validations from the source schema and members
        should be added to adapted schemas and members.

        Note that setting this property will alter the analogous attribute on
        both of the adapter's import and export rule sets (but the opposite
        isn't true; setting X{copy_validations} on either rule set won't
        affect the adapter).

        @type: bool
        """)

    def _get_copy_mode(self):
        return self.__copy_mode

    def _set_copy_mode(self, copy_mode):
        self.__copy_mode = copy_mode
        self.import_rules.copy_mode = copy_mode
        self.export_rules.copy_mode = copy_mode

    copy_mode = property(_get_copy_mode,
                         _set_copy_mode,
                         doc="""
        Indicates the way in which values are copied between objects. This
        should be a function, taking a single parameter (the input value) and
        returning the resulting copy of the value. For convenience, the module
        provides the following functions:

            * L{reference}: This is the default copy mode. It doesn't actually
                perform a copy of the provided value, but rather returns the
                same value unmodified.
            * L{shallow}: Creates a shallow copy of the provided value.
            * L{deep}: Creates a deep copy of the provided value.

        Note that setting this property will alter the analogous attribute on
        both of the adapter's import and export rule sets (but the opposite
        isn't true; setting X{copy_mode} on either rule set won't affect the
        adapter).

        @type: function
        """)

    def _get_collection_copy_mode(self):
        return self.__collection_copy_mode

    def _set_collection_copy_mode(self, copy_mode):
        self.__collection_copy_mode = copy_mode
        self.import_rules.collection_copy_mode = copy_mode
        self.export_rules.collection_copy_mode = copy_mode

    collection_copy_mode = property(
        _get_collection_copy_mode,
        _set_collection_copy_mode,
        doc=""" Indicates the way in which collections are copied between
        objects. This should be a function, taking a single parameter (the
        input collection) and returning the resulting copy of the collection.
        For convenience, the module provides the following functions:

            * L{reference}: This is the default copy mode. It doesn't actually
                perform a copy of the provided collection, but rather returns
                a reference to it. Beware that by using this copy mode, an
                adapted object will share the collection with its source
                object, which may result in unexpected behavior in some cases.
            * L{shallow}: Creates a shallow copy of the collection (a copy of
                the collection itself is made, but its members are copied by
                reference).
            * L{deep}: Creates a deep copy of the collection and its members.

        Note that setting this property will alter the analogous attribute on
        both of the adapter's import and export rule sets (but the opposite
        isn't true; setting X{collection_copy_mode} on either rule set won't
        affect the adapter).

        @type: function
        """)

    def has_rules(self):
        """Indicates if the adapter defines one or more import or export rules.
        @rtype: bool
        """
        return self.export_rules.rules or self.import_rules.rules

    def copy(self,
             mapping,
             export_transform=None,
             import_transform=None,
             import_condition=None,
             export_condition=None,
             rule_position=None,
             properties=None):

        export_rule = Copy(mapping,
                           transform=export_transform,
                           condition=export_condition,
                           properties=properties)

        import_rule = Copy(dict(
            (value, key) for key, value in export_rule.mapping.items()),
                           transform=import_transform,
                           condition=import_condition)

        self.export_rules.add_rule(export_rule, rule_position)
        self.import_rules.add_rule(import_rule, rule_position)

    def exclude(self, members, rule_position=None):

        if isinstance(members, str):
            members = [members]

        exclusion = Exclusion(members)
        self.import_rules.add_rule(exclusion, rule_position)
        self.export_rules.add_rule(exclusion, rule_position)

    def export_read_only(self,
                         mapping,
                         transform=None,
                         condition=None,
                         properties=None,
                         rule_position=None):
        properties = {} if properties is None else properties.copy()
        properties["editable"] = READ_ONLY

        self.export_rules.add_rule(
            Copy(mapping,
                 transform=transform,
                 condition=condition,
                 properties=properties), rule_position)

    def expand(self, member, related_object, adapter, rule_position=None):

        self.export_rules.add_rule(Expansion(member, related_object, adapter),
                                   rule_position)

        self.import_rules.add_rule(
            Contraction(member, related_object, adapter), rule_position)

    def split(self,
              source_member,
              separator,
              target_members,
              rule_position=None):

        self.export_rules.add_rule(
            Split(source_member, separator, target_members), rule_position)

        self.import_rules.add_rule(
            Join(target_members, separator, source_member), rule_position)

    def join(self, source_members, glue, target_member, rule_position=None):

        self.export_rules.add_rule(Join(source_members, glue, target_member),
                                   rule_position)

        self.import_rules.add_rule(Split(target_member, glue, source_members),
                                   rule_position)
Exemple #17
0
class Member(Variable):
    """A member describes the properties and metadata of a unit of data.

    Although not strictly an abstract class, the class is very generic in
    nature, and is inherited by a wealth of other classes that add the
    necessary features to describe a variety of more concrete data types
    (`String`, `Integer`, `Boolean`, etc.).

    Members can also be composited, in order to describe parts of a more complex
    data set, by embedding them within a `Collection` or `Schema`.

    .. attribute:: default

        The default value for the member.

    .. attribute:: required

        Determines if the field requires a value. When set to true, a value of
        None for this member will trigger a validation error of type
        `exceptions.ValueRequiredError`.

    .. attribute:: require_none

        Determines if the field disallows any value other than None.  When set
        to true, a value different than None for this member will trigger a
        validation error of type `exceptions.NoneRequiredError`.

    .. attribute:: enumeration

        Establishes a limited set of acceptable values for the member. If a
        member with this constraint is given a value not found inside the set,
        an `exceptions.EnumerationError` error will be triggered.

    .. attribute:: translated

        Indicates if the member accepts multiple values, each in a different
        language.

    .. attribute:: expression

        An expression used by computed fields to produce their value.
    """

    attached = Event(
        doc="""An event triggered when the member is added to a schema.""")

    validating = Event(
        doc="""An event triggered when a validation of the member begins.

        The main purpose of this event is to modify the parameters of its
        validation context. To implement actual validation rules, check out the
        `_default_validation` and `add_validation` methods.

        :param context: The validation context representing the validation
            process that is about to start.
        """)

    # Groupping and sorting
    member_group = None
    before_member = None
    after_member = None

    # Constraints and behavior
    primary = False
    descriptive = False
    default = None
    required = False
    require_none = False
    enumeration = None
    normalization = None
    expression = None

    # Instance data layout
    accessor = None

    # Copying and adaptation
    _copy_class = None
    source_member = None
    original_member = None
    copy_mode = None

    # Translation
    translated = False
    translation = None
    translation_source = None
    translatable_values = False
    translatable_enumeration = False
    custom_translation_key = None

    # Wether the member is included in full text searches
    text_search = False
    __language_dependant = None

    # Attributes that deserve special treatment when performing a deep copy
    _special_copy_keys = set([
        "__module__", "__class__", "_schema", "_validations_wrapper",
        "_validations", "translation", "translation_source", "original_member",
        "source_member"
    ])

    def __init__(self, name=None, doc=None, **kwargs):
        """Initializes a member, optionally setting its name, docstring and a
        set of arbitrary attributes.

        :param name: The `name` given to the member.
        :type name: str

        :param doc: The docstring to assign to the member.
        :type doc: unicode

        :param kwargs: A set of key/value pairs to set as attributes of the
            member.
        """
        self._name = None
        self._schema = None
        self._validations = OrderedSet()
        self._validations_wrapper = ListWrapper(self._validations)
        self.original_member = self
        self.validation_parameters = {}

        Variable.__init__(self, None)
        self.__type = None

        self.name = name
        self.__doc__ = doc

        validations = kwargs.pop("validations", None)

        for key, value in kwargs.items():
            setattr(self, key, value)

        if validations is not None:
            for validation in validations:
                self.add_validation(validation)

    def __hash__(self):
        return object.__hash__(self)

    def __eq__(self, other):
        return self is other

    def __repr__(self):

        member_desc = self.__class__.__name__
        if (self.__class__.__module__ and
                not self.__class__.__module__.startswith("cocktail.schema.")):
            member_desc = self.__class__.__module__ + "." + member_desc

        return member_desc + " <%s>" % self.get_qualified_name()

    def _get_name(self):
        return self._name

    def _set_name(self, value):

        if self._schema is not None:
            raise exceptions.MemberRenamedError(self, value)

        self._name = value

    name = property(
        _get_name,
        _set_name,
        doc="""The name that uniquely identifies the member on the schema it is
        bound to. Once set it can't be changed (trying to do so will raise a
        `exceptions.MemberRenamedError` exception).
        """)

    def get_qualified_name(self, delimiter=".", include_ns=False):
        member = self
        names = []
        while True:
            if member._name:
                names.append(member._name)
            owner = member._schema
            if owner is None:
                if include_ns:
                    namespace = getattr(member, "__module__", None)
                    if (namespace
                            and not namespace.startswith("cocktail.schema.")):
                        names.append(namespace)
                break
            else:
                member = owner
        names.reverse()
        return delimiter.join(names)

    def _get_schema(self):
        return self._schema

    def _set_schema(self, value):

        if self._schema is not None:
            raise exceptions.MemberReacquiredError(self, value)

        self._schema = value

    schema = property(
        _get_schema,
        _set_schema,
        doc=
        """The `schema <Schema>` that the member is bound to. Once set it can't
        be changed (doing so will raise a `exceptions.MemberReacquiredError`
        exception).
        """)

    def _get_type(self):

        # Resolve string references
        if isinstance(self.__type, str):
            self.__type = import_type(self.__type)

        return self.__type

    def _set_type(self, type):
        self.__type = type

    type = property(
        _get_type,
        _set_type,
        doc=
        """Imposes a data type constraint on the member. All values assigned to
        this member must be instances of the specified data type. Breaking this
        restriction will produce a validation error of type
        `exceptions.TypeCheckError`.

        The value for this constraint can take either a reference to a type
        object or a fully qualified python name. When set using a name, the
        indicated type will be imported lazily, the first time the value for
        this constraint is requested. This can be helpful in avoiding circular
        references between schemas.
        """)

    def _set_exclusive(self, expr):
        self.required = expr

        if isinstance(expr, Expression):
            self.require_none = expr.not_()
        else:
            self.require_none = lambda ctx: not expr(ctx)

    exclusive = property(
        None,
        _set_exclusive,
        doc="""A write only property that eases the definition of members that
        should be required or empty depending on a condition and its negation,
        respectively.

        Should be set to a dynamic expression (a callable object, or an
        instance of `expressions.Expression`).
        """)

    def produce_default(self, instance=None):
        """Generates a default value for the member. Can be overridden (ie. to
        produce dynamic default values).

        :param instance: The instance that the default is produced for.

        :return: The resulting default value.
        """
        if instance is not None and self.name:
            default = getattr(instance, "default_" + self.name, self.default)
        else:
            default = self.default

        if isinstance(default, DynamicDefault):
            default = default()

        return self.normalization(default) if self.normalization else default

    def copy(self, **kwargs):
        """Creates a deep, unbound copy of the member.

        Keyword arguments are assigned as attributes of the new member. It is
        possible to use dotted names to set attributes of nested objects.
        """
        member_copy = self.__deepcopy__({})

        # Set 'primary' before any other property, to avoid constraint
        # violations (f. eg. setting required = False, primary = False would
        # fail if the 'required' property was set first)
        primary = kwargs.pop("primary", None)
        if primary is not None:
            member_copy.primary = primary

        for key, value in kwargs.items():
            obj = member_copy

            name_parts = key.split(".")

            for name in name_parts[:-1]:
                obj = getattr(obj, name)

            setattr(obj, name_parts[-1], value)

        return member_copy

    def __deepcopy__(self, memo):

        # Custom class
        if self._copy_class:
            copy = self._copy_class()

        # Copying a SchemaObject
        elif issubclass(self.__class__, type):
            members = self.members()
            copy = self.__class__(
                self.name, self.__bases__,
                dict((key, value) for key, value in self.__dict__.items()
                     if key not in self._special_copy_keys
                     and key not in members))

        # Regular copy
        else:
            copy = self.__class__()

        memo[id(self)] = copy

        if not isinstance(copy, type):
            for key, value in self.__dict__.items():
                if key not in self._special_copy_keys:
                    copy.__dict__[key] = deepcopy(value, memo)

        copy._validations = list(self._validations)
        memo[id(self._validations)] = copy._validations

        copy._validations_wrapper = ListWrapper(copy._validations)
        memo[id(copy._validations_wrapper)] = copy._validations_wrapper

        copy.source_member = self
        copy.original_member = self.original_member

        return copy

    def detach_from_source_member(self):
        self.source_member = None
        self.original_member = self

    def add_validation(self, validation):
        """Adds a validation function to the member.

        :param validation: A callable that will be added as a validation rule
            for the member. Takes a single positional parameter (a reference to
            a `ValidationContext` object). The callable should produce a sequence
            of `exceptions.ValidationError` instances.
        :type validation: callable

        :return: The validation rule, as provided.
        :rtype: callable
        """
        self._validations.append(validation)
        return validation

    def remove_validation(self, validation):
        """Removes one of the validation rules previously added to a member.

        :param validation: The validation to remove, as previously passed to
            `add_validation`.
        :type validation: callable

        :raise ValueError: Raised if the member doesn't have the indicated
            validation.
        """
        self._validations.remove(validation)

    def validations(self, recursive=True, **validation_parameters):
        """Iterates over all the validation rules that apply to the member.

        :param recursive: Indicates if the produced set of validations should
            include those declared on members contained within the member. This
            parameter is only meaningful on compound members, but is made
            available globally in order to allow the method to be called
            polymorphically using a consistent signature.
        :type recursive: bool

        :return: The sequence of validation rules for the member.
        :rtype: callable iterable
        """
        return self._validations_wrapper

    def validate(self, value, **validation_parameters):
        """Indicates if the given value fulfills all the validation rules
        imposed by the member.

        :param value: The value to validate.

        :param validation_parameters: Additional parameters used to initialize
            the `ValidationContext` instance generated by this validation
            process.
        :type validation_parameters: `ValidationContext`

        :return: True if the given value satisfies the validations imposed by
            this member, False otherwise.
        :rtype: bool
        """
        for error in self.get_errors(value, **validation_parameters):
            return False

        return True

    def get_errors(self, value, **validation_parameters):
        """Tests the given value with all the validation rules declared by the
        member, iterating over the resulting set of errors.

        :param value: The value to evaluate.

        :param validation_parameters: Additional parameters used to initialize
            the `ValidationContext` instance generated by this validation
            process.
        :type validation_parameters: `ValidationContext`

        :return: An iterable sequence of validation errors.
        :rtype: `exceptions.ValidationError` iterable
        """
        context = ValidationContext(self, value, **validation_parameters)

        for error in self._default_validation(context):
            yield error

        for validation in self.validations(**validation_parameters):
            for error in validation(context):
                yield error

    def coerce(self, value: Any, coercion: Coercion,
               **validation_parameters) -> Any:
        """Coerces the given value to conform to the member definition.

        The method applies the behavior indicated by the `coercion` parameter,
        either accepting or rejecting the given value. Depending on the
        selected coercion strategy, rejected values may be transformed into a
        new value or raise an exception.

        New values are both modified in place (when possible) and returned.
        """
        if coercion is Coercion.NONE:
            return value

        errors = self.get_errors(value, **validation_parameters)

        if coercion is Coercion.FAIL:
            errors = list(errors)
            if errors:
                raise InputError(self, value, errors)
        else:
            for error in errors:
                if coercion is Coercion.FAIL_IMMEDIATELY:
                    raise InputError(self, value, [error])
                elif coercion is Coercion.SET_NONE:
                    return None
                elif coercion is Coercion.SET_DEFAULT:
                    return self.produce_default()

        return value

    @classmethod
    def resolve_constraint(cls, expr, context):
        """Resolves a constraint expression for the given context.

        Most constraints can be specified using dynamic expressions instead of
        static values, allowing them to adapt to different validation contexts.
        For example, a field may state that it should be required only if
        another field is set to a certain value. This method normalizes any
        constraint (either static or dynamic) to a static value, given a
        certain validation context.

        Dynamic expressions are formed by assigning a callable object or an
        `expressions.Expression` instance to a constraint value.

        :param expr: The constraint expression to resolve.

        :param context: The validation context that will be made available to
            dynamic constraint expressions.
        :type context: `ValidationContext`

        :return: The normalized expression value.
        """
        if not isinstance(expr, type):
            if isinstance(expr, Expression):
                return expr.eval(
                    context.get_object(-2) if context.parent_context else None)
            elif callable(expr):
                return expr(context)

        return expr

    def _default_validation(self, context):
        """A method implementing the intrinsic validation rules for a member.

        Each member subtype should extend this method to implement its
        constraints and validation rules.

        :param context: The validation context to evaluate.
        :type context: `ValidationContext`

        :return: An iterable sequence of validation errors.
        :rtype: `exceptions.ValidationError` iterable
        """
        # Value required
        if context.value is None:
            if self.resolve_constraint(self.required, context):
                yield exceptions.ValueRequiredError(context)

        # None required
        elif self.resolve_constraint(self.require_none, context):
            yield exceptions.NoneRequiredError(context)

        else:
            # Enumeration
            enumeration = self.resolve_constraint(self.enumeration, context)

            if enumeration is not None and context.value not in enumeration:
                yield exceptions.EnumerationError(context, enumeration)

            # Type check
            type = self.resolve_constraint(self.type, context)

            if type and not isinstance(context.value, type):
                yield exceptions.TypeCheckError(context, type)

    def get_possible_values(self, context=None):
        if self.enumeration is not None:
            if context is None:
                context = ValidationContext(self, None)
            enumeration = self.resolve_constraint(self.enumeration, context)
            if enumeration:
                return enumeration

    def translate_value(self, value, language=None, **kwargs):
        if value is None:
            return translations(self,
                                language=language,
                                suffix=".none",
                                **kwargs)
        elif ((value or value == 0) and self.translatable_values
              or (self.translatable_enumeration and self.enumeration)):
            return translations(self,
                                language=language,
                                suffix=".values.%s" % value)
        else:
            return str(value)

    def translate_error(self, error, language=None, **kwargs):

        for error_class in error.__class__.__mro__:

            try:
                error_class_name = get_full_name(error.__class__)
            except Exception as ex:
                error_class_name = error.__class__.__name__

            error_translation = translations(self,
                                             suffix=".errors." +
                                             error_class_name,
                                             error=error,
                                             language=language)
            if error_translation:
                return error_translation

        # No custom translation for the error, return the stock description
        return translations(error, language=language, **kwargs)

    def get_member_explanation(self, language=None, **kwargs):
        return translations(self,
                            language=language,
                            suffix=".explanation",
                            **kwargs)

    def _get_language_dependant(self):
        explicit_state = self.__language_dependant
        return (self._infer_is_language_dependant()
                if explicit_state is None else explicit_state)

    def _set_language_dependant(self, value):
        self.__language_dependant = value

    def _infer_is_language_dependant(self):
        return (self.translated or (self.translatable_enumeration
                                    and self.enumeration is not None))

    language_dependant = property(_get_language_dependant,
                                  _set_language_dependant)

    def extract_searchable_text(self, extractor):
        extractor.feed(
            self.translate_value(extractor.current.value,
                                 language=extractor.current.language))

    __editable = None

    def _get_editable(self):

        if self.__editable is None:
            if self.expression is None:
                return EDITABLE
            else:
                return READ_ONLY

        return self.__editable

    def _set_editable(self, editable):
        if isinstance(editable, bool):
            warn(
                "Setting Member.editable to a bool is deprecated, use one of "
                "schema.EDITABLE, schema.NOT_EDITABLE or schema.READ_ONLY",
                DeprecationWarning,
                stacklevel=2)
            editable = EDITABLE if editable else NOT_EDITABLE
        self.__editable = editable

    editable = property(_get_editable, _set_editable)

    def to_json_value(self, value, **options):
        return value

    def from_json_value(self, value, **options):
        return value

    def serialize(self, value: Any, **options) -> str:
        return str(value)

    def parse(self, value: str, **options) -> Any:

        if not value.strip():
            return None

        if self.type:
            return self.type(value)
        else:
            return value

    def resolve_expression(self, obj, language=None):
        expression = self.expression
        if getattr(expression, "__self__", None) is obj:
            if self.translated:
                return expression(obj, language)
            else:
                return expression(obj)
        else:
            if self.translated:
                return expression(self, obj, language)
            else:
                return expression(self, obj)
Exemple #18
0
class IdentityProvider(Item):

    instantiable = False

    user_authenticated = Event(
        """An event triggered when the provider resolves a user.

        .. attribute:: user

            The `user <woost.models.User>` resolved by the provider.

        .. attribute:: data

            The user's profile data supplied by the provider (a dictionary with
            provider specific keys).

        .. attribute:: first_login

            Indicates if this is the first time that this user has logged in
            using this provider.
        """
    )

    members_order = [
        "title",
        "hidden",
        "debug_mode"
    ]

    title = schema.String(
        descriptive = True
    )

    hidden = schema.Boolean(
        required = True,
        default = False
    )

    debug_mode = schema.Boolean(
        required = True,
        default = False
    )

    provider_name = None
    user_data_id_field = "id"
    user_data_email_field = "email"
    user_identifier = None

    def get_auth_url(self, target_url = None):
        raise ValueError(
            "%s doesn't implement the get_auth_url() method"
            % self
        )

    def login(self, data):
        user = transaction(self.process_user_data, (data,))
        app.authentication.set_user_session(user)
        return user

    def process_user_data(self, data):

        id = data[self.user_data_id_field]
        email = data.get(self.user_data_email_field)
        user = (
            User.get_instance(**{self.user_identifier: id})
            or (
                email
                and User.get_instance(email = email)
            )
        )

        if user is None:
            user = User()

        if not user.get(self.user_identifier):
            user.set(self.user_identifier, id)
            if not user.email:
                user.email = email
            first_login = True
        else:
            first_login = False

        self.user_authenticated(
            user = user,
            data = data,
            first_login = first_login
        )
        user.insert()
        return user
class RelationMember(Member):
    """Base class for all members that describe a single end of a relation
    between two or more schemas.

    This is an abstract class; Noteworthy concrete subclasses include
    L{Reference<cocktail.schema.schemareference.Reference>} and
    L{Collection<cocktail.schema.schemacollections.Collection>}.

    @ivar relation_constraints: A collection of constraints imposed on related
        items during validation. Constraints can be specified using schema
        expressions or a callable. Expressions are evaluated normally using the
        related object as the context. Callables receive the relation owner and
        a related object as a parameter. Both types of constraints should
        evaluate to True if the related object satisfies their requirements, or
        False otherwise.

        During validation, a failed relation constraint will produce a
        L{RelationConstraintError<cocktail.schemas.exceptions.RelationConstraintError>}
        exception.

    @type relation_constraints: collection of callables or
        L{Expression<cocktail.schema.expressions.Expression>} instances
    """
    bidirectional = False
    related_key = None
    relation_constraints = None
    _integral = None
    _many = False
    __related_end = None
    __anonymous = False

    attached_as_orphan = Event(
        doc="""An event triggered when the relation end is attached to a
        schema after being declared using a self-contained bidirectional
        relation.

        @ivar anonymous: Indicates if the relation end had no name of its own.
        @type anonymous: bool
        """)

    def __init__(self, *args, **kwargs):
        Member.__init__(self, *args, **kwargs)

        if kwargs.get("integral", False) and not self.bidirectional:
            raise SchemaIntegrityError(
                "%s can't be declared 'integral' without setting "
                "'bidirectional' to True" % self)

    def _get_related_end(self):
        """Gets the opposite end of a bidirectional relation.
        @type: L{Member<member.Member>}
        """
        if self.__related_end:
            return self.__related_end

        if self.source_member:
            return self.source_member.related_end

        related_end = None
        related_type = self.related_type

        if self.bidirectional and related_type is not None:

            if self.related_key:
                related_end = related_type.get_member(self.related_key)

                if not getattr(related_end, "bidirectional", False) \
                or (
                    related_end.related_key
                    and related_end.related_key != self.name
                ):
                    related_end = None
            else:
                for member in related_type.members().values():
                    if getattr(member, "bidirectional", False):
                        if member.related_key:
                            if self.name == member.related_key:
                                related_end = member
                                break
                        elif self.schema is member.related_type \
                        and member is not self:
                            related_end = member
                            break

            # Related end missing
            if related_end is None:
                raise SchemaIntegrityError(
                    "Couldn't find the related end for %s" % self)
            # Disallow relations were both ends are declared as integral
            elif self.integral and related_end.integral:
                raise SchemaIntegrityError(
                    "Can't declare both ends of a relation as integral (%s <-> %s)"
                    % (self, related_end))
            # Disallow integral many to many relations
            elif (self.integral or related_end.integral) \
            and (self._many and related_end._many):
                raise SchemaIntegrityError(
                    "Can't declare a many to many relation as integral (%s <-> %s)"
                    % (self, related_end))

            self.__related_end = related_end
            related_end.__related_end = self

        return related_end

    def _set_related_end(self, related_end):

        if related_end is not None:
            self.bidirectional = True

        self.__related_end = related_end
        self._bind_orphan_related_end()

    related_end = property(_get_related_end,
                           _set_related_end,
                           doc="""The member at the other end of the relation.
        @type: L{RelationMember}
        """)

    @event_handler
    def handle_attached(event):
        event.source._bind_orphan_related_end()

    def _bind_orphan_related_end(self):

        related_end = self.__related_end

        if related_end \
        and related_end.schema is None \
        and self.schema is not None \
        and self.source_member is None:

            if related_end.name is None:
                if self.schema.name:
                    related_end.name = self.schema.name + "_" + self.name
                    anonymous = True
            else:
                anonymous = False

            if related_end.name \
            and self.related_type.get_member(related_end.name) is None:
                self.bidirectional = True
                related_end.bidirectional = True
                self.__related_end = related_end
                related_end.__related_end = self
                related_end.attached_as_orphan(anonymous=anonymous)
                related_end.__anonymous = anonymous
                self.related_type.add_member(related_end)

    @property
    def anonymous(self):
        return self.__anonymous

    @property
    @abstractmethod
    def related_type(self):
        pass

    def _get_integral(self):
        integral = self._integral

        if integral is None:
            related_type = self.related_type
            integral = \
                related_type and getattr(related_type, "integral", False) \
                or False

        return integral

    def _set_integral(self, value):
        self._integral = value

    integral = property(
        _get_integral,
        _set_integral,
        doc="""Gets or sets wether the member defines an integral relation.

        Items related through an integral relation belong exclusively to its
        parent.
        @type: bool
        """)

    def add_relation(self, obj, related_obj):
        if _push("relate", obj, related_obj, self):
            try:
                self._add_relation(obj, related_obj)
            finally:
                _pop()

    def remove_relation(self, obj, related_obj):
        if _push("unrelate", obj, related_obj, self):
            try:
                self._remove_relation(obj, related_obj)
            finally:
                _pop()

    def validate_relation_constraint(self, constraint, owner, related):

        # Expression-based constraint
        if isinstance(constraint, Expression):
            return constraint.eval(related)

        # Callable-based constraint
        elif callable(constraint):
            return constraint(owner, related)

        # Unknown constraint type
        else:
            raise TypeError("%r is not a valid relation constraint for %r; "
                            "expected a callable or an Expression instance" %
                            (constraint, self))
Exemple #20
0
class GoogleAnalyticsExtension(Extension):
    def __init__(self, **values):
        Extension.__init__(self, **values)
        self.extension_author = u"Whads/Accent SL"
        self.set("description",
                 u"""Integra el lloc web amb Google Analytics.""", "ca")
        self.set("description",
                 u"""Integra el sitio web con Google Analytics.""", "es")
        self.set("description",
                 u"""Integrates the site with Google Analytics.""", "en")

    def _load(self):

        from woost.extensions.googleanalytics import (strings, configuration,
                                                      website, document,
                                                      eventredirection)

        from cocktail.events import when
        from woost.controllers import CMSController

        @when(CMSController.producing_output)
        def handle_producing_output(e):
            publishable = e.output.get("publishable")
            if (publishable is None or getattr(
                    publishable, "is_ga_tracking_enabled", lambda: True)()):
                html = e.output.get("head_end_html", "")
                if html:
                    html += " "
                html += self.get_analytics_page_hit_script(publishable)
                e.output["head_end_html"] = html

    inclusion_code = {
        "ga.js-async":
        """
            <script type="text/javascript">
                var _gaq = _gaq || [];
                _gaq.push(['_setAccount', '%(account)s']);
                %(commands)s                
                (function() {
                var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
                ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
                var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
                })();
            </script>
            """,
        "ga.js-sync":
        """
            <script type="text/javascript" src="http://www.google-analytics.com/ga.js"></script>
            <script type="text/javascript">
                _gaq.push(['_setAccount', '%(account)s']);
                %(commands)s
            </script>
            """,
        "universal-async":
        """
            <script type="text/javascript">
              (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
              (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
              m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
              })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
              ga("create", "%(account)s");
              %(commands)s
            </script>
            """,
        "universal-sync":
        """
            <script type="text/javascript">
              (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
              (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
              m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
              })(window,document,'script','//www.google-analytics.com/analytics.js','ga');
              ga("create", "%(account)s");
              ga(function () {
                  %(commands)s
              });
            </script>
            """,
    }

    declaring_tracker = Event()

    def get_analytics_script(self,
                             publishable=None,
                             commands=None,
                             async=True):
        config = Configuration.instance
        api = config.google_analytics_api
        classic_api = (api == "ga.js")

        if publishable is None:
            publishable = context.get("publishable")

        event = self.declaring_tracker(
            api=api,
            publishable=publishable,
            account=config.get_setting("google_analytics_account"),
            template=self.inclusion_code[api +
                                         ("-async" if async else "-sync")],
            values=None
            if classic_api else self.get_analytics_values(publishable),
            commands=commands or [])

        parameters = {"account": event.account}

        if classic_api:
            parameters["commands"] = \
                self._serialize_ga_commands(event.commands)
        else:
            commands = event.commands

            if event.values:
                commands.insert(0, ("set", event.values))

            parameters["commands"] = \
                self._serialize_universal_commands(commands)

        return event.template % parameters
Exemple #21
0
 class Foo(object):
     spammed = Event(event_info_class = SpammedEventInfo)
Exemple #22
0
class EditNode(StackNode):
    """An L{edit stack<EditStack>} node, used to maintain a set of changes for
    an edited item before they are finally committed.

    @ivar translations: The list of translations defined by the item (note that
        these can change during the course of an edit operation, so that's why
        they are stored in here).
    @type translations: str list 
    """
    _persistent_keys = frozenset([
        "_stack", "_parent_node", "_index", "_item", "_form_data",
        "translations", "section", "tab"
    ])
    _item = None
    translations = None
    section = "fields"
    tab = None

    saving = Event("""
        An event triggered when saving the changes contained within the node,
        after loading form data to the modified item and before the transaction
        is committed.

        @ivar user: The user that makes the changes.
        @type user: L{User<woost.models.User>}

        @ivar changeset: The change set describing the changes.
        @type changeset: L{ChangeSet<woost.models.ChangeSet>}
        """)

    committed = Event("""
        An event triggered after the changes contained within the node have
        been successfully committed to the data store.
        
        @ivar user: The user that makes the changes.
        @type user: L{User<woost.models.User>}

        @ivar changeset: The change set describing the changes.
        @type changeset: L{ChangeSet<woost.models.ChangeSet>}
        """)

    def __init__(self, item):
        assert item is not None
        self._item = item

    def __translate__(self, language, **kwargs):
        if self.item.is_inserted:
            return translations(self.item)
        else:
            return translations("creating", content_type=self.content_type)

    def uri(self, **params):

        if "edit_stack" not in params:
            params["edit_stack"] = self.stack.to_param(self.index)

        uri = context["cms"].contextual_uri(
            "content",
            str(self.item.id) if self.item.is_inserted or self.item.is_deleted
            else "new", self.section, **params)

        uri += "#" + (self.tab or "")

        return uri

    def __getstate__(self):

        state = {}

        for key, value in self.__dict__.iteritems():
            if key in self._persistent_keys:
                if key == "_item" and not value.is_inserted:
                    value = None
                state[key] = value

        state["content_type"] = self._item.__class__

        if self._item.__class__.translated:
            state["item_translations"] = self._item.translations.keys()

        return state

    def __setstate__(self, state):

        content_type = state.pop("content_type", None)
        item_translations = state.pop("item_translations", None)

        for key, value in state.iteritems():
            if key in self._persistent_keys:
                if key == "_item":
                    if content_type is None:
                        value = None
                    elif value is None:
                        value = content_type()
                        self.initialize_new_item(value, item_translations)

                setattr(self, key, value)

    @getter
    def content_type(self):
        """The edited content type.
        @type: L{Item<woost.models.Item>} subclass
        """
        return self._item.__class__

    @getter
    def item(self):
        """The edited item.
        @type: L{Item<woost.models.Item>}
        """
        return self._item

    def initialize_new_item(self, item, languages=None):
        if item.__class__.translated:
            for language in (languages or [
                    Configuration.instance.get_setting("default_language")
            ]):
                item.new_translation(language)

    def import_form_data(self, form_data, item):
        """Update the edited item with data gathered from the form."""
        self.form_adapter.import_object(form_data, item, self.form_schema,
                                        self.content_type)

        # Drop deleted translations
        if item.__class__.translated:
            user = get_current_user()

            deleted_translations = (
                set(language
                    for language in item.translations if user.has_permission(
                        ReadTranslationPermission, language=language)) -
                set(self.translations))

            for language in deleted_translations:
                del item.translations[language]

    def export_form_data(self, item, form_data):
        """Update the edit form with the data contained on the edited item."""

        self.form_adapter.export_object(item, form_data, self.content_type,
                                        self.form_schema)

        # Default translations
        if self.content_type.translated:

            user = get_current_user()
            available_languages = set(
                language for language in item.translations
                if user.has_permission(ReadTranslationPermission,
                                       language=language))

            if not self._item.translations:
                default_language = \
                    Configuration.instance.get_setting("default_language")
                if default_language in available_languages:
                    self._item.new_translation(default_language)

            self.translations = [
                language for language in self._item.translations.keys()
                if language in available_languages
            ]
        else:
            self.translations = []

    @cached_getter
    def form_adapter(self):
        """The data adapter used to pass data between the edited item and the
        edit form.
        @type: L{Adapter<cocktail.schema.Adapter>}
        """
        adapter = schema.Adapter()
        adapter.collection_copy_mode = self._adapt_collection
        adapter.exclude([
            member.name for member in self.content_type.members().itervalues()
            if self.should_exclude_member(member)
        ])
        return adapter

    def should_exclude_member(self, member):

        if not member.editable:
            return True

        if not member.visible:
            return True

        # Hide relations with relation nodes in the stack
        relation_node = self.get_ancestor_node(RelationNode)
        if relation_node and member is relation_node.member.related_end:
            return True

        user = get_current_user()

        if not user.has_permission(ReadMemberPermission, member=member):
            return True

        if not user.has_permission(ModifyMemberPermission, member=member):
            return True

        if isinstance(member, schema.RelationMember) \
        and member.is_persistent_relation:

            # Hide relations to invisible types
            if not member.related_type.visible:
                return True

            # Hide empty collections with the exclude_when_empty flag
            # set
            if (isinstance(member, schema.Collection)
                    and member.exclude_when_empty and
                    not member.select_constraint_instances(parent=self.item)):
                return True

            def class_family_permission(root, permission_type):
                return any(
                    user.has_permission(permission_type, target=cls)
                    for cls in root.schema_tree())

            # Require read permission for related types
            if not class_family_permission(member.related_type,
                                           ReadPermission):
                return True

            # Integral relation
            if (isinstance(member, schema.Reference) and member.integral
                    and self.item):
                # Empty: require create permission
                # Has an item: require edit or delete permission
                if self.item.get(member) is None:
                    if not class_family_permission(member.type,
                                                   CreatePermission):
                        return True
                elif not (class_family_permission(member.type,
                                                  ModifyPermission)
                          and class_family_permission(member.type,
                                                      DeletePermission)):
                    return True

        # Remove all relations to blocks from the edit view
        if (isinstance(member, schema.RelationMember) and member.related_type
                and issubclass(member.related_type, Block)):
            return True

        return False

    def _adapt_collection(self, context, key, value):
        return self._copy_collection(value)

    def _copy_collection(self, collection):

        if isinstance(collection, (PersistentList, PersistentRelationList)):
            return list(collection)
        elif isinstance(collection,
                        (PersistentMapping, PersistentRelationMapping)):
            return dict(collection.iteritems())
        elif isinstance(collection,
                        (PersistentOrderedSet, PersistentRelationOrderedSet)):
            return OrderedSet(collection)
        elif isinstance(collection, (PersistentSet, PersistentRelationSet)):
            return set(collection)
        else:
            return copy(collection)

    @cached_getter
    def form_schema(self):
        """The schema that describes the edit form for the edited item.
        @type: L{Schema<cocktail.schema.Schema>}
        """
        form_schema = self.form_adapter.export_schema(self.content_type)
        form_schema.name = "BackOfficeEditForm"
        return form_schema

    @cached_getter
    def form_data(self):
        """The data entered into the edit form."""

        form_data = {}

        # First load: fill the form with data from the edited item
        if self is self.stack[-1]:
            self.export_form_data(self.item, form_data)

        return form_data

    def iter_errors(self):
        """Iterates over the sequence of validation errors produced by the
        current edited state.
        @type: L{ValidationError<cocktail.schema.exceptions.ValidationError>}
            sequence
        """
        return self.form_schema.get_errors(self.form_data,
                                           languages=self.translations,
                                           persistent_object=self.item)

    def iter_changes(self, source=None):
        """Iterates over the set of members that the current edit state has
        modified. Each change is expressed as a tuple containing the affected
        member and language.
        @type: (L{Member<cocktail.schema.Member>}, str) sequence
        """
        source_form_data = {}
        self.form_adapter.export_object(source or self._item, source_form_data,
                                        self.content_type, self.form_schema)
        return schema.diff(source_form_data,
                           self.form_data,
                           self.form_schema,
                           exclude=lambda member: not member.editable)

    def relate(self, member, item):
        """Adds a relation between the edited item and another item.
        
        @param member: The member describing the relation between the two
            items. It should be the end nearer to the edited item.
        @type member: L{RelationMember<cocktail.schema.RelationMember>}

        @param item: The item to relate.
        @type item: L{Item<woost.models.item.Item>}
        """
        if isinstance(member, schema.Collection):

            collection = schema.get(self.form_data, member)

            # Editing collections with duplicate entries is not allowed
            if item in collection:
                raise ValueError(
                    "Collections with duplicate entries are not allowed")

            schema.add(collection, item)
        else:
            schema.set(self.form_data, member, item)

    def unrelate(self, member, item):
        """Breaks the relation between the edited item and one of its related
        items.
        
        @param member: The member describing the relation between the two
            items. It should be the end nearer to the edited item.
        @type member: L{RelationMember<cocktail.schema.RelationMember>}

        @param item: The item to unrelate.
        @type item: L{Item<woost.models.item.Item>}
        """
        if isinstance(member, schema.Collection):
            collection = schema.get(self.form_data, member)
            schema.remove(collection, item)
        else:
            schema.set(self.form_data, member, None)

    def item_saved_notification(self, is_new, change):
        """Notifies the user that the node's item has been saved.

        @param is_new: Indicates if the save operation consisted of an
            insertion (True) or an update of an existing item (False).
        @type is_new: bool

        @param change: A change object describing the save operation.
        @type change: L{Change<woost.models.changeset.Change>}            
        """
        item = self.item
        msg = translations("woost.views.BackOfficeEditView Changes saved",
                           item=item,
                           is_new=is_new)
        transient = True

        if is_new and self.parent_node is None:
            controller = cherrypy.request.handler_chain[-1]
            msg += '. <a href="%s">%s</a>.' % (
                controller.edit_uri(item.__class__, edit_stack=None),
                translations("woost.views.BackOfficeEditView Create another"))
            transient = False

        notify_user(msg, "success", transient)
Exemple #23
0
class DataStore(object):
    """A thread safe wrapper over the application's object database. Normally
    used through its global L{datastore} instance.

    The class expects an X{storage} setting, containing a
    L{storage<ZODB.BaseStorage.BaseStorage>} instance pointing to the physical
    location of the database (see the ZODB documentation for more details).
    """
    automatic_migration = False

    def __init__(self, storage=None):
        self._thread_data = local()
        self.__storage_lock = RLock()
        self.__db = None
        self.__storage = None
        self.storage = storage

    storage_changed = Event("""
        An event triggered when changing which storage is used by the
        datastore.
        """)

    cleared = Event("""
        An event triggered when all data in the storage is completely cleared.
        """)

    connection_opened = Event("""
        An event triggered when the datastore spawns a new thread-bound
        connection.

        @ivar connection: The new connection.
        @type connection: L{Connection<>}
        """)

    def _get_storage(self):
        if isinstance(self.__storage, FunctionType):
            self.__storage_lock.acquire()
            try:
                if isinstance(self.__storage, FunctionType):
                    self.__storage = self.__storage()
                    self.__possibly_migrate()
            finally:
                self.__storage_lock.release()
        return self.__storage

    def _set_storage(self, storage):
        self.__storage_lock.acquire()
        try:
            self.__storage = storage
            self.__db = None
            self.__possibly_migrate()
            self.storage_changed()
        finally:
            self.__storage_lock.release()

    storage = property(_get_storage,
                       _set_storage,
                       doc="""
        Gets or sets the underlying ZODB storage for the data store.
        @type: L{ZODB.Storage}
        """)

    def __possibly_migrate(self):
        if self.automatic_migration \
        and self.__storage is not None \
        and not isinstance(self.__storage, FunctionType):
            from cocktail.persistence.migration import migrate
            migrate()

    @property
    def db(self):
        if self.__db is None:
            self.__db = DB(self.storage)

        return self.__db

    @property
    def root(self):
        """Gives access to the root container of the database. The property is
        thread safe; accessing it on different threads will produce different
        containers, each bound to a separate database connection.
        @type: mapping
        """
        root = getattr(self._thread_data, "root", None)

        if root is None:
            root = self.connection.root()
            self._thread_data.root = root

        return root

    @property
    def connection(self):
        """Returns the database connection for the current thread. The property
        is thread safe; accessing it on different threads will produce
        different connections. Once called, each connection remains bound to a
        single thread until the thread finishes or the datastore's L{close}
        method is called.
        @type: L{Connection<ZODB.Connection.Connection>}
        """
        connection = getattr(self._thread_data, "connection", None)

        if connection is None:
            connection = self.db.open()
            self._thread_data.connection = connection
            self.connection_opened(connection=connection)

        return connection

    commit = transaction.commit
    abort = transaction.abort

    def close(self):
        """Closes the connection to the database for the current thread.
        Accessing the L{root} or L{connection} properties after this method is
        called will spawn a new database connection.
        """
        if hasattr(self._thread_data, "root"):
            self._thread_data.root = None

        connection = getattr(self._thread_data, "connection", None)

        if connection is not None:
            self._thread_data.connection.close()
            self._thread_data.connection = None

    def clear(self):
        """Clears all the data in the storage."""
        self.root.clear()
        self.cleared()
        self.commit()

    def sync(self):
        self._thread_data.root = None
        self.connection.sync()

    def _get_transaction_data(self, create_if_missing=False):

        thread_transaction_data = getattr(self._thread_data,
                                          "transaction_data", None)

        if thread_transaction_data is None:
            thread_transaction_data = WeakKeyDictionary()
            self._thread_data.transaction_data = thread_transaction_data

        transaction = getattr(self._thread_data, "transaction", None)
        if transaction is None:
            transaction = self.connection.transaction_manager.get()

        transaction_data = thread_transaction_data.get(transaction)

        if transaction_data is None and create_if_missing:
            transaction_data = {}
            thread_transaction_data[transaction] = transaction_data

        return transaction_data

    def get_transaction_value(self, key, default=None):

        transaction_data = self._get_transaction_data()

        if transaction_data is None:
            return default

        return transaction_data.get(key, default)

    def set_transaction_value(self, key, value):
        transaction_data = self._get_transaction_data(True)
        transaction_data[key] = value

    def unique_after_commit_hook(self, id, callback, *args, **kwargs):

        key = "cocktail.persistence.unique_after_commit_hooks"
        unique_after_commit_hooks = self.get_transaction_value(key)

        if unique_after_commit_hooks is None:
            unique_after_commit_hooks = set([id])
            self.set_transaction_value(key, unique_after_commit_hooks)
        elif id in unique_after_commit_hooks:
            return False
        else:
            unique_after_commit_hooks.add(id)

        def callback_wrapper(success, *args, **kwargs):
            try:
                self._thread_data.transaction = transaction
                return callback(success, *args, **kwargs)
            finally:
                self._thread_data.transaction = None

        transaction = self.connection.transaction_manager.get()
        transaction.addAfterCommitHook(callback_wrapper, args, kwargs)
        return True

    def process_items_after_commit(self,
                                   callback,
                                   item,
                                   item_data=None,
                                   mapping_type=dict):
        id = get_full_name(callback)
        items_key = id + ".items"
        items = self.get_transaction_value(items_key)

        if items is None:
            items = mapping_type()
            self.set_transaction_value(items_key, items)

        items[item] = item_data
        self.unique_after_commit_hook(id, callback, items)
Exemple #24
0
class Translations(object):

    bundle_loaded = Event()
    verbose = False

    def __init__(self, *args, **kwargs):
        self.definitions = {}
        self.__loaded_bundles = set()
        self.__bundle_overrides = defaultdict(list)

    def _get_key(self, key, verbose):

        if verbose:
            print((verbose - 1) * 2 * " " + styled("Key", "slate_blue"), key)

        return self.definitions.get(key)

    def define(self, key, value=None, **per_language_values):

        if value is not None and per_language_values:
            raise ValueError(
                "Can't specify both 'value' and 'per_language_values'")

        self.definitions[key] = per_language_values or value

    def set(self, key, language, value):
        per_language_values = self.definitions.get(key)
        if per_language_values is None:
            per_language_values = {}
            self.definitions[key] = per_language_values
        per_language_values[language] = value

    def instances_of(self, cls):
        def decorator(func):
            try:
                class_name = get_full_name(cls)
            except:
                class_name = cls.__name__

            self.definitions[class_name + ".instance"] = func
            return func

        return decorator

    def load_bundle(self, bundle_path, callback=None, reload=False, **kwargs):

        if reload or bundle_path not in self.__loaded_bundles:
            self.__loaded_bundles.add(bundle_path)

            pkg_name, file_name = bundle_path.rsplit(".", 1)
            file_path = resource_filename(pkg_name, file_name + ".strings")

            file_error = None

            try:
                with open(file_path, "r") as bundle_file:
                    parser = TranslationsFileParser(bundle_file,
                                                    file_path=file_path,
                                                    **kwargs)
                    for key, language, value in parser:
                        if callback:
                            callback(key, language, value)
                        if language is None:
                            self.definitions[key] = value
                        else:
                            self.set(key, language, value)
            except IOError as e:
                file_error = e
            else:
                self.bundle_loaded(file_path=file_path)

            overrides = self.__bundle_overrides.get(bundle_path)
            if overrides:
                for overrides_path, overrides_kwargs in overrides:
                    self.load_bundle(overrides_path, **kwargs)
            elif file_error:
                raise file_error

    def request_bundle(self, bundle_path, **kwargs):
        try:
            self.load_bundle(bundle_path, **kwargs)
        except IOError:
            pass

    def override_bundle(self, original_bundle_path, overrides_bundle_path,
                        **kwargs):
        self.__bundle_overrides[original_bundle_path].append(
            (overrides_bundle_path, kwargs))
        if original_bundle_path in self.__loaded_bundles:
            self.request_bundle(overrides_bundle_path)

    def __iter_class_names(self, cls):

        mro = getattr(cls, "__mro__", None)
        if mro is None:
            mro = (cls, )

        for cls in mro:

            try:
                class_name = get_full_name(cls)
            except:
                class_name = cls.__name__

            yield class_name

    def __call__(self, obj, language=None, default="", chain=None, **kwargs):
        translation = None

        verbose = kwargs.get("verbose", None)
        if verbose is None:
            if callable(self.verbose):
                verbose = self.verbose(obj, **kwargs)
            else:
                verbose = self.verbose

            verbose = 1 if verbose else 0

        if verbose == 1:
            print(styled("TRANSLATION", "white", "blue"), end=' ')
            print(styled(obj, style="bold"))

        if verbose:
            kwargs["verbose"] = verbose + 1

        # Look for a explicit definition for the given value
        if isinstance(obj, str):
            translation = self._get_key(obj, verbose)
        else:
            # Translation of class instances
            for class_name in self.__iter_class_names(obj.__class__):
                translation = self._get_key(class_name + ".instance", verbose)
                if translation:
                    break

            # Translation of class references
            if not translation and isinstance(obj, type):
                for class_name in self.__iter_class_names(obj):
                    translation = self(class_name, language=language, **kwargs)
                    if translation:
                        break

        # Resolve the obtained translation
        if translation:
            if callable(translation):
                with language_context(language):

                    if verbose:
                        print(styled("Function", "slate_blue"), end=' ')
                        print(translation)

                    if isinstance(obj, str):
                        translation = translation(**kwargs)
                    else:
                        translation = translation(obj, **kwargs)

            elif isinstance(translation, Mapping):
                definitions = translation
                translation = None
                prev_lang = language or get_language()
                for lang in iter_language_chain(language):
                    translation = definitions.get(lang)
                    if translation:
                        if callable(translation):
                            with language_context(prev_lang):
                                if isinstance(obj, str):
                                    translation = translation(**kwargs)
                                else:
                                    translation = translation(obj, **kwargs)
                            if translation:
                                break
                        else:
                            if kwargs:
                                translation = translation % kwargs
                            break
            elif kwargs:
                translation = translation % kwargs

        # Custom translation chain
        if not translation and chain is not None:
            translation = self(chain, language=language, **kwargs)

        if not translation:
            translation = str(default)

        return translation or ""
Exemple #25
0
class ECommerceOrder(Item):

    payment_types_completed_status = {
        "payment_gateway": "accepted",
        "transfer": "payment_pending",
        "cash_on_delivery": "payment_pending"
    }

    incoming = Event(doc="""
        An event triggered when a new order is received.
        """)

    completed = Event(doc="""
        An event triggered when an order is completed.
        """)

    groups_order = ["shipping_info", "billing"]

    members_order = [
        "customer", "address", "town", "region", "country", "postal_code",
        "language", "status", "purchases", "payment_type", "total_price",
        "pricing", "total_shipping_costs", "shipping_costs", "total_taxes",
        "taxes", "total"
    ]

    customer = schema.Reference(
        type=User,
        related_end=schema.Collection(),
        required=True,
        default=schema.DynamicDefault(get_current_user))

    address = schema.String(member_group="shipping_info",
                            required=True,
                            listed_by_default=False)

    town = schema.String(member_group="shipping_info",
                         required=True,
                         listed_by_default=False)

    region = schema.String(member_group="shipping_info",
                           required=True,
                           listed_by_default=False)

    country = schema.Reference(
        member_group="shipping_info",
        type=Location,
        relation_constraints=[Location.location_type.equal("country")],
        default_order="location_name",
        related_end=schema.Collection(),
        required=True,
        listed_by_default=False,
        user_filter="cocktail.controllers.userfilter.MultipleChoiceFilter")

    postal_code = schema.String(member_group="shipping_info",
                                required=True,
                                listed_by_default=False)

    language = schema.String(
        required=True,
        format="^[a-z]{2}$",
        editable=False,
        default=schema.DynamicDefault(get_language),
        text_search=False,
        translate_value=lambda value, language=None, **kwargs: u""
        if not value else translations(value, language, **kwargs))

    status = schema.String(
        required=True,
        indexed=True,
        enumeration=[
            "shopping", "payment_pending", "accepted", "failed", "refund"
        ],
        default="shopping",
        text_search=False,
        translate_value=lambda value, language=None, **kwargs: u""
        if not value else translations("ECommerceOrder.status-" + value,
                                       language, **kwargs))

    purchases = schema.Collection(
        items="woost.extensions.ecommerce.ecommercepurchase."
        "ECommercePurchase",
        integral=True,
        bidirectional=True,
        min=1)

    payment_type = schema.String(
        member_group="billing",
        required=True,
        translate_value=lambda value, language=None, **kwargs: translations(
            "ECommerceOrder.payment_type-%s" % value, language=language),
        default=schema.DynamicDefault(_get_default_payment_type),
        text_search=False,
        edit_control="cocktail.html.RadioSelector",
        listed_by_default=False)

    total_price = schema.Decimal(member_group="billing",
                                 editable=False,
                                 listed_by_default=False,
                                 translate_value=_translate_amount)

    pricing = schema.Collection(
        member_group="billing",
        items=schema.Reference(type=ECommerceBillingConcept),
        related_end=schema.Collection(block_delete=True),
        editable=False)

    total_shipping_costs = schema.Decimal(member_group="billing",
                                          editable=False,
                                          listed_by_default=False,
                                          translate_value=_translate_amount)

    shipping_costs = schema.Collection(
        member_group="billing",
        items=schema.Reference(type=ECommerceBillingConcept),
        related_end=schema.Collection(block_delete=True),
        editable=False)

    total_taxes = schema.Decimal(member_group="billing",
                                 editable=False,
                                 listed_by_default=False,
                                 translate_value=_translate_amount)

    taxes = schema.Collection(
        member_group="billing",
        items=schema.Reference(type=ECommerceBillingConcept),
        related_end=schema.Collection(block_delete=True),
        editable=False)

    total = schema.Decimal(member_group="billing",
                           editable=False,
                           translate_value=_translate_amount)

    def calculate_cost(self,
                       apply_pricing=True,
                       apply_shipping_costs=True,
                       apply_taxes=True):
        """Calculates the costs for the order.
        :rtype: dict
        """
        from woost.extensions.ecommerce import ECommerceExtension
        extension = ECommerceExtension.instance

        order_costs = {
            "price": {
                "cost": Decimal("0.00"),
                "percentage": Decimal("0.00"),
                "concepts": []
            },
            "shipping_costs": {
                "cost": Decimal("0.00"),
                "percentage": Decimal("0.00"),
                "concepts": []
            },
            "taxes": {
                "cost": Decimal("0.00"),
                "percentage": Decimal("0.00"),
                "concepts": []
            },
            "purchases": {}
        }

        # Per purchase costs:
        for purchase in self.purchases:
            purchase_costs = purchase.calculate_costs(
                apply_pricing=apply_pricing,
                apply_shipping_costs=apply_shipping_costs,
                apply_taxes=apply_taxes)
            order_costs["purchases"][purchase] = purchase_costs

            order_costs["price"]["cost"] += purchase_costs["price"]["total"]
            order_costs["shipping_costs"]["cost"] += \
                purchase_costs["shipping_costs"]["total"]
            order_costs["taxes"]["cost"] += purchase_costs["taxes"]["total"]

        # Order price
        order_price = order_costs["price"]

        if apply_pricing:
            for pricing in extension.pricing:
                if pricing.applies_to(self):
                    pricing.apply(self, order_price)

        order_price["cost"] += \
            order_price["cost"] * order_price["percentage"] / 100

        order_price["total"] = order_price["cost"]

        # Order shipping costs
        order_shipping_costs = order_costs["shipping_costs"]

        if apply_shipping_costs:
            for shipping_cost in extension.shipping_costs:
                if shipping_cost.applies_to(self):
                    shipping_cost.apply(self, order_shipping_costs)

        order_shipping_costs["total"] = (
            order_shipping_costs["cost"] +
            order_price["total"] * order_shipping_costs["percentage"] / 100)

        # Order taxes
        order_taxes = order_costs["taxes"]

        if apply_taxes:
            for tax in extension.taxes:
                if tax.applies_to(self):
                    tax.apply(self, order_taxes)

        order_taxes["total"] = (
            order_taxes["cost"] +
            order_price["total"] * order_taxes["percentage"] / 100)

        # Total
        order_costs["total"] = (order_price["total"] +
                                order_shipping_costs["total"] +
                                order_taxes["total"])

        return order_costs

    def update_cost(self,
                    apply_pricing=True,
                    apply_shipping_costs=True,
                    apply_taxes=True):
        costs = self.calculate_cost(apply_pricing=apply_pricing,
                                    apply_shipping_costs=apply_shipping_costs,
                                    apply_taxes=apply_taxes)

        self.total_price = costs["price"]["total"]
        self.pricing = list(costs["price"]["concepts"])

        self.total_shipping_costs = costs["shipping_costs"]["total"]
        self.shipping_costs = list(costs["shipping_costs"]["concepts"])

        self.total_taxes = costs["taxes"]["total"]
        self.taxes = list(costs["taxes"]["concepts"])

        self.total = costs["total"]

        for purchase, purchase_costs in costs["purchases"].iteritems():
            purchase.total_price = purchase_costs["price"]["total"]
            purchase.pricing = list(purchase_costs["price"]["concepts"])
            self.pricing.extend(purchase.pricing)

            purchase.total_shipping_costs = \
                purchase_costs["shipping_costs"]["total"]
            purchase.shipping_costs = \
                list(purchase_costs["shipping_costs"]["concepts"])
            self.shipping_costs.extend(purchase.shipping_costs)

            purchase.total_taxes = purchase_costs["taxes"]["total"]
            purchase.taxes = list(purchase_costs["taxes"]["concepts"])
            self.taxes.extend(purchase.taxes)

            purchase.total = purchase_costs["total"]

    def count_units(self):
        return sum(purchase.quantity for purchase in self.purchases)

    def get_weight(self):
        return sum(purchase.get_weight() for purchase in self.purchases)

    def add_purchase(self, purchase):
        for order_purchase in self.purchases:
            if order_purchase.__class__ is purchase.__class__ \
            and order_purchase.product is purchase.product \
            and all(
                order_purchase.get(option) == purchase.get(option)
                for option in purchase.get_options()
                if option.name != "quantity"
            ):
                order_purchase.quantity += purchase.quantity
                purchase.product = None
                if purchase.is_inserted:
                    purchase.delete()
                break
        else:
            self.purchases.append(purchase)

    @classmethod
    def get_public_schema(cls):
        public_schema = schema.Schema("OrderCheckoutSummary")
        cls.get_public_adapter().export_schema(cls, public_schema)

        payment_type = public_schema.get_member("payment_type")
        if payment_type:
            payments = PaymentsExtension.instance

            if payments.enabled and payments.payment_gateway:

                translate_value = payment_type.translate_value

                def payment_type_translate_value(value,
                                                 language=None,
                                                 **kwargs):
                    if value == "payment_gateway":
                        return payments.payment_gateway.label
                    else:
                        return translate_value(value,
                                               language=language,
                                               **kwargs)

                payment_type.translate_value = payment_type_translate_value

        return public_schema

    @classmethod
    def get_public_adapter(cls):
        from woost.extensions.ecommerce import ECommerceExtension

        user = get_current_user()
        adapter = schema.Adapter()
        adapter.exclude(["customer", "status", "purchases"])
        adapter.exclude([
            member.name for member in cls.members().itervalues()
            if not member.visible or not member.editable
            or not issubclass(member.schema, ECommerceOrder)
            or not user.has_permission(ModifyMemberPermission, member=member)
        ])
        if len(ECommerceExtension.instance.payment_types) == 1:
            adapter.exclude(["payment_type"])
        return adapter

    @property
    def is_completed(self):
        return self.status \
        and self.status == self.payment_types_completed_status.get(
            self.payment_type
        )

    @event_handler
    def handle_changed(cls, event):

        item = event.source
        member = event.member

        if member.name == "status":

            if event.previous_value == "shopping" \
            and event.value in ("payment_pending", "accepted"):
                item.incoming()

            if item.is_completed:
                item.completed()

    def get_description_for_gateway(self):
        site_name = Configuration.instance.get_setting("site_name")
        if site_name:
            return translations(
                "woost.extensions.ECommerceOrder description for gateway"
            ) % site_name
        else:
            return translations(self)
Exemple #26
0
        for member in cls.members(False).values():
            if member.indexed and not member.primary:
                if verbose:
                    print("Rebuilding index for %s" % member)
                member.rebuild_index()

        if recursive:
            for subclass in cls.derived_schemas():
                subclass.rebuild_indexes(True)

        cls.rebuilding_indexes(recursive=recursive, verbose=verbose)


PersistentClass.rebuild_indexes = _rebuild_indexes

PersistentClass.rebuilding_indexes = Event()


@when(PersistentObject.declared)
def _handle_declared(event):

    cls = event.source

    # Add 'id' as an alias for custom primary members
    if cls.primary_member:
        if cls.primary_member.schema is cls \
        and cls.primary_member.name != "id":
            cls.id = cls.__dict__[cls.primary_member.name]

    # Add an 'id' field to all indexed schemas that don't define their
    # own primary member explicitly. Will be initialized to an
Exemple #27
0
class Schema(Member):
    """A data structure, made up of one or more L{members<member.Member>}.
    Schemas are themselves members, which allows them to be nested arbitrarely
    (ie. in other schemas or L{collections<schemacollections.Collection>} to
    assemble more complex compound types.

    Schemas support inheritance. All members defined by a base schema will be
    reflected on their derived schemas. This is done dynamically: new members
    added to base schemas automatically appear as members of any derived
    schema, recursively. Derived schemas can override member definitions with
    their own, simply adding a new member matching the name of a existing one.

    Schemas can use multiple inheritance; in case of conflicting member
    definitions, the one defined by the foremost base schema (as passed to the
    L{inherit} method) will take precedence.

    @ivar bases: The list of base schemas that the schema inherits from. This
        is a shallow list; to obtain the full inheritance tree, use the
        L{ascend_inheritance} method instead.
    """
    schema_aliases = ()
    primary_member = None
    descriptive_member = None
    members_order = None
    groups_order = []
    integral = False
    text_search = True

    member_added = Event("""
        An event triggered when a member is added to the schema.

        @ivar member: The added member.
        @type member: L{Member<cocktail.schema.Member>}
        """)

    inherited = Event("""
        An event triggered when the schema is extended by another schema.

        @ivar schema: The derived schema that extends this schema.
        @type schema: L{Schema}
        """)

    _special_copy_keys = Member._special_copy_keys | set(
        ["_Schema__bases", "_Schema__members", "_declared"])

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

        members = kwargs.pop("members", None)
        bases = kwargs.pop("bases", None)
        Member.__init__(self, *args, **kwargs)

        self.__bases = None
        self.bases = ListWrapper(empty_list)

        self.__members = None

        if members:
            if isinstance(members, (list, tuple)) and not self.members_order:
                self.members_order = [member.name for member in members]

            self.expand(members)

        if bases:
            self.inherit(*bases)

    def __deepcopy__(self, memo):
        schema_copy = Member.__deepcopy__(self, memo)

        if not isinstance(schema_copy, type):
            if self.__bases:
                for base in self.__bases:
                    schema_copy.inherit(base)

        if self.__members:
            for member in self.__members.values():
                schema_copy.add_member(deepcopy(member))

        return schema_copy

    def init_instance(self,
                      instance,
                      values=None,
                      accessor=None,
                      excluded_members=None):

        if accessor is None:
            accessor = get_accessor(instance)

        # Set the value of all object members, either from a parameter or from
        # a default value definition
        for name, member in self.members().items():

            if excluded_members is not None and member in excluded_members:
                continue

            value = default if values is None else values.get(name, default)

            if value is undefined:
                continue

            if value is default:

                if member.translated:
                    continue

                value = member.produce_default(instance)

            accessor.set(instance, name, value)

    def produce_default(self, instance=None):
        default = Member.produce_default(self, instance)
        if default is None:
            default = self._create_default_instance()
        return default

    def _create_default_instance(self):
        if self.type:
            default = self.type()
        elif isinstance(self, type):
            default = self()
        else:
            default = {}

        self.init_instance(default)

        return default

    def inherit(self, *bases):
        """Declare an inheritance relationship towards one or more base
        schemas.

        @param bases: The list of base schemas to inherit from.
        @type bases: L{Schema}
        """
        def prevent_cycle(bases):
            for base in bases:
                if base is self:
                    raise SchemaInheritanceCycleError(self)
                if base.__bases:
                    prevent_cycle(base.__bases)

        prevent_cycle(bases)

        if self.__bases is None:
            self.__bases = []
            self.bases = ListWrapper(self.__bases)

        for base in bases:
            self.__bases.append(base)

            for ancestor in reversed(list(base.ascend_inheritance(True))):
                ancestor.inherited(schema=self)

    def ascend_inheritance(self, include_self=False):

        if include_self:
            yield self

        if self.__bases:
            for base in self.__bases:
                for ascendant in base.ascend_inheritance(True):
                    yield ascendant

    def descend_inheritance(self, include_self=False):

        if self.__bases:
            for base in self.__bases:
                for ascendant in base.descend_inheritance(True):
                    yield ascendant

        if include_self:
            yield self

    def add_member(self, member, append=False, after=None, before=None):
        """Adds a new member to the schema.

        @param member: The member to add.
        @type member: L{Member<member.Member>}

        @raise SchemaIntegrityError: Raised when trying to add an anonymous
            member to the schema. All members must have a unique name.
        """
        self._check_member(member)

        if append or after or before:

            if ((1 if append else 0) + (1 if after else 0) +
                (1 if before else 0) > 1):

                raise ValueError(
                    "Can't combine the 'append', 'after' or 'before' "
                    "parameters when calling Schema.add_member()")

            if self.members_order is None:
                self.members_order = []
            elif not isinstance(self.members_order, list):
                self.members_order = list(self.members_order)

            if append:
                self.members_order.append(member.name)
            elif after:
                pos = self.members_order.index(after)
                self.members_order.insert(pos + 1, member.name)
            else:
                pos = self.members_order.index(before)
                self.members_order.insert(pos, member.name)

        self._add_member(member)
        member.attached()
        self.member_added(member=member)

    def _check_member(self, member):
        if member.name is None:
            raise SchemaIntegrityError("Can't add an anonymous member to %s" %
                                       self)

    def _add_member(self, member):
        if self.__members is None:
            self.__members = {}

        if member.primary:
            self.primary_member = member

        if member.descriptive:
            self.descriptive_member = member

        self.__members[member.name] = member
        member._schema = self

    def expand(self, members):
        """Adds several members to the schema.

        @param members: A list or mapping of additional members to add to the
            copy. When given as a mapping, the keys will be used for the member
            names.
        @type members: L{Member<member.Member>} list
            or (str, L{Member<member.Member>}) dict
        """

        # From a dictionary
        if isinstance(members, dict):
            for name, member in members.items():

                if isinstance(member, type):
                    member = member()

                member.name = name
                self.add_member(member)

        # From a list
        else:
            # Use the provided list as an implicit order sequence for the
            # schema members
            if not self.members_order:
                self.members_order = [member.name for member in members]

            for member in members:
                self.add_member(member)

    def remove_member(self, member):
        """Removes a member from the schema.

        @param member: The member to remove. Can be specified using a reference
            to the member object itself, or giving its name.
        @type member: L{Member<member.Member>} or str

        @raise L{SchemaIntegrityError<exceptions.SchemaIntegrityError>}:
            Raised if the member doesn't belong to the schema.
        """

        # Normalize string references to member objects
        if isinstance(member, str):
            member = self[member]

        if member._schema is not self:
            raise SchemaIntegrityError(
                "Trying to remove %s from a schema it doesn't belong to (%s)" %
                (member, self))

        member._schema = None
        del self.__members[member.name]

    def members(self, recursive=True):
        """A dictionary with all the members defined by the schema and its
        bases.

        @param recursive: Indicates if the returned dictionary should contain
            members defined by the schema's bases. This is the default
            behavior; Setting this parameter to False will exclude all
            inherited members.
        @type recursive: False

        @return: A mapping containing the members for the schema, indexed by
            member name.
        @rtype: (str, L{Member<members.Member>}) read only dict
        """
        if recursive and self.__bases:
            return dict(
                (member.name, member) for member in self.iter_members())
        else:
            return DictWrapper(self.__members or empty_dict)

    def iter_members(self, recursive=True):
        """Iterates over all the members defined by the schema and its bases.

        @param recursive: Indicates if the returned dictionary should contain
            members defined by the schema's bases. This is the default
            behavior; Setting this parameter to False will exclude all
            inherited members.
        @type recursive: False

        @return: An iterable sequence containing the members for the schema and
            its bases. No guarantees are made about their order.
        @rtype: L{Member<members.Member>} iterator
        """
        if recursive and self.__bases:
            for base in self.__bases:
                for member in base.iter_members():
                    yield member

        if self.__members:
            for member in self.__members.values():
                yield member

    def get_member(self, name):
        """Obtains one of the schema's members given its name.

        @param name: The name of the member to look for.
        @type name: str

        @return: The requested member, or None if the schema doesn't contain a
            member with the indicated name.
        @rtype: L{Member<member.Member>}
        """
        member = self.__members and self.__members.get(name)

        if member is None and self.__bases:
            for base in self.__bases:
                member = base.get_member(name)
                if member:
                    break

        return member

    def __getitem__(self, name):
        """Overrides the indexing operator to retrieve members by name.

        @param name: The name of the member to retrieve.
        @rtype name: str

        @return: A reference to the requested member.
        @rtype: L{Member<member.Member>}

        @raise KeyError: Raised if neither the schema or its bases possess a
            member with the specified name.
        """
        member = self.get_member(name)

        if member is None:
            raise KeyError("%s doesn't define a '%s' member" % (self, name))

        return member

    def __setitem__(self, name, member):
        """Overrides the indexing operator to bind members to the schema under
        the specified name.

        @param name: The name to assign to the member.
        @type name: str

        @param member: The member to add to the schema.
        @type member: L{Member<member.Member>}
        """
        member.name = name
        self.add_member(member)

    def __contains__(self, name):
        """Indicates if the schema contains a member with the given name.

        @param name: The name of the member to search for.
        @type name: str

        @return: True if the schema contains a member by the given name, False
            otherwise.
        @rtype: bool
        """
        return self.get_member(name) is not None

    def validations(self, recursive=True, **validation_parameters):
        """Iterates over all the validation rules that apply to the schema.

        @param recursive: Indicates if validations inherited from base schemas
            should be included. This is the default behavior.

        @return: The sequence of validation rules for the member.
        @rtype: callable iterable
        """
        if self.__bases:

            validations = OrderedSet()

            def descend(schema):

                if schema.__bases:
                    for base in schema.__bases:
                        descend(base)

                if schema._validations:
                    validations.extend(schema._validations)

            descend(self)
            return ListWrapper(validations)

        elif self._validations:
            return ListWrapper(self._validations)

        else:
            return empty_list

    def _default_validation(self, context):
        """Validation rule for schemas. Applies the validation rules defined by
        all members in the schema, propagating their errors."""

        for error in Member._default_validation(self, context):
            yield error

        accessor = (self.accessor or context.get("accessor", None)
                    or get_accessor(context.value))
        languages = context.get("languages")

        for member in self.ordered_members():
            key = member.name

            if member.translated:
                for language in (languages
                                 or accessor.languages(context.value, key)):
                    value = accessor.get(context.value, key, language=language)
                    for error in member.get_errors(value,
                                                   parent_context=context,
                                                   language=language):
                        yield error
            else:
                value = accessor.get(context.value, key, default=None)

                for error in member.get_errors(value, parent_context=context):
                    yield error

    def coerce(self, value: Any, coercion: Coercion,
               **validation_parameters) -> Any:
        """Coerces the given value to conform to the member definition.

        The method applies the behavior indicated by the `coercion` parameter
        to each member of the scheam, either accepting or rejecting its value.
        Depending on the selected coercion strategy, rejected values may be
        transformed into a new value or raise an exception.

        New values are modified in place.
        """
        if coercion is Coercion.NONE:
            return value

        if coercion is Coercion.FAIL:
            errors = list(self.get_errors(value, **validation_parameters))
            if errors:
                raise InputError(self, value, errors)
        else:
            # Coercion of members affected by schema wide validation rules
            accessor = get_accessor(value)
            schema_level_errors = self.get_errors(value,
                                                  recursive=False,
                                                  **validation_parameters)

            for error in schema_level_errors:
                if coercion is Coercion.FAIL_IMMEDIATELY:
                    raise InputError(self, value, [error])
                elif coercion is Coercion.SET_NONE:
                    for member in error.invalid_members:
                        if member.schema is self:
                            accessor.set(value, member.name, None,
                                         error.language)
                elif coercion is Coercion.SET_DEFAULT:
                    for member in error.invalid_members:
                        if member.schema is self:
                            accessor.set(value, member.name,
                                         member.produce_default(value),
                                         error.language)

            # Per member coercion
            if value is not None:
                for member in self.iter_members():
                    if member.translated:
                        for language in accessor.languages(value, member.name):
                            lang_value = accessor.get(value, member.name,
                                                      language)
                            coerced_lang_value = member.coerce(
                                lang_value, coercion, **validation_parameters)
                            if lang_value != coerced_lang_value:
                                accessor.set(value, member.name,
                                             coerced_lang_value, language)
                    else:
                        member_value = accessor.get(value, member.name)
                        coerced_member_value = member.coerce(
                            member_value, coercion, **validation_parameters)
                        if member_value != coerced_member_value:
                            accessor.set(value, member.name,
                                         coerced_member_value)

        return value

    def ordered_members(self, recursive=True):
        """Gets a list containing all the members defined by the schema, in
        order.

        Schemas can define the ordering for their members by supplying a
        L{members_order} attribute, which should contain a series of object or
        string references to members defined by the schema. Members not in that
        list will be appended at the end, sorted by name. Inherited members
        will be prepended, in the order defined by their parent schema.

        The L{Member.before_member} and L{after_member} attributes can also be
        used to alter the position of the member they are defined in.

        Alternatively, schema subclasses can override this method to allow for
        more involved sorting logic.

        @param recursive: Indicates if the returned list should contain members
            inherited from base schemas (True) or if only members directly
            defined by the schema should be included.
        @type recursive: bool

        @return: The list of members in the schema, in order.
        @rtype: L{Member<member.Member>} list
        """
        ordered = []
        relative = list()

        schemas = self.descend_inheritance(True) if recursive else (self, )

        for schema in schemas:
            if schema.__members:
                remaining = set(schema.__members.values())

                if schema.members_order:
                    for member in schema.members_order:
                        if isinstance(member, str):
                            member = schema[member]
                        if not (member.before_member or member.after_member):
                            remaining.remove(member)
                            ordered.append(member)

                for member in list(remaining):
                    if member.before_member or member.after_member:
                        relative.append(member)
                        remaining.remove(member)

                ordered.extend(sorted(remaining, key=lambda m: m.name))

        if relative:

            def insert_relative(member, visited):

                # Disallow conflicting positions
                if member.before_member and member.after_member:
                    raise ValueError(
                        "Can't decide the proper order for %s, it defines "
                        "both 'before_member' and 'after_member'" % member)

                # Prevent cycles
                if member not in visited:
                    visited.add(member)
                else:
                    raise ValueError(
                        "Cycle detected in the relative position for %s" %
                        member)

                # Locate the 'anchor' member
                pos = -1
                anchor = member.before_member or member.after_member

                if isinstance(anchor, str):
                    if recursive:
                        anchor = self.get_member(anchor)
                    else:
                        anchor = self.__members.get(anchor)

                if anchor:
                    # If the anchor member is also relatively positioned,
                    # fix down its position (this works recursively)
                    if (anchor.before_member or anchor.after_member) \
                    and anchor in relative:
                        relative.remove(anchor)
                        pos = insert_relative(anchor, visited)
                    else:
                        pos = ordered.index(anchor)

                # Insert the member
                if pos == -1:
                    pos = len(ordered)
                elif member.after_member:
                    pos += 1

                ordered.insert(pos, member)
                return pos

            while relative:
                insert_relative(relative.pop(0), set())

        return ordered

    def ordered_groups(self, recursive=True):
        """Gets a list containing all the member groups defined by the schema,
        in order.

        @param recursive: Indicates if the returned list should contain groups
            defined by base schemas.
        @type recursive: bool

        @return: The list of groups defined by the schema, in order.
        @rtype: str list
        """
        ordered_groups = []
        visited = set()

        def collect(schema):
            if schema.groups_order:
                for group in schema.groups_order:
                    if group not in visited:
                        visited.add(group)
                        ordered_groups.append(group)

            for member in schema.ordered_members(recursive=False):
                group = member.member_group
                if group and group not in visited:
                    visited.add(group)
                    ordered_groups.append(group)

            if recursive:
                if schema.__bases:
                    for base in schema.__bases:
                        collect(base)

        collect(self)
        return ordered_groups

    def grouped_members(self, recursive=True):
        """Returns the groups of members defined by the schema.

        @param recursive: Indicates if the returned list should contain members
            inherited from base schemas (True) or if only members directly
            defined by the schema should be included.
        @type recursive: bool

        @return: A list of groups in the schema and their members, in order.
            Each group is represented with a tuple containing its name and the
            list of its members.
        @rtype: list(tuple(str, L{Member<cocktail.schema.member.Member>} sequence))
        """
        members_by_group = {}

        for member in self.ordered_members(recursive):
            group_members = members_by_group.get(member.member_group)
            if group_members is None:
                group_members = []
                members_by_group[member.member_group] = group_members
            group_members.append(member)

        groups = []

        for group_name in self.ordered_groups(recursive):
            group_members = members_by_group.get(group_name)
            if group_members:
                groups.append((group_name, group_members))

        ungroupped_members = members_by_group.get(None)
        if ungroupped_members:
            groups.insert(0, (None, ungroupped_members))

        return groups

    def translate_group(self, group, suffix=None):

        if group:
            suffix_str = ".groups." + group + (suffix or "")
        else:
            suffix_str = ".generic_group" + (suffix or "")

        def get_label(schema):

            if schema.name:
                label = translations(
                    getattr(schema, "full_name", schema.name) + suffix_str)
                if label:
                    return label

                for alias in schema.schema_aliases:
                    label = translations(alias + suffix_str)
                    if label:
                        return label

            for base in schema.bases:
                label = get_label(base)
                if label:
                    return label

        label = get_label(self)

        if label:
            return label

        source_schema = self.source_member
        if source_schema and hasattr(source_schema, "translate_group"):
            return source_schema.translate_group(group, suffix=suffix)

        return None

    def insert_group(self, group, before=None, after=None):

        if before is None and after is None:
            raise ValueError(
                "insert_group() requires a value for either the 'after' or "
                "'before' parameters")
        elif before is not None and after is not None:
            raise ValueError(
                "insert_group() can't take both 'after' and 'before' "
                "parameters at the same time")

        anchor = before or after
        if not isinstance(self.groups_order, list):
            self.groups_order = list(self.groups_order)

        try:
            pos = self.groups_order.index(anchor)
        except ValueError:
            self.groups_order.append(group)
        else:
            if after:
                pos += 1
            self.groups_order.insert(pos, group)

    def extract_searchable_text(self, extractor):

        obj = extractor.current.value

        for member in self.iter_members():
            if member.text_search:
                if member.language_dependant:
                    for language in extractor.iter_node_languages():
                        if language is not None:
                            value = get(obj, member, language=language)
                            extractor.extract(member, value, language)
                else:
                    value = get(obj, member)
                    extractor.extract(member, value)

    def to_json_value(self, value, **options):

        languages = options.get("languages", None)
        if languages is None:
            languages = get_language()

        if value is None:
            return None

        record = {}
        accessor = get_accessor(value)

        for member in self.iter_members():
            if member.translated and not isinstance(languages, str):
                member_value = dict(
                    (lang,
                     member.to_json_value(accessor.get(value, language=lang),
                                          **options))
                    for lang in accessor.languages(value, member.name))
            else:
                member_value = member.to_json_value(
                    accessor.get(value, member.name), **options)

            record[member.name] = member_value

        return record

    def from_json_value(self, value, **options):

        if value is None:
            return None

        record = (self.type or dict)()
        accessor = get_accessor(record)

        for key, member_value in value.items():
            member = self.get_member(key)
            if member is None:
                continue
            if member.translated and isinstance(member_value, Mapping):
                if member_value:
                    for lang, lang_value in member_value.items():
                        accessor.set(record,
                                     member.name,
                                     member.from_json_value(
                                         lang_value, **options),
                                     language=lang)
            else:
                accessor.set(record, member.name,
                             member.from_json_value(member_value, **options))

        return record
Exemple #28
0
class SchemaObject(object, metaclass=SchemaClass):

    _generates_translation_schema = False
    _translation_schema_base = None
    bidirectional = True

    declared = Event(doc="""
        An event triggered on the class after the declaration of its schema has
        finished.
        """)

    instantiated = Event(doc="""
        An event triggered on the class when a new instance is created.

        @ivar instance: A reference to the new instance.
        @type instance: L{SchemaObject}

        @ivar values: The parameters passed to the constructor.
        @type values: dict
        """)

    changing = Event(doc="""
        An event triggered before the value of a member is changed.

        @ivar member: The member that is being changed.
        @type member: L{Member<cocktail.schema.member.Member>}

        @ivar language: Only for translated members, indicates the language
            affected by the change.
        @type language: str

        @ivar value: The new value assigned to the member. Modifying this
            attribute *will* change the value which is finally assigned to the
            member. Will be None for collections.

        @ivar previous_value: The current value for the affected member, before
            any change takes place. Will be None for collections.

        @ivar translation_changes: A dictionary indicating the translations
            that will be affected by this change, and their respective values
            before the change takes effect. Will be None if the changed member
            is not translatable.

        @ivar added: Only for collections. A collection of all the items added
            to the collection.

        @ivar removed: Only for collections. A collection of all the items
            removed from the collection.

        @ivar change_context: A dictionary containing arbitrary key / value
            pairs. It is shared with the L{changed} event, which makes it
            possible to pass information between both events; the typical use
            case for this involves storing object state during the L{changing}
            event so it can be read by L{changed} event handlers.
        """)

    changed = Event(doc="""
        An event triggered after the value of a member has changed.

        @ivar member: The member that has been changed.
        @type member: L{Member<cocktail.schema.member.Member>}

        @ivar language: Only for translated members, indicates the language
            affected by the change.
        @type language: str

        @ivar value: The new value assigned to the member. Will be None for
            collections.

        @ivar previous_value: The value that the member had before the current
            change. Will be None for collections.

        @ivar added: Only for collections. A collection of all the items added
            to the collection.

        @ivar removed: Only for collections. A collection of all the items
            removed from the collection.

        @ivar translation_changes: A dictionary indicating the translations
            that will be affected by this change, and their respective values
            before the change took effect. Will be None if the changed member
            is not translatable.

        @ivar change_context: A dictionary containing arbitrary key / value
            pairs. It is shared with the L{changing} event, which makes it
            possible to pass information between both events; the typical use
            case for this involves storing object state during the L{changing}
            event so it can be read by L{changed} event handlers.
        """)

    collection_item_added = Event(doc="""
        An event triggered after an item is added to one of the object's
        collections.

        @ivar member: The collection that the item has been added to.
        @type member: L{Collection<cocktail.schema.schemacollections.Collection>}

        @ivar item: The item added to the collection.
        @type item: obj
        """)

    collection_item_removed = Event(doc="""
        An event triggered after an item is removed from one of the object's
        collections.

        @ivar member: The collection that the item has been removed from.
        @type member: L{Collection<cocktail.schema.schemacollections.Collection>}

        @ivar item: The item removed from the collection.
        @type item: obj
        """)

    related = Event(doc="""
        An event triggered after establishing a relationship between objects.
        If both ends of the relation are instances of L{SchemaObject}, this
        event will be triggered twice.

        @ivar member: The schema member that describes the relationship.
        @type member: L{Relation<cocktail.schema.schemarelations>}

        @ivar related_object: The object that has been related to the target.
        """)

    unrelated = Event(doc="""
        An event triggered after clearing a relationship between objects.

        @ivar member: The schema member that describes the relationship.
        @type member: L{Relation<cocktail.schema.schemarelations>}

        @ivar related_object: The object that is no longer related to the
            target.
        """)

    adding_translation = Event(doc="""
        An event triggered when a new translation is being added.

        @ivar language: The language of the added translation
        @type language: str

        @ivar translation: The new value assigned to the translation.
        """)

    removing_translation = Event(doc="""
        An event triggered when a translation is being removed.

        @ivar language: The language of the removed translation
        @type language: str

        @ivar translation: The value of the translation.
        """)

    def __init__(self, **values):

        # If supplied, the "bidirectional" attribute must be set before
        # setting any other attribute, otherwise bidirectional relations may
        # still be set!
        bidirectional = values.pop("bidirectional", None)
        if bidirectional is not None:
            self.bidirectional = bidirectional

        self.__class__.init_instance(
            self,
            values,
            SchemaObjectAccessor,
            excluded_members={self.__class__.translations}
            if self.__class__.translated else None)
        self.__class__.instantiated(instance=self, values=values)

    def __repr__(self):
        label = self.__class__.__name__

        primary_member = self.__class__.primary_member
        if primary_member:
            id = getattr(self, primary_member.name, None)
            if id is not None:
                label += " #%s" % id

        try:
            trans = translations(self, discard_generic_translation=True)
            if trans:
                label += " (%s)" % trans
        except NoActiveLanguageError:
            pass

        return label

    def get(self, member, language=None):

        # Normalize the member argument to a member reference
        if not isinstance(member, Member):

            if isinstance(member, str):
                member = self.__class__[member]
            else:
                raise TypeError("Expected a string or a member reference")

        getter = member.schema.__dict__[member.name].__get__
        return getter(self, None, language)

    def set(self, member, value, language=None):

        # Normalize the member argument to a member reference
        if not isinstance(member, Member):

            if isinstance(member, str):
                member = self.__class__[member]
            else:
                raise TypeError("Expected a string or a member reference")

        setter = member.schema.__dict__[member.name].__set__
        setter(self, value, language)

    def new_translation(self, language):
        return self.translation(translated_object=self, language=language)

    def iter_derived_translations(self, language=None, include_self=False):
        language = require_language(language)
        for derived_lang in descend_language_tree(language,
                                                  include_self=include_self):
            for chain_lang in iter_language_chain(derived_lang):
                if chain_lang == language:
                    yield derived_lang
                    break
                elif chain_lang in self.translations:
                    break

    def iter_translations(self, include_derived=True, languages=None):

        for language in self.translations:

            if languages is not None and language not in languages:
                continue

            yield language

            if include_derived:
                for derived_language in descend_language_tree(
                        language, include_self=False):
                    if derived_language not in self.translations:
                        yield derived_language

    def consolidate_translations(self, root_language=None, members=None):

        redundant_languages = []

        # If no language is given, apply the process to all the root languages
        # defined by the translated object
        if root_language is None:
            processed_root_languages = set()
            for language in list(self.translations):
                root_language = get_root_language(language)
                if root_language not in processed_root_languages:
                    processed_root_languages.add(root_language)
                    redundant_languages += \
                        self.consolidate_translations(root_language)
            return redundant_languages

        # Find all present translations for languages derived from the given
        # root language
        affected_langs = [
            language for language in descend_language_tree(root_language,
                                                           include_self=False)
            if language in self.translations
        ]

        if not affected_langs:
            return redundant_languages

        # Find translatable members
        if members is None:
            members = [
                member for member in self.__class__.iter_members()
                if member.translated
            ]

        def get_translated_values(language):
            values = []
            for member in members:
                if member.translated:
                    value = self.get(member, language)
                    values.append((member, value))
            return tuple(values)

        # Translation for the root language present; use it
        if root_language in self.translations:
            common_values = get_translated_values(root_language)

        # Otherwise, find the most common translation and use it as the root
        # translation
        else:
            count = Counter(
                get_translated_values(language) for language in affected_langs)
            common_values = count.most_common()[0][0]
            for member, value in common_values:
                self.set(member, value, root_language)

        # Delete redundant translations
        for language in affected_langs:
            if get_translated_values(language) == common_values:
                del self.translations[language]
                redundant_languages.append(language)

        return redundant_languages

    def get_source_locale(self, locale):
        translations = self.translations
        for locale in iter_language_chain(locale):
            if locale in translations:
                return locale

    def create_text_extractor(self, **kwargs):
        extractor = TextExtractor(**kwargs)
        extractor.extract(self.__class__, self)
        return extractor

    def get_searchable_text(self, languages=None, **kwargs):
        return str(self.create_text_extractor(languages=languages, **kwargs))

    copy_excluded_members = frozenset()

    def create_copy(self, member_copy_modes=None):
        clone = self.__class__()
        self.init_copy(clone, member_copy_modes=member_copy_modes)
        return clone

    def init_copy(self, copy, member_copy_modes=None):

        for member in self.__class__.members().values():
            copy_mode = self.get_member_copy_mode(
                member, member_copy_modes=member_copy_modes)

            if copy_mode == DO_NOT_COPY:
                continue

            if not member.translated:
                languages = (None, )
            else:
                languages = list(self.translations)

            for language in languages:
                value = self.copy_value(member,
                                        language,
                                        mode=copy_mode,
                                        receiver=copy,
                                        member_copy_modes=member_copy_modes)
                copy.set(member, value, language)

    def get_member_copy_mode(self, member, member_copy_modes=None):

        # Explicit mode set by an override
        if member_copy_modes:
            copy_mode = member_copy_modes.get(member)
            if copy_mode is not None:
                return copy_mode

        # Explicit mode set by the member
        if member.copy_mode is not None:
            return member.copy_mode

        # Implicit mode, based on member properties
        if (member.name == "translations" or member.primary
                or member in self.copy_excluded_members):
            return DO_NOT_COPY

        if isinstance(member, (Reference, Collection)):

            if member.anonymous:
                return DO_NOT_COPY

            if member.integral:
                return DEEP_COPY

        return SHALLOW_COPY

    def copy_value(self,
                   member,
                   language,
                   mode=SHALLOW_COPY,
                   receiver=None,
                   member_copy_modes=None):
        value = self.get(member, language)

        if value is not None and mode != SHALLOW_COPY:
            if isinstance(member, Reference):
                if mode == DEEP_COPY \
                or (callable(mode) and mode(self, member, value)):
                    value = value.create_copy(
                        member_copy_modes=None if member.related_end is None
                        else {member.related_end: DO_NOT_COPY})
            else:
                items = []

                for item in value:
                    if item is not None and (mode == DEEP_COPY or
                                             (callable(mode)
                                              and mode(self, member, item))):
                        item = item.create_copy(
                            member_copy_modes=None if member.related_end is
                            None else {member.related_end: DO_NOT_COPY})

                    items.append(item)

                value = items

        return value

    @classmethod
    def observe_relation_changes(cls, relation_path, members=None):
        def decorator(change_handler):

            # Observe relate / unrelate operations at any point of the relation
            # chain. The RelationObserver class is used to propagate them
            # upwards towards the root of the chain and finally trigger the
            # decorated callback.
            model = cls
            current_path = []

            for rel_name in relation_path.split("."):
                relation = model.get_member(rel_name)

                if relation is None:
                    raise ValueError(
                        "Can't observe the relation path %s: %s doesn't contain a "
                        "%s relation" % (relation_path, model, rel_name))

                current_path.append(relation)
                observer = RelationObserver(list(current_path), change_handler)
                model.related.append(observer)
                model.unrelated.append(observer)
                model = relation.related_type

            # Observe changes in certain fields of the objects at the end of the
            # relation chain (optional).
            if members:

                # Normalize the subset of members to observe (if any)
                member_subset = set()

                for member in members:
                    if isinstance(member, str):
                        member_name = member
                        member = model.get_member(member_name)
                    else:
                        member_name = member.name

                    if member is None or not issubclass(model, member.schema):
                        raise ValueError(
                            "Error while setting up a relation observer "
                            "for %s: can't find member '%s' in %s" %
                            (relation_path, member_name, model))

                    member_subset.add(member)

                # Observe changes to the member subset
                observer = RelationObserver(current_path, change_handler,
                                            member_subset)
                model.changed.append(observer)

            return change_handler

        return decorator

    def get_translated_value(self, member, language=None, **kwargs):

        if isinstance(member, str):
            member = self.__class__.get_member(member)

        value = self.get(member, language)
        return member.translate_value(value, **kwargs)

    def get_translated_values(self, member, languages=None):
        return TranslatedValues(
            (language, self.get(member, language))
            for language in (languages or self.translations))
class ExportJob:

    export = None
    exporter = None
    dependencies = None
    reset = False
    errors = "resume"  # "resume" or "raise"
    encoding = "utf-8"

    selecting_export_urls = Event()
    export_starting = Event()
    export_failed = Event()
    export_completed = Event()
    export_ended = Event()
    task_starting = Event()
    task_executed = Event()
    task_successful = Event()
    task_failed = Event()
    dependency_transfers_starting = Event()
    dependency_transfer_starting = Event()
    dependency_transfer_successful = Event()
    dependency_transfer_failed = Event()

    css_url_regexp = re.compile(r"""url\(([^)]+)\)""")
    js_string_regexp = re.compile(
        r"""
        (?P<delim>['"])
        (?P<value>.*)
        \1              # Same string delimiter used in the opening
        (?!\\)          # Not preceded by a escape character
        """, re.VERBOSE)
    js_url_regexp = re.compile(
        r"""
        (?P<head>
            (\s+src\s*=|\s+href\s*=|\Wurl\()
            \s*
            \\?
            ['"]?
        )
        (?P<url>.*?)
        (?P<tail>['")\\])
        """, re.VERBOSE)

    def __init__(self, export):
        self.export = export
        self.exporter = self.create_exporter()
        self.document_urls = set()
        self.dependencies = set()
        self.pending_dependencies = set()
        self.__url_resolutions = {}

    def create_exporter(self, **kwargs) -> Exporter:
        return self.export.destination.create_exporter(**kwargs)

    def get_export_urls(self, item: PublishableObject,
                        language: str) -> Sequence[URL]:

        e = self.selecting_export_urls(
            item=item,
            language=language,
            urls=[item.get_uri(host="!", language=language)])
        return e.urls

    def get_source_url(self,
                       item: PublishableObject,
                       language: str,
                       path: Sequence[str] = None,
                       parameters: Dict[str, str] = None) -> URL:

        return item.get_uri(language=language,
                            path=path,
                            parameters=parameters,
                            host="!")

    def execute(self):
        @transaction
        def begin():
            self.export.state = "running"
            if self.reset:
                for task in self.export.tasks.itervalues():
                    task["state"] = "pending"

        with self.exporter:

            self.export_starting()

            try:
                for task in self.export.tasks.itervalues():

                    # Ignore completed / failed tasks
                    if task["state"] != "pending":
                        continue

                    # Give other scripts a chance to abort the export operation
                    datastore.sync()
                    if self.export.state != "running":
                        raise Halt()

                    self.execute_task(task)

                if self.dependencies:
                    self.export_dependencies()

            except Halt:
                pass
            except Exception as error:

                @transaction
                def complete():
                    self.export.state = "idle"

                self.export_failed(error=error)
                if self.errors == "raise":
                    raise
            else:

                @transaction
                def complete():
                    self.export.state = "completed"

                self.export_completed()
            finally:
                self.export_ended()
                self.exporter.close()

    def execute_task(self, task: dict):

        self.task_starting(task=task)

        action = task["action"]
        item = task["item"]
        language = task["language"]

        try:
            tags = set()

            for source_url in self.get_export_urls(item, language):

                if action == "post":
                    resource = ExportedResource(self, source_url)
                    resource.language = language
                    resource.open(**self.get_request_parameters(resource))

                    url_tags = resource.headers.get("X-Woost-Cache-Tags")
                    if url_tags:
                        tags.update(url_tags.split())

                    self.process_resource(resource)

                    # Prevent documents from also being downloaded as
                    # dependencies
                    self.document_urls.add(resource.source_url)
                    self.dependencies.discard(resource.source_url)
                    self.pending_dependencies.discard(resource.source_url)

                    self.exporter.write_file(
                        resource.export_path,
                        resource.content,
                        content_type=resource.content_type)

                elif action == "delete":
                    export_path = \
                        self.export.destination.get_export_path(source_url)
                    self.exporter.remove_file(export_path)

            self.task_executed(task=task)

        except Exception as export_error:
            if self.errors == "raise":
                raise
        else:
            export_error = None

        @transaction
        def update_task():

            if export_error:
                task["state"] = "failed"
                task["error_message"] = repr(export_error)
            else:
                task["state"] = "success"
                publishable = task["item"]
                self.export.destination.set_pending_task(
                    publishable, language, None)
                self.export.destination.set_exported_content_tags(
                    publishable, language, tags)

        if export_error:
            self.task_failed(task=task, error=export_error)
        else:
            self.task_successful(task=task)

    def get_request_parameters(self,
                               resource: 'ExportedResource') -> Dict[str, str]:

        headers = {
            "User-agent": USER_AGENT,
            EXPORT_HEADER: str(self.export.id),
        }

        if self.export.auth_token:
            headers[app.authentication.AUTH_TOKEN_HEADER] = \
                self.export.auth_token

        return {"headers": headers}

    def process_resource(self, resource: 'ExportedResource'):

        if resource.content_type == "text/html":
            document = BeautifulSoup(resource.content, features="lxml")
            self.process_html(document, resource)
            resource.content = str(document)

        elif resource.content_type == "text/css":
            css = resource.content.decode(self.encoding)
            css = self.process_css(css, resource)
            resource.content = css.encode(self.encoding)

    def process_html(self, document: BeautifulSoup,
                     resource: 'ExportedResource'):

        # Process embedded styles
        for element in document.find_all("style"):
            content_type = element.get("type")
            if ((not content_type or content_type == "text/css")
                    and element.string):
                element.string = self.process_css(element.string, resource)

        # Process embdded scripts
        for element in document.find_all("script"):
            content_type = element.get("type")
            if ((not content_type or content_type == "text/javascript")
                    and element.string):
                element.string = self.process_embedded_javascript(
                    element.string, resource)

        # Transform URLs
        for element, attr, url, content_type \
        in self.iter_urls_in_html(document, resource):
            self.process_html_url(element, attr, url, content_type, resource)

    def iter_urls_in_html(
            self, document: BeautifulSoup,
            resource: 'ExportedResource') -> Iterable[ResourceWithinDocument]:

        for link in document.find_all("link"):
            href = link.get("href")
            if href:
                ctype = link.get("type")
                if not ctype:
                    rel = link.get("rel")
                    if rel and str(rel).lower() == "stylesheet":
                        ctype = "text/css"
                yield link, "href", URL(href), ctype

        for script in document.find_all("script"):
            src = script.get("src")
            if src:
                yield (script, "src", URL(src), script.get("type")
                       or "application/javascript")

        for img in document.find_all("img"):
            src = img.get("src")
            if src:
                yield img, "src", URL(src), None

        for video in document.find_all("video"):
            src = video.get("src")
            if src:
                yield video, "src", URL(src), video.get("type")

        for audio in document.find_all("audio"):
            src = audio.get("src")
            if src:
                yield audio, "src", URL(src), audio.get("type")

        for source in document.find_all("source"):
            src = source.get("src")
            if src:
                yield source, "src", URL(src), source.get("type")

        for a in document.find_all("a"):
            href = a.get("href")
            if href and not href.startswith("#"):
                yield a, "href", URL(href), a.get("type")

        for iframe in document.find_all("iframe"):
            src = iframe.get("src")
            if src:
                yield iframe, "src", URL(src), None

    def process_html_url(self, element: Tag, attr: str, url: URL,
                         content_type: str, resource: 'ExportedResource'):

        if url.scheme in ("javascript", "mailto"):
            return url

        url = self.normalize_href(url, resource)

        # Collect dependencies
        self.add_dependency(url, content_type=content_type)

        # Transform the resource URL into a relative path
        url = self.transform_href(url, resource, content_type=content_type)
        element[attr] = url

    def process_css(self, content: str, resource: 'ExportedResource') -> str:
        def replace_url(match):

            value = match.group(1).strip("'").strip('"')

            if value.startswith("javascript:"):
                return value

            url = URL(value)
            url = self.normalize_href(url, resource)
            self.add_dependency(url, content_type="text/css")
            url = self.transform_href(url, resource, content_type="text/css")
            return f"url('{url}')"

        return self.css_url_regexp.sub(replace_url, content)

    def process_embedded_javascript(self, content: str,
                                    resource: 'ExportedResource') -> str:
        def process_strings(match):
            c = match.group("delim")
            return (c +
                    self.js_url_regexp.sub(replace_url, match.group("value")) +
                    c)

        def replace_url(match):

            value = match.group("url")

            if value.startswith("javascript:"):
                return match.group(0)

            url = URL(value)
            url = self.normalize_href(url, resource)
            self.add_dependency(url, content_type="application/javascript")
            url = self.transform_href(url,
                                      resource,
                                      content_type="application/javascript")
            return match.group("head") + url + match.group("tail")

        return self.js_string_regexp.sub(process_strings, content)

    def get_base_url(self, url: URL) -> URL:
        if len(url.path) > 1:
            return url.copy(path=url.path.pop(-1))
        else:
            return url

    def normalize_href(self, url: URL, resource: 'ExportedResource') -> URL:

        # Normalize relative URLs using the source URL for the processed
        # document
        if not url.hostname:
            url = resource.base_url.copy(path=resource.base_url.path.merge(
                url.path),
                                         query=url.query,
                                         fragment=url.fragment)

        # Normalize URLs to their canonical form
        if not self.url_is_external(url):
            resolution = self.resolve_url(url)
            if resolution and resolution.publishable:
                url = app.url_mapping.get_canonical_url(
                    url, language=resource.language, preserve_extra_path=True)

        return url

    def transform_href(self,
                       url: URL,
                       resource: "ExportedResource",
                       content_type: str = None) -> URL:

        if self.url_is_external(url):
            return url
        else:
            if self.should_make_url_absolute(url, resource):
                return self.export.destination.get_export_url(
                    url, content_type=content_type)
            else:
                return self.get_relative_url(url,
                                             resource,
                                             content_type=content_type)

    def should_make_url_absolute(self, url: URL,
                                 resource: 'ExportedResource') -> bool:

        return False

    def get_relative_url(self,
                         url: URL,
                         resource: "ExportedResource",
                         content_type: str = None) -> URL:

        # Express URLs as paths relative to the exported document
        url_export_path = self.export.destination.get_export_path(
            url, content_type=content_type)

        i = 0
        for a, b in zip_longest(resource.export_folder, url_export_path):
            if a != b:
                break
            i += 1

        url_export_path = url_export_path[i:]

        for n in range(len(resource.export_folder) - i):
            url_export_path.insert(0, u"..")

        return URL(path=url_export_path,
                   query=url.query,
                   fragment=url.fragment)

    def url_is_external(self, url: URL) -> bool:

        if not url.hostname:
            return False

        config = Configuration.instance
        return config.get_website_by_host(url.hostname) is None

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

        # Ignore external URLs
        if self.url_is_external(url):
            return False

        # Ignore publishable elements
        resolution = self.resolve_url(url)
        return (
            not resolution or not resolution.publishable or
            (content_type or resolution.publishable.mime_type) != "text/html")

    def resolve_url(self, url: URL) -> URLResolution:
        try:
            return self.__url_resolutions[url]
        except KeyError:
            resolution = app.url_mapping.resolve(url)
            self.__url_resolutions[url] = resolution
            return resolution

    def add_dependency(self, url: URL, content_type: str = None):

        if url not in self.dependencies and url not in self.document_urls:
            if self.url_is_exportable_dependency(url, content_type):
                self.dependencies.add(url)
                self.pending_dependencies.add(url)

    def export_dependencies(self):

        if self.dependencies:
            self.dependency_transfers_starting()

        while self.pending_dependencies:

            source_url = self.pending_dependencies.pop()
            resource = ExportedResource(self, source_url)
            self.dependency_transfer_starting(resource=resource)

            try:
                resource.open(**self.get_request_parameters(resource))
                self.process_resource(resource)
                self.exporter.write_file(resource.export_path,
                                         resource.content,
                                         content_type=resource.content_type)
            except Exception as export_error:
                self.dependency_transfer_failed(resource=resource,
                                                error=export_error)
                if self.errors == "raise":
                    raise
            else:
                self.dependency_transfer_successful(resource=resource)
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."
            )