view content/Coding/016-django-fixed-pages.rst @ 8:e29fd75628d6

Turn on disqus comments.
author Brian Neal <bgneal@gmail.com>
date Sat, 01 Feb 2014 13:58:51 -0600
parents 7ce6393e6d30
children
line wrap: on
line source
Fixed pages for Django
######################

:date: 2012-05-13 13:30
:tags: Django
:slug: fixed-pages-for-django
:author: Brian Neal

I have been using Django's `flatpages app`_ for some simple, static pages that
were supposed to be temporary. I slapped a Javascript editor on the admin page
and it has worked very well. However some of the pages have long outlived their
"temporary" status, and I find myself needing to update them. It is then that I
get angry at the Javascript editor, and there is no way to keep any kind of
history on the page without having to fish through old database backups. I
started to think it would be nice to write the content in a nice markup
language, for example reStructuredText_, which I could then commit to version
control. I would just need a way to generate HTML from the source text to
produce the flatpage content.

Of course I could use the template filters in `django.contrib.markup`_. But
turning markup into HTML at page request time can be more expensive than I like.
Yes, I could cache the page, but I'd like the process to be more explicit.

In my first attempt at doing this, I wrote a `custom management command`_ that
used a dictionary in my ``settings.py`` file to map reStructuredText files to
flatpage URLs. My management command would open the input file, convert it to
HTML, then find the ``FlatPage`` object associated with the URL. It would then
update the object with the new HTML content and save it.

This worked okay, but in the end I decided that the pages I wanted to update
were not temporary, quick & dirty pages, which is kind of how I view flatpages.
So I decided to stop leaning on the flatpages app for these pages.

I then modified the management command to read a given input file, convert it
to an HTML fragment, then save it in my templates directory. Thus, a file stored
in my project directory as ``fixed/about.rst`` would get transformed to
``templates/fixed/about.html``. Here is the source to the command which I saved
as ``make_fixed_page.py``:

.. sourcecode:: python

   import os.path
   import glob

   import docutils.core
   from django.core.management.base import LabelCommand, CommandError
   from django.conf import settings


   class Command(LabelCommand):
       help = "Generate HTML from restructured text files"
       args = "<inputfile1> <inputfile2> ... | all"

       def handle_label(self, filename, **kwargs):
           """Process input file(s)"""

           if not hasattr(settings, 'PROJECT_PATH'):
               raise CommandError("Please add a PROJECT_PATH setting")

           self.src_dir = os.path.join(settings.PROJECT_PATH, 'fixed')
           self.dst_dir = os.path.join(settings.PROJECT_PATH, 'templates', 'fixed')

           if filename == 'all':
               files = glob.glob("%s%s*.rst" % (self.src_dir, os.path.sep))
               files = [os.path.basename(f) for f in files]
           else:
               files = [filename]

           for f in files:
               self.process_page(f)

       def process_page(self, filename):
           """Processes one fixed page"""

           # retrieve source text
           src_path = os.path.join(self.src_dir, filename)
           try:
               with open(src_path, 'r') as f:
                   src_text = f.read()
           except IOError, ex:
               raise CommandError(str(ex))

           # transform text
           content = self.transform_input(src_text)

           # write output
           basename = os.path.splitext(os.path.basename(filename))[0]
           dst_path = os.path.join(self.dst_dir, '%s.html' % basename)

           try:
               with open(dst_path, 'w') as f:
                   f.write(content.encode('utf-8'))
           except IOError, ex:
               raise CommandError(str(ex))

           prefix = os.path.commonprefix([src_path, dst_path])
           self.stdout.write("%s -> %s\n" % (filename, dst_path[len(prefix):]))

       def transform_input(self, src_text):
           """Transforms input restructured text to HTML"""

           return docutils.core.publish_parts(src_text, writer_name='html',
                   settings_overrides={
                       'doctitle_xform': False,
                       'initial_header_level': 2,
                       })['html_body']

Next I would need a template that could render these fragments. I remembered
that the Django `include tag`_ could take a variable as an argument. Thus I
could create a single template that could render all of these "fixed" pages.
Here is the template ``templates/fixed/base.html``::

   {% extends 'base.html' %}
   {% block title %}{{ title }}{% endblock %}
   {% block content %}
   {% include content_template %}
   {% endblock %}

I just need to pass in ``title`` and ``content_template`` context variables. The
latter will control which HTML fragment I include.

I then turned to the view function which would render this template. I wanted to
make this as generic and easy to do as possible. Since I was abandoning
flatpages, I would need to wire these up in my ``urls.py``. At first I didn't
think I could use Django's new `class-based generic views`_ for this, but after
some fiddling around, I came up with a very nice solution:

.. sourcecode:: python

   from django.views.generic import TemplateView

   class FixedView(TemplateView):
       """
       For displaying our "fixed" views generated with the custom command
       make_fixed_page.

       """
       template_name = 'fixed/base.html'
       title = ''
       content_template = ''

       def get_context_data(self, **kwargs):
           context = super(FixedView, self).get_context_data(**kwargs)
           context['title'] = self.title
           context['content_template'] = self.content_template
           return context

This allowed me to do the following in my ``urls.py`` file:

.. sourcecode:: python

   urlpatterns = patterns('',
      # ...

      url(r'^about/$',
          FixedView.as_view(title='About', content_template='fixed/about.html'),
          name='about'),
      url(r'^colophon/$',
          FixedView.as_view(title='Colophon', content_template='fixed/colophon.html'),
          name='colophon'),

      # ...

Now I have a way to efficiently serve reStructuredText files as "fixed pages"
that I can put under source code control.

.. _flatpages app: https://docs.djangoproject.com/en/1.4/ref/contrib/flatpages/
.. _reStructuredText: http://docutils.sourceforge.net/rst.html
.. _custom management command: https://docs.djangoproject.com/en/1.4/howto/custom-management-commands/
.. _include tag: https://docs.djangoproject.com/en/1.4/ref/templates/builtins/#include
.. _class-based generic views: https://docs.djangoproject.com/en/1.4/topics/class-based-views/
.. _django.contrib.markup: https://docs.djangoproject.com/en/1.4/ref/contrib/markup/