Пример #1
0
class Content(object):
    template = None
    template_engine = None
    _json_dict = None
    _loc = None
    '''Template engine to render this content. Overwritten my metadata.
    If not available, the application default engine is used'''
    mandatory_properties = ()

    def __init__(self, app, content, metadata, src, path=None, context=None,
                 **params):
        self._app = app
        self._content = content
        self._context_for = context
        self._additional_context = {}
        self._src = src
        self._path = path or src
        self._meta = AttributeDictionary(params)
        if src:
            self._meta.modified = modified_datetime(src)
        else:
            self._meta.modified = datetime.now()
        # Get the site meta data dictionary.
        # Used to render Content metadata
        self._update_meta(metadata)
        meta = self._meta
        if self.is_html:
            dir, slug = os.path.split(self._path)
            if not slug:
                slug = self._path
                dir = None
            if not meta.slug:
                meta.slug = slugify(slug, separator='_')
            if dir:
                meta.slug = '%s/%s' % (dir, meta.slug)
        else:
            if self.suffix:  # Any other file
                suffix = '.%s' % self.suffix
                if not self._path.endswith(suffix):
                    self._path = self._path + suffix
            if not meta.slug:
                meta.slug = self._path
        meta.name = slugify(meta.slug, separator='_')
        for name in self.mandatory_properties:
            if not meta.get(name):
                raise BuildError("Property '%s' not available in %s"
                                 % (name, self))

    @property
    def name(self):
        return self._meta.name

    @property
    def src(self):
        return self._src

    @property
    def loc(self):
        return self._loc or self._src

    @loc.setter
    def loc(self, value):
        self._loc = value

    @property
    def content_type(self):
        return self._meta.content_type

    @property
    def is_text(self):
        return self._meta.content_type in CONTENT_EXTENSIONS

    @property
    def is_html(self):
        return is_html(self._meta.content_type)

    @property
    def suffix(self):
        return CONTENT_EXTENSIONS.get(self._meta.content_type)

    @property
    def path(self):
        return self._path

    @property
    def reldate(self):
        return self._meta.date or self._meta.modified

    @property
    def year(self):
        return self.reldate.year

    @property
    def month(self):
        return self.reldate.month

    @property
    def month2(self):
        return self.reldate.strftime('%m')

    @property
    def month3(self):
        return self.reldate.strftime('%b').lower()

    @property
    def id(self):
        if self.is_html:
            return '%s.json' % self._meta.slug

    @property
    def context_for(self):
        '''A list of contents names for which this snippet is required
        in the context dictionary
        '''
        return self._context_for

    @property
    def additional_context(self):
        '''Dictionary of key and :class:`.Snippet` providing additional
        keys for this content
        '''
        return self._additional_context

    def __repr__(self):
        return self._src
    __str__ = __repr__

    def key(self, name=None):
        '''The key for a context dictionary
        '''
        name = name or self.name
        return 'html_%s' % name if self.is_html else name

    def context(self, context=None):
        '''Extract the context dictionary for server side template rendering
        '''
        ctx = dict(self._flatten(self._meta))
        if context:
            ctx.update(context)
        return ctx

    def urlparams(self, names=None):
        urlparams = {}
        if names:
            for name in names:
                value = self._meta.get(name) or getattr(self, name, None)
                if value in (None, ''):
                    if name == 'id':
                        raise SkipBuild
                    elif names:
                        raise KeyError("%s could not obtain url variable '%s'"
                                       % (self, name))
                urlparams[name] = value
        return urlparams

    def render(self, context=None):
        '''Render the content
        '''
        if self.is_html:
            context = self.context(context)
            content = self._engine(self._content, context)
            if self.template:
                template = self._app.template_full_path(self.template)
                if template:
                    context[self.key('main')] = content
                    with open(template, 'r') as file:
                        template_str = file.read()
                    raw = self._engine(template_str, context)
                    reader = get_reader(self._app, template)
                    ct = reader.process(raw, template)
                    content = ct._content
            return content
        else:
            return self._content

    def json(self, request):
        '''Convert the content into a Json dictionary for the API
        '''
        if not self._json_dict and self.is_html:
            context = self._app.context(request)
            context = self.context(context)
            # Add additional context keys
            if self.additional_context:
                for key, ct in self.additional_context.items():
                    if isinstance(ct, Content):
                        key = ct.key(key)
                        ct = ct.render(context)
                    context[key] = ct
            #
            assert self.suffix
            data = self._to_json(self._meta)
            text = data.get(self.suffix) or {}
            data[self.suffix] = text
            text['main'] = self.render(context)
            #
            head = {}
            for key in HEAD_META:
                value = data.get(key)
                if value:
                    head[key] = value
            #
            require_css = data.get('require_css')
            if require_css:
                data['require_css'] = []
                cfg = request.config
                links = Links(cfg['MEDIA_URL'],
                              minified=cfg['MINIFIED_MEDIA'])
                for css in require_css:
                    css = CssLibraries.get(css, css)
                    links.append(css)
                for link in links.children:
                    link = link.split("href=")
                    if len(link) == 2:
                        href = link[1]
                        c = href[0]
                        href = href[1:]
                        link = href[:href.find(c)]
                        data['require_css'].append(link)
            #
            if 'head' in data:
                head.update(data['head'])

            data['head'] = head
            self._json_dict = data
        return self._json_dict

    def html(self, request):
        '''Build the ``html_main`` key for this content and set
        content specific values to the ``head`` tag of the
        HTML5 document.
        '''
        if not self.is_html:
            raise Unsupported
        # The JSON data for this page
        data = self.json(request)
        doc = request.html_document
        doc.jscontext['page'] = dict(page_info(data))
        #
        doc.meta.update({'og:image': data.get('image'),
                         'og:published_time': data.get('date'),
                         'og:modified_time': data.get('modified')})
        doc.meta.update(data['head'])
        #
        if not request.config['ANGULAR_UI_ROUTER']:
            for css in data.get('require_css') or ():
                doc.head.links.append(css)
            doc.head.scripts.require.extend(data.get('require_js') or ())
        #
        if request.cache.uirouter is False:
            doc.head.scripts.require.extend(data.get('require_js') or ())

        self.on_html(doc)
        return data[self.suffix]['main']

    def on_html(self, doc):
        pass

    @classmethod
    def as_draft(cls):
        mp = tuple((a for a in cls.mandatory_properties
                    if a not in no_draft_field))
        return cls.__class__('Draft', (cls,), {'mandatory_properties': mp})

    # INTERNALS
    def _update_meta(self, metadata):
        meta = self._meta
        meta.site = self._app.extensions['static'].build_info(self._app)
        context = self.context()
        for name in ('template_engine', 'template'):
            default = getattr(self, name)
            value = metadata.pop(name, default)
            meta.site[name] = value
            setattr(self, name, value)
        self._engine = engine = self._app.template_engine(self.template_engine)
        meta.update(((key, self._render_meta(value, context))
                    for key, value in metadata.items()))

    def _flatten(self, meta):
        for key, value in mapping_iterator(meta):
            if isinstance(value, Mapping):
                for child, value in self._flatten(value):
                    yield '%s_%s' % (key, child), value
            else:
                yield key, self._to_string(value)

    def _to_string(self, value):
        if isinstance(value, Mapping):
            raise BuildError('A dictionary found when coverting to string')
        elif isinstance(value, (list, tuple)):
            return ', '.join(self._to_string(v) for v in value)
        elif isinstance(value, date):
            return iso8601(value)
        else:
            return to_string(value)

    def _render_meta(self, value, context):
        if isinstance(value, Mapping):
            return dict(((k, self._render_meta(v, context))
                         for k, v in value.items()))
        elif isinstance(value, (list, tuple)):
            return [self._render_meta(v, context) for v in value]
        elif isinstance(value, date):
            return value
        elif value is not None:
            return self._engine(to_string(value), context)

    def _to_json(self, value):
        if isinstance(value, Mapping):
            return dict(((k, self._to_json(v)) for k, v in value.items()))
        elif isinstance(value, (list, tuple)):
            return [self._to_json(v) for v in value]
        elif isinstance(value, date):
            return iso8601(value)
        else:
            return value
Пример #2
0
class Router(RouterType('RouterBase', (object,), {})):
    '''A :ref:`WSGI middleware <wsgi-middleware>` to handle client requests
    on multiple :ref:`routes <apps-wsgi-route>`.

    The user must implement the HTTP methods
    required by the application. For example if the route needs to
    serve a ``GET`` request, the ``get(self, request)`` method must
    be implemented.

    :param rule: String used for creating the :attr:`route` of this
        :class:`Router`.
    :param routes: Optional :class:`Router` instances which are added to the
        children :attr:`routes` of this router.
    :param parameters: Optional parameters for this router.
        They are stored in the :attr:`parameters` attribute.
        If a ``response_content_types`` value is
        passed, it overrides the :attr:`response_content_types` attribute.

    .. attribute:: routes

        List of children :class:`Router` of this :class:`Router`.

    .. attribute:: parent

        The parent :class:`Router` of this :class:`Router`.

    .. attribute:: response_content_types

        a list/tuple of possible content types of a response to a
        client request.

        The client request must accept at least one of the response content
        types, otherwise an HTTP ``415`` exception occurs.

    .. attribute:: allows_redirects

        boolean indicating if this router can redirect requests to valid urls
        within this router and its children. For example, if a router serves
        the '/echo' url but not the ``/echo/`` one, a request on ``/echo/``
        will be redirected to ``/echo``.

        Default: ``False``

    .. attribute:: parameters

        A :class:`.AttributeDictionary` of parameters for
        this :class:`Router`. Parameters are created at initialisation from
        the ``parameters`` class attribute and the key-valued parameters
        passed to the ``__init__`` method for which the value is not callable.
    '''
    _creation_count = 0
    _parent = None
    _name = None

    response_content_types = RouterParam(None)
    allows_redirects = RouterParam(False)

    def __init__(self, rule, *routes, **parameters):
        Router._creation_count += 1
        self._creation_count = Router._creation_count
        if not isinstance(rule, Route):
            rule = Route(rule)
        self._route = rule
        self._name = parameters.pop('name', rule.rule)
        self.routes = []
        # add routes specified via the initialiser
        for router in routes:
            self.add_child(router)
        # copy parameters
        self.parameters = AttributeDictionary(self.parameters)
        for name, rule_method in self.rule_methods.items():
            rule, method, params, _, _ = rule_method
            rparameters = params.copy()
            handler = getattr(self, name)
            router = self.add_child(Router(rule, **rparameters))
            setattr(router, method, handler)
        for name, value in parameters.items():
            if name in self.parameters:
                self.parameters[name] = value
            else:
                setattr(self, name, value)

    @property
    def route(self):
        '''The relative :class:`.Route` served by this
        :class:`Router`.
        '''
        parent = self._parent
        if parent and parent._route.is_leaf:
            return parent.route + self._route
        else:
            return self._route

    @property
    def full_route(self):
        '''The full :attr:`route` for this :class:`.Router`.

        It includes the :attr:`parent` portion of the route if a parent
        router is available.
        '''
        if self._parent:
            return self._parent.full_route + self._route
        else:
            return self._route

    @property
    def name(self):
        '''The name of this :class:`Router`.

        This attribute can be specified during initialisation.
        If available, it can be used to retrieve a child router
        by name via the :meth:`get_route` method.
        '''
        return self._name

    @property
    def root(self):
        '''The root :class:`Router` for this :class:`Router`.'''
        if self.parent:
            return self.parent.root
        else:
            return self

    @property
    def parent(self):
        return self._parent

    @property
    def default_content_type(self):
        '''The default content type for responses.

        This is the first element in the :attr:`response_content_types` list.
        '''
        ct = self.response_content_types
        return ct[0] if ct else None

    @property
    def creation_count(self):
        '''Integer for sorting :class:`Router` by creation.

        Auto-generated during initialisation.'''
        return self._creation_count

    @property
    def rule(self):
        '''The full ``rule`` string for this :class:`Router`.

        It includes the :attr:`parent` portion of the rule if a :attr:`parent`
        router is available.
        '''
        return self.full_route.rule

    def path(self, **urlargs):
        '''The full path of this :class:`Router`.

        It includes the :attr:`parent` portion of url if a parent router
        is available.
        '''
        return self.full_route.url(**urlargs)

    def __getattr__(self, name):
        '''Check the value of a :attr:`parameters` ``name``.

        If the parameter is not available, retrieve the parameter from the
        :attr:`parent` :class:`Router` if it exists.
        '''
        if not name.startswith('_'):
            return self.get_parameter(name, False)
        self.no_param(name)

    def get_parameter(self, name, safe=True):
        value = self.parameters.get(name)
        if value is None:
            if self._parent:
                return self._parent.get_parameter(name, safe)
            elif name in self.parameters:
                return value
            elif not safe:
                self.no_param(name)
        else:
            return value

    def no_param(self, name):
        raise AttributeError("'%s' object has no attribute '%s'" %
                             (self.__class__.__name__, name))

    def content_type(self, request):
        '''Evaluate the content type for the response to a client ``request``.

        The method uses the :attr:`response_content_types` parameter of
        accepted content types and the content types accepted by the client
        and figure out the best match.
        '''
        return request.content_types.best_match(self.response_content_types)

    def accept_content_type(self, content_type):
        '''Check if ``content_type`` is accepted by this :class:`Router`.

        Return the best mach or ``None`` if not accepted.'''
        response_content_types = self.response_content_types
        if response_content_types:
            return ContentAccept(
                [(content_type, 1)]).best_match(response_content_types)

    def __repr__(self):
        return self.route.__repr__()

    def __call__(self, environ, start_response=None):
        path = environ.get('PATH_INFO') or '/'
        path = path[1:]
        router_args = self.resolve(path)
        if router_args:
            router, args = router_args
            return router.response(environ, args)
        elif self.allows_redirects:
            if self.route.is_leaf:
                if path.endswith('/'):
                    router_args = self.resolve(path[:-1])
                    if router_args is not None:
                        return self.redirect(environ, '/%s' % path[:-1])
            else:
                if not path.endswith('/'):
                    router_args = self.resolve('%s/' % path)
                    if router_args is not None:
                        return self.redirect(environ, '/%s/' % path)

    def resolve(self, path, urlargs=None):
        '''Resolve a path and return a ``(handler, urlargs)`` tuple or
        ``None`` if the path could not be resolved.
        '''
        match = self.route.match(path)
        if match is None:
            if not self.route.is_leaf:  # no match
                return
        elif '__remaining__' in match:
            path = match.pop('__remaining__')
            urlargs = update_args(urlargs, match)
        else:
            return self, update_args(urlargs, match)
        #
        for handler in self.routes:
            view_args = handler.resolve(path, urlargs)
            if view_args is None:
                continue
            return view_args

    def response(self, environ, args):
        '''Once the :meth:`resolve` method has matched the correct
        :class:`Router` for serving the request, this matched router invokes
        this method to produce the WSGI response.
        '''
        request = wsgi_request(environ, self, args)
        # Set the response content type
        request.response.content_type = self.content_type(request)
        method = request.method.lower()
        callable = getattr(self, method, None)
        if callable is None:
            raise HttpException(status=405,
                                msg='Method "%s" not allowed' % method)
        return callable(request)

    def redirect(self, environ, path):
        raise HttpRedirect(path)

    def add_child(self, router):
        '''Add a new :class:`Router` to the :attr:`routes` list.
        '''
        assert isinstance(router, Router), 'Not a valid Router'
        assert router is not self, 'cannot add self to children'
        # Loop over available routers to check it the router
        # is already available
        for r in self.routes:
            if r.route == router.route:
                r.parameters.update(router.parameters)
                return r
        if router.parent:
            router.parent.remove_child(router)
        router._parent = self
        self.routes.append(router)
        return router

    def remove_child(self, router):
        '''remove a :class:`Router` from the :attr:`routes` list.'''
        if router in self.routes:
            self.routes.remove(router)
            router._parent = None

    def get_route(self, name):
        '''Get a child :class:`Router` by its :attr:`name`.'''
        for route in self.routes:
            if route.name == name:
                return route

    def link(self, *args, **urlargs):
        '''Return an anchor :class:`Html` element with the `href` attribute
        set to the url of this :class:`Router`.'''
        if len(args) > 1:
            raise ValueError
        url = self.route.url(**urlargs)
        if len(args) == 1:
            text = args[0]
        else:
            text = url
        return Html('a', text, href=url)

    def sitemap(self, root=None):
        '''This utility method returns a sitemap starting at root.

        If *root* is ``None`` it starts from this :class:`Router`.

        :param request: a :ref:`wsgi request wrapper <app-wsgi-request>`
        :param root: Optional url path where to start the sitemap.
            By default it starts from this :class:`Router`. Pass `"/"` to
            start from the root :class:`Router`.
        :param levels: Number of nested levels to include.
        :return: A list of children
        '''
        if not root:
            root = self
        else:
            handler_urlargs = self.root.resolve(root[1:])
            if handler_urlargs:
                root, urlargs = handler_urlargs
            else:
                return []
        return list(self.routes)

    def encoding(self, request):
        '''The encoding to use for the response.

        By default it returns ``utf-8``.'''
        return 'utf-8'
Пример #3
0
class Content(Cacheable):
    '''A class for managing a file-based content
    '''
    template = None
    template_engine = None

    def __init__(self, app, content, metadata, path, src=None, **params):
        self._app = app
        self._content = content
        self._path = path
        self._src = src
        self._meta = AttributeDictionary(params)
        self._update_meta(metadata)
        if not self._meta.modified:
            if src:
                self._meta.modified = modified_datetime(src)
            else:
                self._meta.modified = datetime.now()
        self._meta.name = slugify(self._path, separator='_')

    @property
    def app(self):
        return self._app

    @property
    def content_type(self):
        return self._meta.content_type

    @property
    def is_text(self):
        return self._meta.content_type in CONTENT_EXTENSIONS

    @property
    def is_html(self):
        return is_html(self._meta.content_type)

    @property
    def suffix(self):
        return CONTENT_EXTENSIONS.get(self._meta.content_type)

    @property
    def path(self):
        return self._path

    @property
    def reldate(self):
        return self._meta.date or self._meta.modified

    @property
    def year(self):
        return self.reldate.year

    @property
    def month(self):
        return self.reldate.month

    @property
    def month2(self):
        return self.reldate.strftime('%m')

    @property
    def month3(self):
        return self.reldate.strftime('%b').lower()

    @property
    def id(self):
        if self.is_html:
            return '%s.json' % self._path

    def cache_key(self, app):
        return self._meta.name

    def __repr__(self):
        return self._path
    __str__ = __repr__

    def key(self, name=None):
        '''The key for a context dictionary
        '''
        name = name or self.name
        suffix = self.suffix
        return '%s_%s' % (suffix, name) if suffix else name

    def context(self, context=None):
        '''Extract the context dictionary for server side template rendering
        '''
        ctx = dict(self._flatten(self._meta))
        if context:
            ctx.update(context)
        return ctx

    def urlparams(self, names=None):
        urlparams = {}
        if names:
            for name in names:
                value = self._meta.get(name) or getattr(self, name, None)
                if value in (None, ''):
                    if name == 'id':
                        raise SkipBuild
                    elif names:
                        raise KeyError("%s could not obtain url variable '%s'"
                                       % (self, name))
                urlparams[name] = value
        return urlparams

    def render(self, context=None):
        '''Render the content
        '''
        if self.is_html:
            context = self.context(context)
            content = self._engine(self._content, context)
            if self.template:
                template = self._app.template_full_path(self.template)
                if template:
                    context[self.key('main')] = content
                    with open(template, 'r') as file:
                        template_str = file.read()
                    content = self._engine(template_str, context)
            return content
        else:
            return self._content

    def raw(self, request):
        return self._content

    @cached
    def json(self, request):
        '''Convert the content into a Json dictionary for the API
        '''
        if self.is_html:
            context = self._app.context(request)
            context = self.context(context)
            #
            data = self._to_json(request, self._meta)
            text = data.get(self.suffix) or {}
            data[self.suffix] = text
            text['main'] = self.render(context)
            #
            head = {}
            for key in HEAD_META:
                value = data.get(key)
                if value:
                    head[key] = value
            #
            if 'head' in data:
                head.update(data['head'])

            data['url'] = request.absolute_uri(self._path)
            data['head'] = head
            return data

    def html(self, request):
        '''Build the ``html_main`` key for this content and set
        content specific values to the ``head`` tag of the
        HTML5 document.
        '''
        if not self.is_html:
            raise Unsupported
        # The JSON data for this page
        data = self.json(request)
        doc = request.html_document
        doc.jscontext['page'] = dict(page_info(data))
        #
        image = absolute_uri(request, data.get('image'))
        doc.meta.update({'og:image': image,
                         'og:published_time': data.get('date'),
                         'og:modified_time': data.get('modified')})
        doc.meta.update(data['head'])
        #
        if not request.config.get('HTML5_NAVIGATION'):
            for css in data.get('require_css') or ():
                doc.head.links.append(css)
            doc.head.scripts.require.extend(data.get('require_js') or ())
        #
        if request.cache.uirouter is False:
            doc.head.scripts.require.extend(data.get('require_js') or ())

        self.on_html(doc)
        return data[self.suffix]['main']

    def on_html(self, doc):
        pass

    @classmethod
    def as_draft(cls):
        mp = tuple((a for a in cls.mandatory_properties
                    if a not in no_draft_field))
        return cls.__class__('Draft', (cls,), {'mandatory_properties': mp})

    # INTERNALS
    def _update_meta(self, metadata):
        meta = self._meta
        meta.site = {}
        for name in ('template_engine', 'template'):
            default = getattr(self, name)
            value = metadata.pop(name, default)
            meta.site[name] = value
            setattr(self, name, value)

        context = self.context(self.app.config)
        self._engine = self._app.template_engine(self.template_engine)
        meta.update(((key, self._render_meta(value, context))
                    for key, value in metadata.items()))

    def _flatten(self, meta):
        for key, value in mapping_iterator(meta):
            if isinstance(value, Mapping):
                for child, value in self._flatten(value):
                    yield '%s_%s' % (key, child), value
            else:
                yield key, self._to_string(value)

    def _to_string(self, value):
        if isinstance(value, Mapping):
            raise BuildError('A dictionary found when coverting to string')
        elif isinstance(value, (list, tuple)):
            return ', '.join(self._to_string(v) for v in value)
        elif isinstance(value, date):
            return iso8601(value)
        else:
            return to_string(value)

    def _render_meta(self, value, context):
        if isinstance(value, Mapping):
            return dict(((k, self._render_meta(v, context))
                         for k, v in value.items()))
        elif isinstance(value, (list, tuple)):
            return [self._render_meta(v, context) for v in value]
        elif isinstance(value, str):
            return self._engine(to_string(value), context)
        else:
            return value

    def _to_json(self, request, value):
        if isinstance(value, Mapping):
            return dict(((k, self._to_json(request, v))
                         for k, v in value.items()))
        elif isinstance(value, (list, tuple)):
            return [self._to_json(request, v) for v in value]
        elif isinstance(value, date):
            return iso8601(value)
        elif isinstance(value, URLWrapper):
            return value.to_json(request)
        else:
            return value
Пример #4
0
class Router(RouterType('RouterBase', (object, ), {})):
    '''A :ref:`WSGI middleware <wsgi-middleware>` to handle client requests
    on multiple :ref:`routes <apps-wsgi-route>`.

    The user must implement the HTTP methods
    required by the application. For example if the route needs to
    serve a ``GET`` request, the ``get(self, request)`` method must
    be implemented.

    :param rule: String used for creating the :attr:`route` of this
        :class:`Router`.
    :param routes: Optional :class:`Router` instances which are added to the
        children :attr:`routes` of this router.
    :param parameters: Optional parameters for this router.
        They are stored in the :attr:`parameters` attribute with the
        exception of :attr:`response_content_types` and
        :attr:`response_wrapper`

    .. attribute:: rule_methods

        A class attribute built during class creation. It is an ordered
        dictionary mapping method names with a five-elements tuple
        containing information
        about a child route (See the :class:`.route` decorator).

    .. attribute:: routes

        List of children :class:`Router` of this :class:`Router`.

    .. attribute:: parent

        The parent :class:`Router` of this :class:`Router`.

    .. attribute:: response_content_types

        A list/tuple of possible content types of a response to a
        client request.

        The client request must accept at least one of the response content
        types, otherwise an HTTP ``415`` exception occurs.

    .. attribute:: response_wrapper

        Optional function which wraps all handlers of this :class:`.Router`.
        The function must accept two parameters, the original handler
        and the :class:`.WsgiRequest`::

            def response_wrapper(handler, request):
                ...
                return handler(request)

    .. attribute:: parameters

        A :class:`.AttributeDictionary` of parameters for
        this :class:`Router`. Parameters are created at initialisation from
        the ``parameters`` class attribute and the key-valued parameters
        passed to the ``__init__`` method which are available in the
        class ``parameters`` attribute.
    '''
    _creation_count = 0
    _parent = None
    _name = None

    response_content_types = RouterParam(None)
    response_wrapper = RouterParam(None)

    def __init__(self, rule, *routes, **parameters):
        Router._creation_count += 1
        self._creation_count = Router._creation_count
        if not isinstance(rule, Route):
            rule = Route(rule)
        self._route = rule
        self._name = parameters.pop('name', rule.name)
        self.routes = []
        # add routes specified via the initialiser first
        for router in routes:
            self.add_child(router)
        # copy parameters
        self.parameters = AttributeDictionary(self.parameters)
        for name, rule_method in self.rule_methods.items():
            rule, method, params, _, _ = rule_method
            rparameters = params.copy()
            handler = getattr(self, name)
            router = self.add_child(self.make_router(rule, **rparameters))
            setattr(router, method, handler)
        for name, value in parameters.items():
            if name in self.parameters:
                self.parameters[name] = value
            else:
                setattr(self, slugify(name, separator='_'), value)

    @property
    def route(self):
        '''The relative :class:`.Route` served by this
        :class:`Router`.
        '''
        parent = self._parent
        if parent and parent._route.is_leaf:
            return parent.route + self._route
        else:
            return self._route

    @property
    def full_route(self):
        '''The full :attr:`route` for this :class:`.Router`.

        It includes the :attr:`parent` portion of the route if a parent
        router is available.
        '''
        if self._parent:
            return self._parent.full_route + self._route
        else:
            return self._route

    @property
    def name(self):
        '''The name of this :class:`Router`.

        This attribute can be specified during initialisation.
        If available, it can be used to retrieve a child router
        by name via the :meth:`get_route` method.
        '''
        return self._name

    @property
    def root(self):
        '''The root :class:`Router` for this :class:`Router`.'''
        if self.parent:
            return self.parent.root
        else:
            return self

    @property
    def parent(self):
        return self._parent

    @property
    def creation_count(self):
        '''Integer for sorting :class:`Router` by creation.

        Auto-generated during initialisation.'''
        return self._creation_count

    @property
    def rule(self):
        '''The full ``rule`` string for this :class:`Router`.

        It includes the :attr:`parent` portion of the rule if a :attr:`parent`
        router is available.
        '''
        return self.full_route.rule

    def path(self, **urlargs):
        '''The full path of this :class:`Router`.

        It includes the :attr:`parent` portion of url if a parent router
        is available.
        '''
        return self.full_route.url(**urlargs)

    def getparam(self, name, default=None):
        '''A parameter in this :class:`.Router`
        '''
        try:
            return getattr(self, name)
        except AttributeError:
            return default

    def __getattr__(self, name):
        '''Check the value of a :attr:`parameters` ``name``.

        If the parameter is not available, retrieve the parameter from the
        :attr:`parent` :class:`Router` if it exists.
        '''
        if not name.startswith('_'):
            return self._get_router_parameter(name, False)
        self._no_param(name)

    def content_type(self, request):
        '''Evaluate the content type for the response to a client ``request``.

        The method uses the :attr:`response_content_types` parameter of
        accepted content types and the content types accepted by the client
        ``request`` and figures out the best match.
        '''
        content_types = self.response_content_types
        ct = request.content_types.best_match(content_types)
        if ct and '*' in ct:
            ct = None
        if not ct and content_types:
            raise HttpException(status=415, msg=request.content_types)
        return ct

    def __repr__(self):
        return self.route.__repr__()

    def __call__(self, environ, start_response=None):
        path = environ.get('PATH_INFO') or '/'
        path = path[1:]
        router_args = self.resolve(path)
        if router_args:
            router, args = router_args
            return router.response(environ, args)

    def resolve(self, path, urlargs=None):
        '''Resolve a path and return a ``(handler, urlargs)`` tuple or
        ``None`` if the path could not be resolved.
        '''
        match = self.route.match(path)
        if match is None:
            if not self.route.is_leaf:  # no match
                return
        elif '__remaining__' in match:
            path = match.pop('__remaining__')
            urlargs = update_args(urlargs, match)
        else:
            return self, update_args(urlargs, match)
        #
        for handler in self.routes:
            view_args = handler.resolve(path, urlargs)
            if view_args is None:
                continue
            return view_args

    def response(self, environ, args):
        '''Once the :meth:`resolve` method has matched the correct
        :class:`Router` for serving the request, this matched router invokes
        this method to produce the WSGI response.
        '''
        request = wsgi_request(environ, self, args)
        request.response.content_type = self.content_type(request)
        method = request.method.lower()
        callable = getattr(self, method, None)
        if callable is None:
            raise HttpException(status=405)
        response_wrapper = self.response_wrapper
        if response_wrapper:
            return response_wrapper(callable, request)
        return callable(request)

    def add_child(self, router):
        '''Add a new :class:`Router` to the :attr:`routes` list.
        '''
        assert isinstance(router, Router), 'Not a valid Router'
        assert router is not self, 'cannot add self to children'
        #
        # Remove from previous parent
        if router.parent:
            router.parent.remove_child(router)
        router._parent = self
        # Loop over available routers to check it the router
        # is already available
        for r in self.routes:
            if r.route == router.route:
                r.parameters.update(router.parameters)
                return r
        self.routes.append(router)
        return router

    def remove_child(self, router):
        '''remove a :class:`Router` from the :attr:`routes` list.'''
        if router in self.routes:
            self.routes.remove(router)
            router._parent = None

    def get_route(self, name):
        '''Get a child :class:`Router` by its :attr:`name`.

        This method search child routes recursively.
        '''
        for route in self.routes:
            if route.name == name:
                return route
        for child in self.routes:
            route = child.get_route(name)
            if route:
                return route

    def link(self, *args, **urlargs):
        '''Return an anchor :class:`Html` element with the `href` attribute
        set to the url of this :class:`Router`.'''
        if len(args) > 1:
            raise ValueError
        url = self.route.url(**urlargs)
        if len(args) == 1:
            text = args[0]
        else:
            text = url
        return Html('a', text, href=url)

    def has_parent(self, router):
        '''Check if ``router`` is ``self`` or a parent or ``self``
        '''
        parent = self
        while parent and parent is not router:
            parent = parent._parent
        return parent is not None

    def make_router(self, rule, cls=None, **params):
        '''Create a new :class:`.Router` from a ``rule`` and parameters.

        This method is used during initialisation when building child
        Routers from the :attr:`rule_methods`.
        '''
        cls = cls or Router
        return cls(rule, **params)

    def _no_param(self, name):
        raise AttributeError("'%s' object has no attribute '%s'" %
                             (self.__class__.__name__, name))

    def _get_router_parameter(self, name, safe=True):
        value = self.parameters.get(name)
        if value is None:
            if self._parent:
                return self._parent._get_router_parameter(name, safe)
            elif name in self.parameters:
                return value
            elif not safe:
                self._no_param(name)
        else:
            return value
Пример #5
0
class Router(RouterType('RouterBase', (object, ), {})):
    '''A :ref:`WSGI middleware <wsgi-middleware>` to handle client requests
on multiple :ref:`routes <apps-wsgi-route>`.

The user must implement the HTTP methods
required by the application. For example if the route needs to serve a ``GET``
request, the ``get(self, request)`` method must be implemented.

:param rule: String used for creating the :attr:`route` of this
    :class:`Router`.
:param routes: Optional :class:`Router` instances which are added to the
    children :attr:`routes` of this router.
:param parameters: Optional parameters for this router. They are stored in the
    :attr:`parameters` attribute. If a ``response_content_types`` value is
    passed, it overrides the :attr:`response_content_types` attribute.

.. attribute:: route

    The :ref:`Route <apps-wsgi-route>` served by this :class:`Router`.

.. attribute:: routes

    List of children :class:`Router` of this :class:`Router`.

.. attribute:: parent

    The parent :class:`Router` of this :class:`Router`.

.. attribute:: response_content_types

    a list/tuple of possible content types of a response to a client request.

    The client request must accept at least one of the response content
    types, otherwise an HTTP ``415`` exception occurs.

.. attribute:: parameters

    A :class:`.AttributeDictionary` of parameters for
    this :class:`Router`. Parameters are created at initialisation from
    the ``parameters`` class attribute and the key-valued parameters
    passed to the ``__init__`` method for which the value is not callable.
'''
    _creation_count = 0
    _parent = None
    _name = None

    response_content_types = RouterParam(None)

    def __init__(self, rule, *routes, **parameters):
        Router._creation_count += 1
        self._creation_count = Router._creation_count
        if not isinstance(rule, Route):
            rule = Route(rule)
        self.route = rule
        self._name = parameters.pop('name', rule.rule)
        self.routes = []
        for router in routes:
            self.add_child(router)
        # copy parameters
        self.parameters = AttributeDictionary(self.parameters)
        for name, rule_method in self.rule_methods.items():
            rule, method, params, _, _ = rule_method
            rparameters = params.copy()
            handler = getattr(self, name)
            if rparameters.pop('async', False):  # asynchronous method
                handler = async ()(handler)
                handler.rule_method = rule_method
            router = self.add_child(Router(rule, **rparameters))
            setattr(router, method, handler)
        for name, value in parameters.items():
            if name in self.parameters:
                self.parameters[name] = value
            else:
                setattr(self, name, value)

    @property
    def name(self):
        '''The name of this :class:`Router`.

        This attribute can be specified during initialisation.
        If available, it can be used to retrieve a child router
        by name via the :meth:`get_route` method.
        '''
        return self._name

    @property
    def root(self):
        '''The root :class:`Router` for this :class:`Router`.'''
        if self.parent:
            return self.parent.root
        else:
            return self

    @property
    def parent(self):
        return self._parent

    @property
    def default_content_type(self):
        '''The default content type for responses. This is the first element
in the :attr:`response_content_types` list.'''
        ct = self.response_content_types
        return ct[0] if ct else None

    @property
    def creation_count(self):
        '''Integer for sorting :class:`Router` by creation.

        Auto-generated during initialisation.'''
        return self._creation_count

    @property
    def full_route(self):
        '''The full :attr:`route` for this :class:`Router`. It includes the
:attr:`parent` portion of the route if a parent router is available.'''
        route = self.route
        if self._parent:
            route = self._parent.route + route
        return route

    @property
    def rule(self):
        '''The full ``rule`` string for this :class:`Router`. It includes the
:attr:`parent` portion of rule if a parent router is available.'''
        return self.full_route.rule

    def path(self, **urlargs):
        '''The full path of this :class:`Router`. It includes the
:attr:`parent` portion of url if a parent router is available.'''
        route = self.route
        if self._parent:
            route = self._parent.route + route
        return route.url(**urlargs)

    def __getattr__(self, name):
        '''Check the value of a :attr:`parameters` ``name``.

        If the parameter is not available, retrieve the parameter from the
        :attr:`parent` :class:`Router` if it exists.
        '''
        if not name.startswith('_'):
            return self.get_parameter(name, False)
        self.no_param(name)

    def get_parameter(self, name, safe=True):
        value = self.parameters.get(name)
        if value is None:
            if self._parent:
                return self._parent.get_parameter(name, safe)
            elif name in self.parameters:
                return value
            elif not safe:
                self.no_param(name)
        else:
            return value

    def no_param(self, name):
        raise AttributeError("'%s' object has no attribute '%s'" %
                             (self.__class__.__name__, name))

    def content_type(self, request):
        '''Evaluate the content type for the response to a client ``request``.

        The method uses the :attr:`response_content_types` parameter of
        accepted content types and the content types accepted by the client
        and figure out the best match.
        '''
        response_content_types = self.response_content_types
        if response_content_types:
            return request.content_types.best_match(response_content_types)

    def accept_content_type(self, content_type):
        '''Check if ``content_type`` is accepted by this :class:`Router`.

        Return the best mach or ``None`` if not accepted.'''
        response_content_types = self.response_content_types
        if response_content_types:
            return ContentAccept([(content_type, 1)
                                  ]).best_match(response_content_types)

    def __repr__(self):
        return self.route.__repr__()

    def __call__(self, environ, start_response=None):
        path = environ.get('PATH_INFO') or '/'
        path = path[1:]
        router_args = self.resolve(path)
        if router_args:
            router, args = router_args
            return router.response(environ, args)
        else:
            if self.route.is_leaf:
                if path.endswith('/'):
                    router_args = self.resolve(path[:-1])
                    if router_args is not None:
                        return self.redirect(environ, '/%s' % path[:-1])
            else:
                if not path.endswith('/'):
                    router_args = self.resolve('%s/' % path)
                    if router_args is not None:
                        return self.redirect(environ, '/%s/' % path)

    def resolve(self, path, urlargs=None):
        '''Resolve a path and return a ``(handler, urlargs)`` tuple or
``None`` if the path could not be resolved.'''
        urlargs = urlargs if urlargs is not None else {}
        match = self.route.match(path)
        if match is None:
            return
        if '__remaining__' in match:
            remaining_path = match['__remaining__']
            for handler in self.routes:
                view_args = handler.resolve(remaining_path, urlargs)
                if view_args is None:
                    continue
                #remaining_path = match.pop('__remaining__','')
                #urlargs.update(match)
                return view_args
        else:
            return self, match

    @async (get_result=True)
    def response(self, environ, args):
        '''Once the :meth:`resolve` method has matched the correct
:class:`Router` for serving the request, this matched router invokes
this method to produce the WSGI response.'''
        request = wsgi_request(environ, self, args)
        # Set the response content type
        request.response.content_type = self.content_type(request)
        method = request.method.lower()
        callable = getattr(self, method, None)
        if callable is None:
            raise HttpException(status=405,
                                msg='Method "%s" not allowed' % method)
        # make sure cache does not contain asynchronous data
        async_cache = multi_async(request.cache, raise_on_error=False)
        cache = yield async_cache
        if async_cache.num_failures:
            for key, value in list(cache.items()):
                if isinstance(value, Failure):
                    cache.pop(key)
            environ['pulsar.cache'] = cache
            yield async_cache.failures
        else:
            environ['pulsar.cache'] = cache
            yield callable(request)

    @async (get_result=True)
    def redirect(self, environ, path):
        request = wsgi_request(environ, self)
        environ['pulsar.cache'] = yield multi_async(request.cache)
        raise HttpRedirect(path)

    def add_child(self, router):
        '''Add a new :class:`Router` to the :attr:`routes` list. If this
:class:`Router` is a leaf route, add a slash to the url.'''
        assert isinstance(router, Router), 'Not a valid Router'
        assert router is not self, 'cannot add self to children'
        if self.route.is_leaf:
            self.route = Route('%s/' % self.route.rule)
        for r in self.routes:
            if r.route == router.route:
                r.parameters.update(router.parameters)
                return r
        if router.parent:
            router.parent.remove_child(router)
        router._parent = self
        self.routes.append(router)
        return router

    def remove_child(self, router):
        '''remove a :class:`Router` from the :attr:`routes` list.'''
        if router in self.routes:
            self.routes.remove(router)
            router._parent = None

    def get_route(self, name):
        '''Get a child :class:`Router` by its :attr:`name`.'''
        for route in self.routes:
            if route.name == name:
                return route

    def link(self, *args, **urlargs):
        '''Return an anchor :class:`Html` element with the `href` attribute
set to the url of this :class:`Router`.'''
        if len(args) > 1:
            raise ValueError
        url = self.route.url(**urlargs)
        if len(args) == 1:
            text = args[0]
        else:
            text = url
        return Html('a', text, href=url)

    def sitemap(self, root=None):
        '''This utility method returns a sitemap starting at root.
If *root* is ``None`` it starts from this :class:`Router`.

:param request: a :ref:`wsgi request wrapper <app-wsgi-request>`
:param root: Optional url path where to start the sitemap.
    By default it starts from this :class:`Router`. Pass `"/"` to
    start from the root :class:`Router`.
:param levels: Number of nested levels to include.
:return: A list of children
'''
        if not root:
            root = self
        else:
            handler_urlargs = self.root.resolve(root[1:])
            if handler_urlargs:
                root, urlargs = handler_urlargs
            else:
                return []
        return list(self.routes)

    def encoding(self, request):
        '''The encoding to use for the response. By default it
returns ``utf-8``.'''
        return 'utf-8'
Пример #6
0
class Router(RouterType('RouterBase', (object,), {})):
    '''A :ref:`WSGI middleware <wsgi-middleware>` to handle client requests
    on multiple :ref:`routes <apps-wsgi-route>`.

    The user must implement the HTTP methods
    required by the application. For example if the route needs to
    serve a ``GET`` request, the ``get(self, request)`` method must
    be implemented.

    :param rule: String used for creating the :attr:`route` of this
        :class:`Router`.
    :param routes: Optional :class:`Router` instances which are added to the
        children :attr:`routes` of this router.
    :param parameters: Optional parameters for this router.
        They are stored in the :attr:`parameters` attribute with the
        exception of :attr:`response_content_types` and
        :attr:`response_wrapper`

    .. attribute:: rule_methods

        A class attribute built during class creation. It is an ordered
        dictionary mapping method names with a five-elements tuple
        containing information
        about a child route (See the :class:`.route` decorator).

    .. attribute:: routes

        List of children :class:`Router` of this :class:`Router`.

    .. attribute:: parent

        The parent :class:`Router` of this :class:`Router`.

    .. attribute:: response_content_types

        A list/tuple of possible content types of a response to a
        client request.

        The client request must accept at least one of the response content
        types, otherwise an HTTP ``415`` exception occurs.

    .. attribute:: response_wrapper

        Optional function which wraps all handlers of this :class:`.Router`.
        The function must accept two parameters, the original handler
        and the :class:`.WsgiRequest`::

            def response_wrapper(handler, request):
                ...
                return handler(request)

    .. attribute:: parameters

        A :class:`.AttributeDictionary` of parameters for
        this :class:`Router`. Parameters are created at initialisation from
        the ``parameters`` class attribute and the key-valued parameters
        passed to the ``__init__`` method which are available in the
        class ``parameters`` attribute.
    '''
    _creation_count = 0
    _parent = None
    _name = None

    response_content_types = RouterParam(None)
    response_wrapper = RouterParam(None)

    def __init__(self, rule, *routes, **parameters):
        Router._creation_count += 1
        self._creation_count = Router._creation_count
        if not isinstance(rule, Route):
            rule = Route(rule)
        self._route = rule
        self._name = parameters.pop('name', rule.name)
        self.routes = []
        # add routes specified via the initialiser first
        for router in routes:
            self.add_child(router)
        # copy parameters
        self.parameters = AttributeDictionary(self.parameters)
        for name, rule_method in self.rule_methods.items():
            rule, method, params, _, _ = rule_method
            rparameters = params.copy()
            handler = getattr(self, name)
            router = self.add_child(self.make_router(rule, **rparameters))
            setattr(router, method, handler)
        for name, value in parameters.items():
            if name in self.parameters:
                self.parameters[name] = value
            else:
                setattr(self, slugify(name, separator='_'), value)

    @property
    def route(self):
        '''The relative :class:`.Route` served by this
        :class:`Router`.
        '''
        parent = self._parent
        if parent and parent._route.is_leaf:
            return parent.route + self._route
        else:
            return self._route

    @property
    def full_route(self):
        '''The full :attr:`route` for this :class:`.Router`.

        It includes the :attr:`parent` portion of the route if a parent
        router is available.
        '''
        if self._parent:
            return self._parent.full_route + self._route
        else:
            return self._route

    @property
    def name(self):
        '''The name of this :class:`Router`.

        This attribute can be specified during initialisation.
        If available, it can be used to retrieve a child router
        by name via the :meth:`get_route` method.
        '''
        return self._name

    @property
    def root(self):
        '''The root :class:`Router` for this :class:`Router`.'''
        if self.parent:
            return self.parent.root
        else:
            return self

    @property
    def parent(self):
        return self._parent

    @property
    def creation_count(self):
        '''Integer for sorting :class:`Router` by creation.

        Auto-generated during initialisation.'''
        return self._creation_count

    @property
    def rule(self):
        '''The full ``rule`` string for this :class:`Router`.

        It includes the :attr:`parent` portion of the rule if a :attr:`parent`
        router is available.
        '''
        return self.full_route.rule

    def path(self, **urlargs):
        '''The full path of this :class:`Router`.

        It includes the :attr:`parent` portion of url if a parent router
        is available.
        '''
        return self.full_route.url(**urlargs)

    def getparam(self, name, default=None):
        '''A parameter in this :class:`.Router`
        '''
        try:
            return getattr(self, name)
        except AttributeError:
            return default

    def __getattr__(self, name):
        '''Check the value of a :attr:`parameters` ``name``.

        If the parameter is not available, retrieve the parameter from the
        :attr:`parent` :class:`Router` if it exists.
        '''
        if not name.startswith('_'):
            return self._get_router_parameter(name, False)
        self._no_param(name)

    def content_type(self, request):
        '''Evaluate the content type for the response to a client ``request``.

        The method uses the :attr:`response_content_types` parameter of
        accepted content types and the content types accepted by the client
        ``request`` and figures out the best match.
        '''
        response_content_types = self.response_content_types
        request_content_types = request.content_types
        if request_content_types:
            ct = request_content_types.best_match(response_content_types)
            if ct and '*' in ct:
                ct = None
            if not ct and response_content_types:
                raise HttpException(status=415, msg=request_content_types)
            return ct

    def __repr__(self):
        return self.route.__repr__()

    def __call__(self, environ, start_response=None):
        path = environ.get('PATH_INFO') or '/'
        path = path[1:]
        router_args = self.resolve(path)
        if router_args:
            router, args = router_args
            return router.response(environ, args)

    def resolve(self, path, urlargs=None):
        '''Resolve a path and return a ``(handler, urlargs)`` tuple or
        ``None`` if the path could not be resolved.
        '''
        match = self.route.match(path)
        if match is None:
            if not self.route.is_leaf:  # no match
                return
        elif '__remaining__' in match:
            path = match.pop('__remaining__')
            urlargs = update_args(urlargs, match)
        else:
            return self, update_args(urlargs, match)
        #
        for handler in self.routes:
            view_args = handler.resolve(path, urlargs)
            if view_args is None:
                continue
            return view_args

    def response(self, environ, args):
        '''Once the :meth:`resolve` method has matched the correct
        :class:`Router` for serving the request, this matched router invokes
        this method to produce the WSGI response.
        '''
        request = wsgi_request(environ, self, args)
        request.response.content_type = self.content_type(request)
        method = request.method.lower()
        callable = getattr(self, method, None)
        if callable is None:
            raise HttpException(status=405)
        response_wrapper = self.response_wrapper
        if response_wrapper:
            return response_wrapper(callable, request)
        return callable(request)

    def add_child(self, router):
        '''Add a new :class:`Router` to the :attr:`routes` list.
        '''
        assert isinstance(router, Router), 'Not a valid Router'
        assert router is not self, 'cannot add self to children'
        #
        # Remove from previous parent
        if router.parent:
            router.parent.remove_child(router)
        router._parent = self
        # Loop over available routers to check it the router
        # is already available
        for r in self.routes:
            if r.route == router.route:
                r.parameters.update(router.parameters)
                return r
        self.routes.append(router)
        return router

    def remove_child(self, router):
        '''remove a :class:`Router` from the :attr:`routes` list.'''
        if router in self.routes:
            self.routes.remove(router)
            router._parent = None

    def get_route(self, name):
        '''Get a child :class:`Router` by its :attr:`name`.

        This method search child routes recursively.
        '''
        for route in self.routes:
            if route.name == name:
                return route
        for child in self.routes:
            route = child.get_route(name)
            if route:
                return route

    def link(self, *args, **urlargs):
        '''Return an anchor :class:`Html` element with the `href` attribute
        set to the url of this :class:`Router`.'''
        if len(args) > 1:
            raise ValueError
        url = self.route.url(**urlargs)
        if len(args) == 1:
            text = args[0]
        else:
            text = url
        return Html('a', text, href=url)

    def has_parent(self, router):
        '''Check if ``router`` is ``self`` or a parent or ``self``
        '''
        parent = self
        while parent and parent is not router:
            parent = parent._parent
        return parent is not None

    def make_router(self, rule, cls=None, **params):
        '''Create a new :class:`.Router` from a ``rule`` and parameters.

        This method is used during initialisation when building child
        Routers from the :attr:`rule_methods`.
        '''
        cls = cls or Router
        return cls(rule, **params)

    def _no_param(self, name):
        raise AttributeError("'%s' object has no attribute '%s'" %
                             (self.__class__.__name__, name))

    def _get_router_parameter(self, name, safe=True):
        value = self.parameters.get(name)
        if value is None:
            if self._parent:
                return self._parent._get_router_parameter(name, safe)
            elif name in self.parameters:
                return value
            elif not safe:
                self._no_param(name)
        else:
            return value
Пример #7
0
class Content(Cacheable):
    '''A class for managing a file-based content
    '''
    template = None
    template_engine = None

    def __init__(self, app, content, metadata, path, src=None, **params):
        self._app = app
        self._content = content
        self._path = path
        self._src = src
        self._meta = AttributeDictionary(params)
        self._update_meta(metadata)
        if not self._meta.modified:
            if src:
                self._meta.modified = modified_datetime(src)
            else:
                self._meta.modified = datetime.now()
        self._meta.name = slugify(self._path, separator='_')

    @property
    def app(self):
        return self._app

    @property
    def content_type(self):
        return self._meta.content_type

    @property
    def is_text(self):
        return self._meta.content_type in CONTENT_EXTENSIONS

    @property
    def is_html(self):
        return is_html(self._meta.content_type)

    @property
    def suffix(self):
        return CONTENT_EXTENSIONS.get(self._meta.content_type)

    @property
    def path(self):
        return self._path

    @property
    def reldate(self):
        return self._meta.date or self._meta.modified

    @property
    def year(self):
        return self.reldate.year

    @property
    def month(self):
        return self.reldate.month

    @property
    def month2(self):
        return self.reldate.strftime('%m')

    @property
    def month3(self):
        return self.reldate.strftime('%b').lower()

    @property
    def id(self):
        if self.is_html:
            return '%s.json' % self._path

    def cache_key(self, app):
        return self._meta.name

    def __repr__(self):
        return self._path

    __str__ = __repr__

    def key(self, name=None):
        '''The key for a context dictionary
        '''
        name = name or self.name
        suffix = self.suffix
        return '%s_%s' % (suffix, name) if suffix else name

    def context(self, context=None):
        '''Extract the context dictionary for server side template rendering
        '''
        ctx = dict(self._flatten(self._meta))
        if context:
            ctx.update(context)
        return ctx

    def urlparams(self, names=None):
        urlparams = {}
        if names:
            for name in names:
                value = self._meta.get(name) or getattr(self, name, None)
                if value in (None, ''):
                    if name == 'id':
                        raise SkipBuild
                    elif names:
                        raise KeyError(
                            "%s could not obtain url variable '%s'" %
                            (self, name))
                urlparams[name] = value
        return urlparams

    def render(self, context=None):
        '''Render the content
        '''
        if self.is_html:
            context = self.context(context)
            content = self._engine(self._content, context)
            if self.template:
                template = self._app.template_full_path(self.template)
                if template:
                    context[self.key('main')] = content
                    with open(template, 'r') as file:
                        template_str = file.read()
                    content = self._engine(template_str, context)
            return content
        else:
            return self._content

    def raw(self, request):
        return self._content

    @cached
    def json(self, request):
        '''Convert the content into a Json dictionary for the API
        '''
        if self.is_html:
            context = self._app.context(request)
            context = self.context(context)
            #
            data = self._to_json(request, self._meta)
            text = data.get(self.suffix) or {}
            data[self.suffix] = text
            text['main'] = self.render(context)
            #
            head = {}
            for key in HEAD_META:
                value = data.get(key)
                if value:
                    head[key] = value
            #
            if 'head' in data:
                head.update(data['head'])

            data['url'] = request.absolute_uri(self._path)
            data['head'] = head
            return data

    def html(self, request):
        '''Build the ``html_main`` key for this content and set
        content specific values to the ``head`` tag of the
        HTML5 document.
        '''
        if not self.is_html:
            raise Unsupported
        # The JSON data for this page
        data = self.json(request)
        doc = request.html_document
        doc.jscontext['page'] = dict(page_info(data))
        #
        image = absolute_uri(request, data.get('image'))
        doc.meta.update({
            'og:image': image,
            'og:published_time': data.get('date'),
            'og:modified_time': data.get('modified')
        })
        doc.meta.update(data['head'])
        #
        if not request.config.get('HTML5_NAVIGATION'):
            for css in data.get('require_css') or ():
                doc.head.links.append(css)
            doc.head.scripts.require.extend(data.get('require_js') or ())
        #
        if request.cache.uirouter is False:
            doc.head.scripts.require.extend(data.get('require_js') or ())

        self.on_html(doc)
        return data[self.suffix]['main']

    def on_html(self, doc):
        pass

    @classmethod
    def as_draft(cls):
        mp = tuple(
            (a for a in cls.mandatory_properties if a not in no_draft_field))
        return cls.__class__('Draft', (cls, ), {'mandatory_properties': mp})

    # INTERNALS
    def _update_meta(self, metadata):
        meta = self._meta
        meta.site = {}
        for name in ('template_engine', 'template'):
            default = getattr(self, name)
            value = metadata.pop(name, default)
            meta.site[name] = value
            setattr(self, name, value)

        context = self.context(self.app.config)
        self._engine = self._app.template_engine(self.template_engine)
        meta.update(((key, self._render_meta(value, context))
                     for key, value in metadata.items()))

    def _flatten(self, meta):
        for key, value in mapping_iterator(meta):
            if isinstance(value, Mapping):
                for child, value in self._flatten(value):
                    yield '%s_%s' % (key, child), value
            else:
                yield key, self._to_string(value)

    def _to_string(self, value):
        if isinstance(value, Mapping):
            raise BuildError('A dictionary found when coverting to string')
        elif isinstance(value, (list, tuple)):
            return ', '.join(self._to_string(v) for v in value)
        elif isinstance(value, date):
            return iso8601(value)
        else:
            return to_string(value)

    def _render_meta(self, value, context):
        if isinstance(value, Mapping):
            return dict(
                ((k, self._render_meta(v, context)) for k, v in value.items()))
        elif isinstance(value, (list, tuple)):
            return [self._render_meta(v, context) for v in value]
        elif isinstance(value, str):
            return self._engine(to_string(value), context)
        else:
            return value

    def _to_json(self, request, value):
        if isinstance(value, Mapping):
            return dict(
                ((k, self._to_json(request, v)) for k, v in value.items()))
        elif isinstance(value, (list, tuple)):
            return [self._to_json(request, v) for v in value]
        elif isinstance(value, date):
            return iso8601(value)
        elif isinstance(value, URLWrapper):
            return value.to_json(request)
        else:
            return value