manhattan's publishing process

19th January 2018

In this video we take a look at manhattan's publishing process and how Python's context syntax is used to manage draft and published documents.

The goal with our approach is to make the task of implementing a publishing processes against a database collection (table) simple and ensure that integration with the rest of the application's environment (e.g the CMS, public/frontend views, other database models, etc.) is seamless.

I've posted the diagram and code snippets from  the video (as I'm aware these may appear blurry in the video).

Using contexts to switch between draft and published collections

Below is a diagram from the video which explains how the collection (table) is switched by the context when a database query is executed.

Defining a publishable frame (model)

To support for draft and published context's all that's needed is to inherit from the PublishableFrame class.

from manhattan.publishing import PublishableFrame, PublishingError


class Entry(PublishableFrame):
    """
    An entry in the code journal.
    """

    _fields = {
        'visible',
        'publish_date_str',
        'title',
        'slug',
        'meta_desc',
        'flows',
        'comment_count'
    }

    _unpublished_fields = PublishableFrame._unpublished_fields | {
        'comment_count'
    }

    ...

    def validate_publish(self):
        # Ensure the page's slug is unique within the published context
        with Entry._context_manager.published():
            query = And(
                Q.publish_date_str == self.publish_date_str,
                Q._id != self._id
            )
            if Entry.count(query):
                raise PublishingError('The page slug is not unique')

Creating fake data for publishable frames

I don't think fixtures have come up in previous videos but manhattan (along with mongoframes) makes it simple to define fixture factory blueprints (say that 10 times fast) with which copious amounts of fake data can be generated.

For PublishableFrames a different fixture blueprint is required.

"""
Fixtures for entries.
"""

from manhattan.formatters.text import slugify
from manhattan.publishing.factory import Blueprint
from mongoframes.factory import makers, quotas
from mongoframes.factory.makers import dates, selections, text

from blueprints.entries.models import Entry

__all__ = ['EntryFixture']


class EntryFixture(Blueprint):

    _frame_cls = Entry
    _meta_fields = {'publish_date'}

    # Pre
    _instructions = {
        'title': text.Markov('default', 'sentence', quotas.Random(5, 15))
    }

    # Post
    visible = selections.OneOf(
        [makers.Static(True), makers.Static(False)],
        [0.95, 0.05]
    )
    publish_date = makers.Unique(dates.DateBetween('today-70', 'today+5'))
    slug = makers.Lambda(
        lambda d, v: slugify(d['title']),
        assembler=False,
        finisher=True
    )
    meta_desc = text.Markov('default', 'sentence', quotas.Random(15, 30))

Searching for entries in the published context

The following code implements the search on this website, as it's a public view it should only select entries that have been published and we search within the published context.

@blueprint.route('/search')
def search():
    """Search the entries"""

    # Get the query string
    q = request.args.get('q', '').strip()

    # Find matching enquiries
    entries = []
    if q:
        with Entry._context_manager.published():
            entries = Entry.many(
                And(
                    Q.visible == True,
                    Q.publish_date_str <= today_tz().strftime('%Y-%m-%d'),
                    {'$text': {'$search': q}}
                ),
                projection={
                    'publish_date_str': True,
                    'slug': True,
                    'title': True
                }
            )

    return render_template('entries/search.html', entries=entries, q=q)

Switching the context based on whether a user is logged or not

When the public view also provides the view in which a user will edit the document we need to switch the context between published for non-users and draft for users.

In the video view_page was used as an example which allows the about me page to be edited when I'm signed-in.

@blueprint.route('/')
@blueprint.route('/<path:slug>')
def view_page(slug=None):
    """View a page"""

    # Index is a reserved slug name and therefore cannot be specified
    # directly.
    if slug == 'index':
        abort(404)

    # If there's no slug look for the reserved 'index' page
    if slug == None:
        slug = 'index'

    # Switch the context to draft if being viewed by a signed in user
    context = Page._context_manager.published
    if User.from_session():
        context = Page._context_manager.draft

    with context():

        # Attempt to find the page based on the slug
        page = Page.one(Q.slug == slug)
        if not page:
            abort(404, 'Page not found')

        return render_template('pages/' + page.template, page_doc=page)