bgneal@4: Fixed pages for Django bgneal@4: ###################### bgneal@4: bgneal@4: :date: 2012-05-13 13:30 bgneal@4: :tags: Django bgneal@4: :slug: fixed-pages-for-django bgneal@4: :author: Brian Neal bgneal@4: bgneal@4: I have been using Django's `flatpages app`_ for some simple, static pages that bgneal@4: were supposed to be temporary. I slapped a Javascript editor on the admin page bgneal@4: and it has worked very well. However some of the pages have long outlived their bgneal@4: "temporary" status, and I find myself needing to update them. It is then that I bgneal@4: get angry at the Javascript editor, and there is no way to keep any kind of bgneal@4: history on the page without having to fish through old database backups. I bgneal@4: started to think it would be nice to write the content in a nice markup bgneal@4: language, for example reStructuredText_, which I could then commit to version bgneal@4: control. I would just need a way to generate HTML from the source text to bgneal@4: produce the flatpage content. bgneal@4: bgneal@4: Of course I could use the template filters in `django.contrib.markup`_. But bgneal@4: turning markup into HTML at page request time can be more expensive than I like. bgneal@4: Yes, I could cache the page, but I'd like the process to be more explicit. bgneal@4: bgneal@4: In my first attempt at doing this, I wrote a `custom management command`_ that bgneal@4: used a dictionary in my ``settings.py`` file to map reStructuredText files to bgneal@4: flatpage URLs. My management command would open the input file, convert it to bgneal@4: HTML, then find the ``FlatPage`` object associated with the URL. It would then bgneal@4: update the object with the new HTML content and save it. bgneal@4: bgneal@4: This worked okay, but in the end I decided that the pages I wanted to update bgneal@4: were not temporary, quick & dirty pages, which is kind of how I view flatpages. bgneal@4: So I decided to stop leaning on the flatpages app for these pages. bgneal@4: bgneal@4: I then modified the management command to read a given input file, convert it bgneal@4: to an HTML fragment, then save it in my templates directory. Thus, a file stored bgneal@4: in my project directory as ``fixed/about.rst`` would get transformed to bgneal@4: ``templates/fixed/about.html``. Here is the source to the command which I saved bgneal@4: as ``make_fixed_page.py``: bgneal@4: bgneal@4: .. sourcecode:: python bgneal@4: bgneal@4: import os.path bgneal@4: import glob bgneal@4: bgneal@4: import docutils.core bgneal@4: from django.core.management.base import LabelCommand, CommandError bgneal@4: from django.conf import settings bgneal@4: bgneal@4: bgneal@4: class Command(LabelCommand): bgneal@4: help = "Generate HTML from restructured text files" bgneal@4: args = " ... | all" bgneal@4: bgneal@4: def handle_label(self, filename, **kwargs): bgneal@4: """Process input file(s)""" bgneal@4: bgneal@4: if not hasattr(settings, 'PROJECT_PATH'): bgneal@4: raise CommandError("Please add a PROJECT_PATH setting") bgneal@4: bgneal@4: self.src_dir = os.path.join(settings.PROJECT_PATH, 'fixed') bgneal@4: self.dst_dir = os.path.join(settings.PROJECT_PATH, 'templates', 'fixed') bgneal@4: bgneal@4: if filename == 'all': bgneal@4: files = glob.glob("%s%s*.rst" % (self.src_dir, os.path.sep)) bgneal@4: files = [os.path.basename(f) for f in files] bgneal@4: else: bgneal@4: files = [filename] bgneal@4: bgneal@4: for f in files: bgneal@4: self.process_page(f) bgneal@4: bgneal@4: def process_page(self, filename): bgneal@4: """Processes one fixed page""" bgneal@4: bgneal@4: # retrieve source text bgneal@4: src_path = os.path.join(self.src_dir, filename) bgneal@4: try: bgneal@4: with open(src_path, 'r') as f: bgneal@4: src_text = f.read() bgneal@4: except IOError, ex: bgneal@4: raise CommandError(str(ex)) bgneal@4: bgneal@4: # transform text bgneal@4: content = self.transform_input(src_text) bgneal@4: bgneal@4: # write output bgneal@4: basename = os.path.splitext(os.path.basename(filename))[0] bgneal@4: dst_path = os.path.join(self.dst_dir, '%s.html' % basename) bgneal@4: bgneal@4: try: bgneal@4: with open(dst_path, 'w') as f: bgneal@4: f.write(content.encode('utf-8')) bgneal@4: except IOError, ex: bgneal@4: raise CommandError(str(ex)) bgneal@4: bgneal@4: prefix = os.path.commonprefix([src_path, dst_path]) bgneal@4: self.stdout.write("%s -> %s\n" % (filename, dst_path[len(prefix):])) bgneal@4: bgneal@4: def transform_input(self, src_text): bgneal@4: """Transforms input restructured text to HTML""" bgneal@4: bgneal@4: return docutils.core.publish_parts(src_text, writer_name='html', bgneal@4: settings_overrides={ bgneal@4: 'doctitle_xform': False, bgneal@4: 'initial_header_level': 2, bgneal@4: })['html_body'] bgneal@4: bgneal@4: Next I would need a template that could render these fragments. I remembered bgneal@4: that the Django `include tag`_ could take a variable as an argument. Thus I bgneal@4: could create a single template that could render all of these "fixed" pages. bgneal@4: Here is the template ``templates/fixed/base.html``:: bgneal@4: bgneal@4: {% extends 'base.html' %} bgneal@4: {% block title %}{{ title }}{% endblock %} bgneal@4: {% block content %} bgneal@4: {% include content_template %} bgneal@4: {% endblock %} bgneal@4: bgneal@4: I just need to pass in ``title`` and ``content_template`` context variables. The bgneal@4: latter will control which HTML fragment I include. bgneal@4: bgneal@4: I then turned to the view function which would render this template. I wanted to bgneal@4: make this as generic and easy to do as possible. Since I was abandoning bgneal@4: flatpages, I would need to wire these up in my ``urls.py``. At first I didn't bgneal@4: think I could use Django's new `class-based generic views`_ for this, but after bgneal@4: some fiddling around, I came up with a very nice solution: bgneal@4: bgneal@4: .. sourcecode:: python bgneal@4: bgneal@4: from django.views.generic import TemplateView bgneal@4: bgneal@4: class FixedView(TemplateView): bgneal@4: """ bgneal@4: For displaying our "fixed" views generated with the custom command bgneal@4: make_fixed_page. bgneal@4: bgneal@4: """ bgneal@4: template_name = 'fixed/base.html' bgneal@4: title = '' bgneal@4: content_template = '' bgneal@4: bgneal@4: def get_context_data(self, **kwargs): bgneal@4: context = super(FixedView, self).get_context_data(**kwargs) bgneal@4: context['title'] = self.title bgneal@4: context['content_template'] = self.content_template bgneal@4: return context bgneal@4: bgneal@4: This allowed me to do the following in my ``urls.py`` file: bgneal@4: bgneal@4: .. sourcecode:: python bgneal@4: bgneal@4: urlpatterns = patterns('', bgneal@4: # ... bgneal@4: bgneal@4: url(r'^about/$', bgneal@4: FixedView.as_view(title='About', content_template='fixed/about.html'), bgneal@4: name='about'), bgneal@4: url(r'^colophon/$', bgneal@4: FixedView.as_view(title='Colophon', content_template='fixed/colophon.html'), bgneal@4: name='colophon'), bgneal@4: bgneal@4: # ... bgneal@4: bgneal@4: Now I have a way to efficiently serve reStructuredText files as "fixed pages" bgneal@4: that I can put under source code control. bgneal@4: bgneal@4: .. _flatpages app: https://docs.djangoproject.com/en/1.4/ref/contrib/flatpages/ bgneal@4: .. _reStructuredText: http://docutils.sourceforge.net/rst.html bgneal@4: .. _custom management command: https://docs.djangoproject.com/en/1.4/howto/custom-management-commands/ bgneal@4: .. _include tag: https://docs.djangoproject.com/en/1.4/ref/templates/builtins/#include bgneal@4: .. _class-based generic views: https://docs.djangoproject.com/en/1.4/topics/class-based-views/ bgneal@4: .. _django.contrib.markup: https://docs.djangoproject.com/en/1.4/ref/contrib/markup/