annotate content/Coding/016-django-fixed-pages.rst @ 24:ae90dc3de83d

Capture last blog edit. Put github before bitbucket in blog roll.
author Brian Neal <bgneal@gmail.com>
date Mon, 15 Feb 2021 13:20:35 -0600
parents 7ce6393e6d30
children
rev   line source
bgneal@4 1 Fixed pages for Django
bgneal@4 2 ######################
bgneal@4 3
bgneal@4 4 :date: 2012-05-13 13:30
bgneal@4 5 :tags: Django
bgneal@4 6 :slug: fixed-pages-for-django
bgneal@4 7 :author: Brian Neal
bgneal@4 8
bgneal@4 9 I have been using Django's `flatpages app`_ for some simple, static pages that
bgneal@4 10 were supposed to be temporary. I slapped a Javascript editor on the admin page
bgneal@4 11 and it has worked very well. However some of the pages have long outlived their
bgneal@4 12 "temporary" status, and I find myself needing to update them. It is then that I
bgneal@4 13 get angry at the Javascript editor, and there is no way to keep any kind of
bgneal@4 14 history on the page without having to fish through old database backups. I
bgneal@4 15 started to think it would be nice to write the content in a nice markup
bgneal@4 16 language, for example reStructuredText_, which I could then commit to version
bgneal@4 17 control. I would just need a way to generate HTML from the source text to
bgneal@4 18 produce the flatpage content.
bgneal@4 19
bgneal@4 20 Of course I could use the template filters in `django.contrib.markup`_. But
bgneal@4 21 turning markup into HTML at page request time can be more expensive than I like.
bgneal@4 22 Yes, I could cache the page, but I'd like the process to be more explicit.
bgneal@4 23
bgneal@4 24 In my first attempt at doing this, I wrote a `custom management command`_ that
bgneal@4 25 used a dictionary in my ``settings.py`` file to map reStructuredText files to
bgneal@4 26 flatpage URLs. My management command would open the input file, convert it to
bgneal@4 27 HTML, then find the ``FlatPage`` object associated with the URL. It would then
bgneal@4 28 update the object with the new HTML content and save it.
bgneal@4 29
bgneal@4 30 This worked okay, but in the end I decided that the pages I wanted to update
bgneal@4 31 were not temporary, quick & dirty pages, which is kind of how I view flatpages.
bgneal@4 32 So I decided to stop leaning on the flatpages app for these pages.
bgneal@4 33
bgneal@4 34 I then modified the management command to read a given input file, convert it
bgneal@4 35 to an HTML fragment, then save it in my templates directory. Thus, a file stored
bgneal@4 36 in my project directory as ``fixed/about.rst`` would get transformed to
bgneal@4 37 ``templates/fixed/about.html``. Here is the source to the command which I saved
bgneal@4 38 as ``make_fixed_page.py``:
bgneal@4 39
bgneal@4 40 .. sourcecode:: python
bgneal@4 41
bgneal@4 42 import os.path
bgneal@4 43 import glob
bgneal@4 44
bgneal@4 45 import docutils.core
bgneal@4 46 from django.core.management.base import LabelCommand, CommandError
bgneal@4 47 from django.conf import settings
bgneal@4 48
bgneal@4 49
bgneal@4 50 class Command(LabelCommand):
bgneal@4 51 help = "Generate HTML from restructured text files"
bgneal@4 52 args = "<inputfile1> <inputfile2> ... | all"
bgneal@4 53
bgneal@4 54 def handle_label(self, filename, **kwargs):
bgneal@4 55 """Process input file(s)"""
bgneal@4 56
bgneal@4 57 if not hasattr(settings, 'PROJECT_PATH'):
bgneal@4 58 raise CommandError("Please add a PROJECT_PATH setting")
bgneal@4 59
bgneal@4 60 self.src_dir = os.path.join(settings.PROJECT_PATH, 'fixed')
bgneal@4 61 self.dst_dir = os.path.join(settings.PROJECT_PATH, 'templates', 'fixed')
bgneal@4 62
bgneal@4 63 if filename == 'all':
bgneal@4 64 files = glob.glob("%s%s*.rst" % (self.src_dir, os.path.sep))
bgneal@4 65 files = [os.path.basename(f) for f in files]
bgneal@4 66 else:
bgneal@4 67 files = [filename]
bgneal@4 68
bgneal@4 69 for f in files:
bgneal@4 70 self.process_page(f)
bgneal@4 71
bgneal@4 72 def process_page(self, filename):
bgneal@4 73 """Processes one fixed page"""
bgneal@4 74
bgneal@4 75 # retrieve source text
bgneal@4 76 src_path = os.path.join(self.src_dir, filename)
bgneal@4 77 try:
bgneal@4 78 with open(src_path, 'r') as f:
bgneal@4 79 src_text = f.read()
bgneal@4 80 except IOError, ex:
bgneal@4 81 raise CommandError(str(ex))
bgneal@4 82
bgneal@4 83 # transform text
bgneal@4 84 content = self.transform_input(src_text)
bgneal@4 85
bgneal@4 86 # write output
bgneal@4 87 basename = os.path.splitext(os.path.basename(filename))[0]
bgneal@4 88 dst_path = os.path.join(self.dst_dir, '%s.html' % basename)
bgneal@4 89
bgneal@4 90 try:
bgneal@4 91 with open(dst_path, 'w') as f:
bgneal@4 92 f.write(content.encode('utf-8'))
bgneal@4 93 except IOError, ex:
bgneal@4 94 raise CommandError(str(ex))
bgneal@4 95
bgneal@4 96 prefix = os.path.commonprefix([src_path, dst_path])
bgneal@4 97 self.stdout.write("%s -> %s\n" % (filename, dst_path[len(prefix):]))
bgneal@4 98
bgneal@4 99 def transform_input(self, src_text):
bgneal@4 100 """Transforms input restructured text to HTML"""
bgneal@4 101
bgneal@4 102 return docutils.core.publish_parts(src_text, writer_name='html',
bgneal@4 103 settings_overrides={
bgneal@4 104 'doctitle_xform': False,
bgneal@4 105 'initial_header_level': 2,
bgneal@4 106 })['html_body']
bgneal@4 107
bgneal@4 108 Next I would need a template that could render these fragments. I remembered
bgneal@4 109 that the Django `include tag`_ could take a variable as an argument. Thus I
bgneal@4 110 could create a single template that could render all of these "fixed" pages.
bgneal@4 111 Here is the template ``templates/fixed/base.html``::
bgneal@4 112
bgneal@4 113 {% extends 'base.html' %}
bgneal@4 114 {% block title %}{{ title }}{% endblock %}
bgneal@4 115 {% block content %}
bgneal@4 116 {% include content_template %}
bgneal@4 117 {% endblock %}
bgneal@4 118
bgneal@4 119 I just need to pass in ``title`` and ``content_template`` context variables. The
bgneal@4 120 latter will control which HTML fragment I include.
bgneal@4 121
bgneal@4 122 I then turned to the view function which would render this template. I wanted to
bgneal@4 123 make this as generic and easy to do as possible. Since I was abandoning
bgneal@4 124 flatpages, I would need to wire these up in my ``urls.py``. At first I didn't
bgneal@4 125 think I could use Django's new `class-based generic views`_ for this, but after
bgneal@4 126 some fiddling around, I came up with a very nice solution:
bgneal@4 127
bgneal@4 128 .. sourcecode:: python
bgneal@4 129
bgneal@4 130 from django.views.generic import TemplateView
bgneal@4 131
bgneal@4 132 class FixedView(TemplateView):
bgneal@4 133 """
bgneal@4 134 For displaying our "fixed" views generated with the custom command
bgneal@4 135 make_fixed_page.
bgneal@4 136
bgneal@4 137 """
bgneal@4 138 template_name = 'fixed/base.html'
bgneal@4 139 title = ''
bgneal@4 140 content_template = ''
bgneal@4 141
bgneal@4 142 def get_context_data(self, **kwargs):
bgneal@4 143 context = super(FixedView, self).get_context_data(**kwargs)
bgneal@4 144 context['title'] = self.title
bgneal@4 145 context['content_template'] = self.content_template
bgneal@4 146 return context
bgneal@4 147
bgneal@4 148 This allowed me to do the following in my ``urls.py`` file:
bgneal@4 149
bgneal@4 150 .. sourcecode:: python
bgneal@4 151
bgneal@4 152 urlpatterns = patterns('',
bgneal@4 153 # ...
bgneal@4 154
bgneal@4 155 url(r'^about/$',
bgneal@4 156 FixedView.as_view(title='About', content_template='fixed/about.html'),
bgneal@4 157 name='about'),
bgneal@4 158 url(r'^colophon/$',
bgneal@4 159 FixedView.as_view(title='Colophon', content_template='fixed/colophon.html'),
bgneal@4 160 name='colophon'),
bgneal@4 161
bgneal@4 162 # ...
bgneal@4 163
bgneal@4 164 Now I have a way to efficiently serve reStructuredText files as "fixed pages"
bgneal@4 165 that I can put under source code control.
bgneal@4 166
bgneal@4 167 .. _flatpages app: https://docs.djangoproject.com/en/1.4/ref/contrib/flatpages/
bgneal@4 168 .. _reStructuredText: http://docutils.sourceforge.net/rst.html
bgneal@4 169 .. _custom management command: https://docs.djangoproject.com/en/1.4/howto/custom-management-commands/
bgneal@4 170 .. _include tag: https://docs.djangoproject.com/en/1.4/ref/templates/builtins/#include
bgneal@4 171 .. _class-based generic views: https://docs.djangoproject.com/en/1.4/topics/class-based-views/
bgneal@4 172 .. _django.contrib.markup: https://docs.djangoproject.com/en/1.4/ref/contrib/markup/