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