1 from django.conf import settings |
|
2 |
|
3 import codecs |
|
4 import datetime |
|
5 import logging; log = logging.getLogger('qmsk_www_pages.pages') |
|
6 import os, os.path |
|
7 |
|
8 import markdown |
|
9 import mako.template |
|
10 |
|
11 class NotFound (Exception): |
|
12 pass |
|
13 |
|
14 class RenderError (Exception): |
|
15 pass |
|
16 |
|
17 class Site (object): |
|
18 @classmethod |
|
19 def lookup (cls): |
|
20 return cls( |
|
21 root = settings.QMSK_WWW_PAGES_DIR, |
|
22 name = settings.QMSK_WWW_PAGES_SITE, |
|
23 ) |
|
24 |
|
25 def __init__ (self, root, name): |
|
26 self.root = root |
|
27 self.name = name |
|
28 |
|
29 def tree (self): |
|
30 return Tree(self.root, None, (), self, |
|
31 title = self.name, |
|
32 ) |
|
33 |
|
34 class Tree (object): |
|
35 INDEX = 'index' |
|
36 |
|
37 @classmethod |
|
38 def lookup (cls, site, parts): |
|
39 """ |
|
40 Returns Tree |
|
41 |
|
42 Raises NotFound |
|
43 """ |
|
44 |
|
45 parents = ( ) |
|
46 tree = site.tree() |
|
47 |
|
48 for name in parts: |
|
49 if name.startswith('.'): |
|
50 # evil |
|
51 raise NotFound() |
|
52 |
|
53 if not name: |
|
54 continue |
|
55 |
|
56 path = os.path.join(tree.path, name) |
|
57 |
|
58 if not os.path.exists(path): |
|
59 raise NotFound() |
|
60 |
|
61 if not os.path.isdir(path): |
|
62 raise NotFound() |
|
63 |
|
64 # title |
|
65 title = tree.item_title(name) |
|
66 |
|
67 parents += (tree, ) |
|
68 tree = cls(path, name, parents, site, |
|
69 title = title, |
|
70 ) |
|
71 |
|
72 return tree |
|
73 |
|
74 def __init__ (self, path, name, parents, site, |
|
75 title = None, |
|
76 ): |
|
77 """ |
|
78 path: filesystem path |
|
79 name: subtree name, or None for root |
|
80 parents: (Tree) |
|
81 site: Site |
|
82 """ |
|
83 |
|
84 self.path = path |
|
85 self.name = name |
|
86 self.parents = parents |
|
87 self.site = site |
|
88 |
|
89 self.title = title or name |
|
90 |
|
91 def hierarchy (self): |
|
92 """ |
|
93 Yield Tree. |
|
94 """ |
|
95 |
|
96 for tree in self.parents: |
|
97 yield tree |
|
98 |
|
99 yield self |
|
100 |
|
101 def url (self, tree=None, page=None): |
|
102 path = '/'.join(tree.name for tree in self.hierarchy() if tree.name is not None) |
|
103 |
|
104 if path: |
|
105 path += '/' |
|
106 |
|
107 if tree: |
|
108 path = tree + '/' |
|
109 |
|
110 if page: |
|
111 path += page |
|
112 |
|
113 return path |
|
114 |
|
115 def scan (self): |
|
116 """ |
|
117 Scan for files in tree. |
|
118 """ |
|
119 |
|
120 for filename in os.listdir(self.path): |
|
121 if filename.startswith('.'): |
|
122 continue |
|
123 |
|
124 if '.' in filename: |
|
125 file_name, file_type = filename.rsplit('.', 1) |
|
126 else: |
|
127 file_name = filename |
|
128 file_type = None |
|
129 |
|
130 if not file_name: |
|
131 continue |
|
132 |
|
133 path = os.path.join(self.path, filename) |
|
134 |
|
135 yield path, file_name, file_type |
|
136 |
|
137 def item_title (self, name): |
|
138 """ |
|
139 Lookup item title if exists. |
|
140 """ |
|
141 |
|
142 title_path = os.path.join(self.path, name + '.title') |
|
143 |
|
144 log.info("%s: %s title_path=%s", self, name, title_path) |
|
145 |
|
146 if os.path.exists(title_path): |
|
147 return open(title_path).read().strip() |
|
148 else: |
|
149 return None |
|
150 |
|
151 def list (self): |
|
152 """ |
|
153 Lists all Trees and Pages for this Tree. |
|
154 |
|
155 Yields (name, url, page_type or None, title) |
|
156 """ |
|
157 |
|
158 for path, name, file_type in self.scan(): |
|
159 title = self.item_title(name) or name |
|
160 |
|
161 # trees |
|
162 if os.path.isdir(path): |
|
163 yield name, self.url(tree=name), None, title |
|
164 |
|
165 if name == self.INDEX: |
|
166 continue |
|
167 |
|
168 # pages |
|
169 if not file_type: |
|
170 continue |
|
171 |
|
172 if file_type not in TYPES: |
|
173 continue |
|
174 |
|
175 yield name, self.url(page=name), file_type, title |
|
176 |
|
177 def list_sorted (self): |
|
178 return sorted(list(self.list())) |
|
179 |
|
180 def page (self, name): |
|
181 """ |
|
182 Scans through tree looking for a matching page. |
|
183 |
|
184 Returns Page or None. |
|
185 """ |
|
186 |
|
187 if not name: |
|
188 name = self.INDEX |
|
189 title_default = self.title |
|
190 else: |
|
191 title_default = None |
|
192 |
|
193 parents = self.parents + (self, ) |
|
194 |
|
195 for path, file_name, file_type in self.scan(): |
|
196 # match on name |
|
197 if file_name == name: |
|
198 pass |
|
199 elif file_type and (file_name + '.' + file_type == name): |
|
200 pass |
|
201 else: |
|
202 continue |
|
203 |
|
204 # redirects? |
|
205 if os.path.islink(path): |
|
206 target = os.readlink(path) |
|
207 |
|
208 # XXX: this should be some kind of common code |
|
209 if '.' in target: |
|
210 target, target_type = target.rsplit('.', 1) |
|
211 |
|
212 log.info("%s: %s -> %s", self, name, target) |
|
213 |
|
214 return RedirectPage(path, name, self, parents, |
|
215 target = target, |
|
216 ) |
|
217 |
|
218 # match on type |
|
219 if not file_type: |
|
220 continue |
|
221 |
|
222 page_type = TYPES.get(file_type) |
|
223 |
|
224 if not page_type: |
|
225 continue |
|
226 |
|
227 # out |
|
228 title = self.item_title(file_name) or title_default |
|
229 |
|
230 return page_type(path, name, self, parents, |
|
231 title = title, |
|
232 ) |
|
233 |
|
234 class Page (object): |
|
235 ENCODING = 'utf-8' |
|
236 |
|
237 @classmethod |
|
238 def lookup (cls, site, page): |
|
239 """ |
|
240 Returns Page. |
|
241 |
|
242 Raises NotFound |
|
243 """ |
|
244 |
|
245 log.info("page=%r", page) |
|
246 |
|
247 if page: |
|
248 parts = page.split('/') |
|
249 else: |
|
250 parts = [ ] |
|
251 |
|
252 if parts: |
|
253 page_name = parts.pop(-1) |
|
254 tree_parts = parts |
|
255 else: |
|
256 page_name = '' |
|
257 tree_parts = [] |
|
258 |
|
259 # scan dir |
|
260 tree = Tree.lookup(site, tree_parts) |
|
261 |
|
262 # scan page |
|
263 page = tree.page(page_name) |
|
264 |
|
265 if not page: |
|
266 raise NotFound() |
|
267 |
|
268 return page |
|
269 |
|
270 def __init__ (self, path, name, tree, parents=(), encoding=ENCODING, title=None): |
|
271 self.path = path |
|
272 self.name = name |
|
273 self.tree = tree |
|
274 self.parents = parents |
|
275 |
|
276 self.encoding = encoding |
|
277 self.title = title or name |
|
278 |
|
279 def hierarchy (self): |
|
280 """ |
|
281 Yield (Tree, name) pairs |
|
282 """ |
|
283 |
|
284 parent = None |
|
285 |
|
286 for tree in self.parents: |
|
287 if parent: |
|
288 yield parent, tree.name |
|
289 |
|
290 parent = tree |
|
291 |
|
292 yield parent, self.name |
|
293 |
|
294 def url (self): |
|
295 return self.tree.url(page=self.name) |
|
296 |
|
297 def open (self): |
|
298 return codecs.open(self.path, encoding=self.encoding) |
|
299 |
|
300 def stat (self): |
|
301 return os.stat(self.path) |
|
302 |
|
303 def breadcrumb (self): |
|
304 for tree in self.tree.hierarchy(): |
|
305 yield tree.url(), tree.title |
|
306 |
|
307 if self.name != self.tree.INDEX: |
|
308 yield self.url(), self.title |
|
309 |
|
310 def modified (self): |
|
311 return datetime.datetime.utcfromtimestamp(self.stat().st_mtime) |
|
312 |
|
313 def redirect_page (self, request): |
|
314 return None |
|
315 |
|
316 def render_html (self, request): |
|
317 raise NotImplementedError() |
|
318 |
|
319 # TODO: tree redirects |
|
320 class RedirectPage (Page): |
|
321 def __init__ (self, path, name, tree, parents, |
|
322 target, |
|
323 **opts |
|
324 ) : |
|
325 super(RedirectPage, self).__init__(path, name, tree, parents, **opts) |
|
326 |
|
327 self.target = target |
|
328 |
|
329 def redirect_page (self, request): |
|
330 return os.path.normpath(self.tree.url() + '/' + self.target) |
|
331 |
|
332 class HTML_Page (Page): |
|
333 def render_html (self, request): |
|
334 return self.open().read() |
|
335 |
|
336 class MarkdownPage (Page): |
|
337 FORMAT = 'html5' |
|
338 |
|
339 def __init__ (self, path, name, tree, parents, |
|
340 format=FORMAT, |
|
341 **opts |
|
342 ) : |
|
343 super(MarkdownPage, self).__init__(path, name, tree, parents, **opts) |
|
344 |
|
345 self.format = format |
|
346 |
|
347 def render_html (self, request): |
|
348 return markdown.markdown(self.open().read(), |
|
349 output_format = self.format, |
|
350 ) |
|
351 |
|
352 class TemplatePage (Page): |
|
353 def render_html (self, request): |
|
354 """ |
|
355 Raises RenderError if !DEBUG, arbitrary error with stack trace otherwise. |
|
356 """ |
|
357 |
|
358 try: |
|
359 return mako.template.Template(filename=self.path).render( |
|
360 request = request, |
|
361 ) |
|
362 except Exception as error: |
|
363 if settings.DEBUG: |
|
364 raise |
|
365 else: |
|
366 raise RenderError(error) |
|
367 |
|
368 SITE = Site.lookup() |
|
369 |
|
370 TYPES = { |
|
371 'html': HTML_Page, |
|
372 'md': MarkdownPage, |
|
373 'markdown': MarkdownPage, |
|
374 'tmpl': TemplatePage, |
|
375 } |
|
376 |
|
377 def page (page): |
|
378 return Page.lookup(SITE, page) |
|
379 |
|