diff content/Coding/016-django-fixed-pages.rst @ 4:7ce6393e6d30

Adding converted blog posts from old blog.
author Brian Neal <bgneal@gmail.com>
date Thu, 30 Jan 2014 21:45:03 -0600
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/content/Coding/016-django-fixed-pages.rst	Thu Jan 30 21:45:03 2014 -0600
@@ -0,0 +1,172 @@
+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/