Skip to content

Commit

Permalink
Do better providing a meaningful context.
Browse files Browse the repository at this point in the history
  • Loading branch information
jamadden committed Oct 12, 2017
1 parent e81fb10 commit cbb1d16
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 16 deletions.
51 changes: 42 additions & 9 deletions docs/using_zca.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@
Leveraging the Component Architecture
=======================================

.. todo:: Write ZCA section

Background
==========

Expand All @@ -15,14 +13,49 @@ If you're not familiar with component architectures, and
Theory of Operation
===================

Talk about component lookup: context, request/layer, view and how we
can register macros and viewlets for each of those things.
Three of the `the standard names
<https://docs.zope.org/zope2/zope2book/AppendixC.html#built-in-names>`_
available in templates play a particularly important part in the way
templates, macros and viewlets are used.

Those names are ``context``, ``request`` and ``view``. These names are
inherited from the Zope family of Web frameworks, but they have equal
relevance here.

The context is the object being rendered. It is most commonly a Nikola
post (represented by :class:`~nti.nikola_chameleon.interfaces.IPost`),
but it can also be a
:class:`~nti.nikola_chameleon.interfaces.IPostList` on index pages, or
:class:`~nti.nikola_chameleon.interfaces.IGallery`,
:class:`~nti.nikola_chameleon.interfaces.IListing`
or :class:`~nti.nikola_chameleon.interfaces.ISlide` on their
respective pages.

.. tip:: The `template variables
<https://getnikola.com/template-variables.html>`_ for
specific template pages are available from the ``context``
object, in addition to being available from the ``options``
dictionary. Using ``context`` can result templates that are
more clear in their intent.

.. todo:: Rethink how we have these separated out. Maybe more should
go on the view and less on the request?

The request (or layer) expresses the intent of the render. It tells us
the purpose of our rendering. It will always provide the
:class:`~nti.nikola_chameleon.interfaces.IPageKind` interface (it will
actually always provide one of the more specific interfaces that
better identifies what we're trying to render, such as
:class:`~nti.nikola_chameleon.interfaces.ITagsPageKind`).

.. todo:: Talk about component lookup: context, request/layer, view and how we
can register macros and viewlets for each of those things.

Talk about what each represents (object in use, details about what
we're asking for, how we're handling the asking).
.. todo:: Talk about what each represents (object in use, details about what
we're asking for, how we're handling the asking).

Talk about layers applied to the "request".
.. todo:: Talk about layers applied to the "request".

Talk about the different page kinds.
.. todo:: Talk about the different page kinds.

Talk about comment systems.
.. todo:: Talk about comment systems.
38 changes: 38 additions & 0 deletions src/nti/nikola_chameleon/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from nikola.post import Post
from zope import interface
from zope.interface.common.sequence import IReadSequence
from zope.viewlet.interfaces import IViewletManager

class IPost(interface.Interface):
Expand All @@ -30,6 +31,43 @@ class IPost(interface.Interface):

interface.classImplements(Post, IPost)

class IPostList(IReadSequence):
"""
A list of posts.
"""

class IListing(interface.Interface):
"""
A listing object.
"""

code = interface.Attribute("The formatted HTML code.")
crumbs = interface.Attribute("Breadcrum list")
folders = interface.Attribute("List of folders")
files = interface.Attribute("List of files")
source_link = interface.Attribute("Link to the source file")

class IGallery(interface.Interface):
"""
A gallery object.
"""

crumbs = interface.Attribute("Breadcrumbs")
enable_comments = interface.Attribute("Whether comments are enabled.")
folders = interface.Attribute("List of folders (path, title)")
permalink = interface.Attribute("Permalink")
photo_array = interface.Attribute("List Photo array (contains dicts with image data: url, url_thumb, title, size{w, h}) ")
post = interface.Attribute("Optionally the post for this gallery")
thumbnail_size = interface.Attribute("THUMBNAIL_SIZE setting")

class ISlide(interface.Interface):
"""
A slide page.
"""

carousel_id = interface.Attribute("The string id")
slides_content = interface.Attribute("A list of strings of image paths")

class IPageKind(interface.Interface):
"""
The type of page being rendered.
Expand Down
62 changes: 55 additions & 7 deletions src/nti/nikola_chameleon/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@

from zope.configuration import xmlconfig
from zope.dottedname import resolve as dottedname
from zope.proxy.decorator import SpecificationDecoratorBase

import nti.nikola_chameleon
from nti.nikola_chameleon import interfaces
Expand All @@ -40,12 +41,42 @@

logger = __import__('logging').getLogger(__name__)

class Context(object):
class _Context(object):
"""
Instances of this object will be the context
of a template when no post is available.
"""

@interface.implementer(interfaces.IPostList)
class _PostListContext(SpecificationDecoratorBase):
"""
A list of posts as the context.
"""

class _OptionsProxy(object):

_properties = ()

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

def __getattr__(self, name):
if name not in self._properties:
raise AttributeError(name)
return self.options[name]

@interface.implementer(interfaces.IListing)
class _ListingContext(_OptionsProxy):
_properties = tuple(interfaces.IListing.names())

@interface.implementer(interfaces.IGallery)
class _GalleryContext(_OptionsProxy):
_properties = tuple(interfaces.IGallery.names())

@interface.implementer(interfaces.ISlide)
class _SlideContext(_OptionsProxy):
_properties = tuple(interfaces.ISlide.names())

def getViewTemplate(name, view, request, context):
template = component.queryMultiAdapter(
(view, request, context),
Expand Down Expand Up @@ -169,13 +200,30 @@ def render_template_to_string(self, template, context):
context = options.get('post')

if context is not None:
# We only render these things once in a given run,
# so we don't bother stripping the interfaces off of it
# or using a proxy.
assert not interfaces.IPageKind.providedBy(context)
interface.alsoProvides(context, interfaces.IPostPage)
if template == 'gallery.tmpl':
# Some galleries can have posts
context = _GalleryContext(options)
else:
# We only render these things once in a given run,
# so we don't bother stripping the interfaces off of it
# or using a proxy.
assert not interfaces.IPageKind.providedBy(context)
interface.alsoProvides(context, interfaces.IPostPage)
# XXX: Need to look at the post's `type` and add that to the
# post https://getnikola.com/handbook.html#post-types
elif 'posts' in options:
context = _PostListContext(options['posts'])
elif 'code' in options and template == 'listing.tmpl':
context = _ListingContext(options)
elif template == 'gallery.tmpl':
context = _GalleryContext(options)
elif template == 'slides.tmpl':
context = _SlideContext(options)
else:
context = Context()
# We shouldn't get here.
logger.warn("Unknown context type for template %r and options %r",
template, list(options))
context = _Context()

request = Request(context, options)
# apply the "layer" to the request
Expand Down

0 comments on commit cbb1d16

Please sign in to comment.