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/
|