Merge lp:~fenryxo/zim/template-resources into lp:~jaap.karssenberg/zim/pyzim
- template-resources
- Merge into pyzim
Status: | Merged |
---|---|
Merged at revision: | 412 |
Proposed branch: | lp:~fenryxo/zim/template-resources |
Merge into: | lp:~jaap.karssenberg/zim/pyzim |
Diff against target: |
442 lines (+238/-23) 7 files modified
tests/data/template-resources/Default.html (+85/-0) tests/export.py (+41/-1) tests/www.py (+33/-10) zim/exporter.py (+31/-6) zim/formats/__init__.py (+6/-2) zim/templates.py (+13/-3) zim/www.py (+29/-1) |
To merge this branch: | bzr merge lp:~fenryxo/zim/template-resources |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Jaap Karssenberg | Approve | ||
Review via email: mp+60421@code.launchpad.net |
Commit message
Description of the change
This branch contains feature "Template Resources".
Templates are allowed to have resources (images, stylesheets, whatever) in the directory with the same name as template without extension (~/foo/
* Template files are linked by template function template() (e.g. `<img src="[% template(
* Template dir is exported to subdir "_template" (similarly like "_icons" before).
* The path in WWWInteface is "/+template/*".
Template can also override default pixmaps (for checkboxes, favicon, etc.):
* Pixmaps are not exported to subdir "_icons" anymore. They are exported to "_template" subdir unless the template has its own pixmap.
* The WWWInterface is also looking for pixmaps in template directory, the original pixmaps are fallback.
Basic test coverage included.
Jaap Karssenberg (jaap.karssenberg) wrote : | # |
Jiří Janoušek (fenryxo) wrote : | # |
I think "_resources" sounds better, because there is actually no template in the directory, only template resources. And how about the template function name?
Jaap Karssenberg (jaap.karssenberg) wrote : | # |
2011/5/12 Jiří Janoušek <email address hidden>
> I think "_resources" sounds better, because there is actually no template
> in the directory, only template resources. And how about the template
> function name?
>
Guess that would also become "resource()" and "+resource" for the web
interface.
-- Jaap
Jiří Janoušek (fenryxo) wrote : | # |
Sure, the name should be consistent. Will you rename it during merging
or it's up to me?
2011/5/12 Jaap Karssenberg <email address hidden>:
> 2011/5/12 Jiří Janoušek <email address hidden>
>
>> I think "_resources" sounds better, because there is actually no template
>> in the directory, only template resources. And how about the template
>> function name?
>>
>
> Guess that would also become "resource()" and "+resource" for the web
> interface.
>
> -- Jaap
>
> --
> https:/
> You are the owner of lp:~janousek.jiri/zim/template-resources.
>
Jaap Karssenberg (jaap.karssenberg) wrote : | # |
2011/5/12 Jiří Janoušek <email address hidden>
> Sure, the name should be consistent. Will you rename it during merging
> or it's up to me?
>
I can take care of that.
Thanks for the patch!
-- Jaap
Jaap Karssenberg (jaap.karssenberg) : | # |
Preview Diff
1 | === added directory 'tests/data/template-resources' |
2 | === added directory 'tests/data/template-resources/Default' |
3 | === added file 'tests/data/template-resources/Default.html' |
4 | --- tests/data/template-resources/Default.html 1970-01-01 00:00:00 +0000 |
5 | +++ tests/data/template-resources/Default.html 2011-05-09 20:14:23 +0000 |
6 | @@ -0,0 +1,85 @@ |
7 | +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> |
8 | +<html> |
9 | + <head> |
10 | + <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> |
11 | + <title>[% title %]</title> |
12 | + <meta name='Generator' content='Zim [% zim.version %]'> |
13 | + <style type='text/css'> |
14 | + a { text-decoration: none } |
15 | + a:hover { text-decoration: underline } |
16 | + a:active { text-decoration: underline } |
17 | + strike { color: grey } |
18 | + u { text-decoration: none; |
19 | + background-color: yellow } |
20 | + tt { color: #2e3436; } |
21 | + pre { color: #2e3436; |
22 | + margin-left: 20px } |
23 | + h1 { text-decoration: underline; |
24 | + color: #4e9a06 } |
25 | + h2 { color: #4e9a06 } |
26 | + h3 { color: #4e9a06 } |
27 | + h4 { color: #4e9a06 } |
28 | + h5 { color: #4e9a06 } |
29 | + span.insen { color: grey } |
30 | + </style> |
31 | + </head> |
32 | + <body> |
33 | + |
34 | +<!-- Header --> |
35 | + |
36 | +[% IF pages.previous -%] |
37 | + [ <a href='[% url(pages.previous) %]'>Prev</a> ] |
38 | +[%- ELSE -%] |
39 | + [ <span class='insen'>Prev</span> ] |
40 | +[%- END %] |
41 | + |
42 | +[% IF page.properties.type == 'namespace-index' -%] |
43 | + [ <span class='insen'>Index</span> ] |
44 | +[%- ELSE -%] |
45 | + [% IF pages.index -%] |
46 | + [ <a href='[% url(pages.index) %]'>Index</a> ] |
47 | + [%- ELSE -%] |
48 | + [ <a href='/'>Index</a> ] |
49 | + [%- END %] |
50 | +[%- END %] |
51 | + |
52 | +[% IF pages.next -%] |
53 | + [ <a href='[% url(pages.next) %]'>Next</a> ] |
54 | +[%- ELSE -%] |
55 | + [ <span class='insen'>Next</span> ] |
56 | +[%- END %] |
57 | + |
58 | +<!-- End Header --> |
59 | + |
60 | +<hr /> |
61 | + |
62 | +<!-- Wiki content --> |
63 | + |
64 | +[% IF page.properties.type == 'namespace-index' -%] |
65 | +<h1>Document Index</h1> |
66 | +[%- ELSE -%] |
67 | +<h1>[% page.heading %]</h1> |
68 | +[%- END %] |
69 | + |
70 | +[% page.body %] |
71 | + |
72 | +<!-- End wiki content --> |
73 | + |
74 | +<hr /> |
75 | +<a href="http:/www.zim-wiki.org" style="float:right;">Powered by Zim [% zim.version %]<img src="[% template('favicon/zim.png') %]" alt=" "></a> |
76 | +<!-- Backlinks --> |
77 | + |
78 | +[% IF page.backlinks -%] |
79 | + Backlinks: |
80 | + [%- FOREACH link = page.backlinks -%] |
81 | + <a href='[% url(link) %]'>[% link.name %]</a></li> |
82 | + [%- END -%] |
83 | +[%- ELSE -%] |
84 | + No backlinks to this page. |
85 | +[%- END %] |
86 | + |
87 | +<!-- End Backlinks --> |
88 | + |
89 | + </body> |
90 | + |
91 | +</html> |
92 | |
93 | === added file 'tests/data/template-resources/Default/checked-box.png' |
94 | Binary files tests/data/template-resources/Default/checked-box.png 1970-01-01 00:00:00 +0000 and tests/data/template-resources/Default/checked-box.png 2011-05-09 20:14:23 +0000 differ |
95 | === added directory 'tests/data/template-resources/Default/favicon' |
96 | === added file 'tests/data/template-resources/Default/favicon/zim.png' |
97 | Binary files tests/data/template-resources/Default/favicon/zim.png 1970-01-01 00:00:00 +0000 and tests/data/template-resources/Default/favicon/zim.png 2011-05-09 20:14:23 +0000 differ |
98 | === added file 'tests/data/template-resources/Default/unchecked-box.png' |
99 | Binary files tests/data/template-resources/Default/unchecked-box.png 1970-01-01 00:00:00 +0000 and tests/data/template-resources/Default/unchecked-box.png 2011-05-09 20:14:23 +0000 differ |
100 | === added file 'tests/data/template-resources/Default/xchecked-box.png' |
101 | Binary files tests/data/template-resources/Default/xchecked-box.png 1970-01-01 00:00:00 +0000 and tests/data/template-resources/Default/xchecked-box.png 2011-05-09 20:14:23 +0000 differ |
102 | === modified file 'tests/export.py' |
103 | --- tests/export.py 2011-04-02 12:36:48 +0000 |
104 | +++ tests/export.py 2011-05-09 20:14:23 +0000 |
105 | @@ -7,6 +7,8 @@ |
106 | from subprocess import check_call |
107 | |
108 | from zim.fs import * |
109 | +from zim.fs import _md5 |
110 | +from zim.config import data_file |
111 | from zim.notebook import Path, Notebook, init_notebook |
112 | from zim.exporter import Exporter, StaticLinker |
113 | |
114 | @@ -56,7 +58,45 @@ |
115 | text = file.read() |
116 | self.assertTrue('<!-- Wiki content -->' in text, 'template used') |
117 | self.assertTrue('<h1>Foo</h1>' in text) |
118 | - |
119 | + for i in ('checked-box', 'unchecked-box', 'xchecked-box'): |
120 | + self.assertTrue(self.dir.file('_template/%s.png' % i).exists()) |
121 | + # Default template doesn't have its own checkboxes |
122 | + self.assertTrue(_md5(self.dir.file('_template/%s.png' % i).raw()) |
123 | + == _md5(data_file('pixmaps/%s.png' % i).raw())) |
124 | + |
125 | +class TestExportTemplateResources(TestCase): |
126 | + |
127 | + slowTest = True |
128 | + file = File('tests/data/template-resources/Default.html') |
129 | + options = {'format': 'html', 'template': file.path} |
130 | + |
131 | + def setUp(self): |
132 | + self.dir = Dir(create_tmp_dir('export_ExportTemplateResources')) |
133 | + |
134 | + def export(self): |
135 | + notebook = get_test_notebook() |
136 | + notebook.get_store(Path(':')).dir = Dir('/foo/bar') # fake source dir |
137 | + notebook.index.update() |
138 | + exporter = Exporter(notebook, **self.options) |
139 | + exporter.export_all(self.dir) |
140 | + |
141 | + def runTest(self): |
142 | + '''Test export notebook to html with template resources''' |
143 | + self.export() |
144 | + |
145 | + file = self.dir.file('Test/foo.html') |
146 | + self.assertTrue(file.exists()) |
147 | + text = file.read() |
148 | + self.assertTrue('src="../_template/favicon/zim.png"' in text) |
149 | + |
150 | + self.assertTrue(self.dir.file('_template/favicon/zim.png').exists()) |
151 | + for i in ('checked-box', 'unchecked-box', 'xchecked-box'): |
152 | + self.assertTrue(self.dir.file('_template/%s.png' % i).exists()) |
153 | + # Template has its own checkboxes |
154 | + self.assertFalse(_md5(self.dir.file('_template/%s.png' % i).raw()) |
155 | + == _md5(data_file('pixmaps/%s.png' % i).raw())) |
156 | + |
157 | + |
158 | |
159 | class TestExportFullOptions(TestExport): |
160 | |
161 | |
162 | === modified file 'tests/www.py' |
163 | --- tests/www.py 2011-02-19 16:27:44 +0000 |
164 | +++ tests/www.py 2011-05-09 20:14:23 +0000 |
165 | @@ -5,7 +5,7 @@ |
166 | from __future__ import with_statement |
167 | |
168 | from tests import TestCase, LoggingFilter, get_test_notebook |
169 | - |
170 | +from zim.fs import File |
171 | import sys |
172 | from cStringIO import StringIO |
173 | import logging |
174 | @@ -54,6 +54,8 @@ |
175 | |
176 | def setUp(self): |
177 | self.template = None |
178 | + self.not_found_paths = ['/Test', '/nonexistingpage.html', '/nonexisting/'] |
179 | + self.file_paths = ['/favicon.ico', '/+template/checked-box.png'] |
180 | |
181 | def runTest(self): |
182 | 'Test WWW interface' |
183 | @@ -92,19 +94,21 @@ |
184 | response = call('GET', '/Test/foo.html') |
185 | self.assertResponseOK(response) |
186 | self.assertTrue('<h1>Foo</h1>' in response) |
187 | - |
188 | + |
189 | + |
190 | # page not found |
191 | |
192 | with Filter404(): |
193 | - for path in ('/Test', '/nonexistingpage.html', '/nonexisting/'): |
194 | + for path in self.not_found_paths: |
195 | response = call('GET', path) |
196 | header, body = self.assertResponseWellFormed(response) |
197 | self.assertEqual(header[0], 'HTTP/1.0 404 Not Found') |
198 | |
199 | - # favicon |
200 | - response = call('GET', '/favicon.ico') |
201 | - header, body = self.assertResponseWellFormed(response) |
202 | - self.assertEqual(header[0], 'HTTP/1.0 200 OK') |
203 | + # favicon and other files |
204 | + for path in self.file_paths: |
205 | + response = call('GET', path) |
206 | + header, body = self.assertResponseWellFormed(response) |
207 | + self.assertEqual(header[0], 'HTTP/1.0 200 OK') |
208 | |
209 | |
210 | class TestWWWInterfaceTemplate(TestWWWInterface): |
211 | @@ -115,8 +119,27 @@ |
212 | self.assertTrue('<!-- Wiki content -->' in body, 'Template is used') |
213 | |
214 | def setUp(self): |
215 | + TestWWWInterface.setUp(self) |
216 | self.template = 'Default' |
217 | - |
218 | - def runTest(self): |
219 | - 'Test WWW interface with a template' |
220 | + self.not_found_paths.append('/+template/favicon/zim.png') |
221 | + |
222 | + def runTest(self): |
223 | + 'Test WWW interface with a template. "ERROR: No such file: ..." message expected' |
224 | + TestWWWInterface.runTest(self) |
225 | + |
226 | +class TestWWWInterfaceTemplateResources(TestWWWInterface): |
227 | + |
228 | + def assertResponseOK(self, response, expectbody=True): |
229 | + header, body = TestWWWInterface.assertResponseOK(self, response, expectbody) |
230 | + if expectbody: |
231 | + self.assertTrue('src="/%2Btemplate/favicon/zim.png"' ''.join(body), 'Template is used') |
232 | + |
233 | + def setUp(self): |
234 | + TestWWWInterface.setUp(self) |
235 | + self.file = File('tests/data/template-resources/Default.html') |
236 | + self.template = self.file.path |
237 | + self.file_paths.append('/+template/favicon/zim.png') |
238 | + |
239 | + def runTest(self): |
240 | + 'Test WWW interface with a template with resources.' |
241 | TestWWWInterface.runTest(self) |
242 | |
243 | === modified file 'zim/exporter.py' |
244 | --- zim/exporter.py 2011-04-02 12:36:48 +0000 |
245 | +++ zim/exporter.py 2011-05-09 20:14:23 +0000 |
246 | @@ -6,7 +6,7 @@ |
247 | |
248 | import logging |
249 | |
250 | -from zim.fs import * |
251 | +from zim.fs import Path as FsPath, Dir, File |
252 | from zim.config import data_file |
253 | from zim.formats import get_format, BaseLinker |
254 | from zim.templates import get_template, Template |
255 | @@ -60,12 +60,30 @@ |
256 | logger.info('Exporting notebook to %s', dir) |
257 | self.linker.target_dir = dir # Needed to resolve icons |
258 | |
259 | + # Copy template resources |
260 | + if self.template and self.template.resources and self.template.resources.exists(): |
261 | + #~ print '>>>', self.template.resources, "exists!" |
262 | + def copy_dir(source, target): |
263 | + target.touch() |
264 | + for item in source.list(): |
265 | + child = FsPath((source.path, item)) |
266 | + if child.isdir(): |
267 | + copy_dir(source.subdir(item), target.subdir(item)) # recur |
268 | + else: |
269 | + source.file(item).copyto(target) |
270 | + |
271 | + copy_dir(self.template.resources, dir.subdir('_template')) |
272 | + #~ else: |
273 | + #~ print '>>>', self.template.resources, "doesn't exist!" |
274 | + |
275 | # Copy icons |
276 | for name in ('checked-box', 'unchecked-box', 'xchecked-box'): |
277 | icon = data_file('pixmaps/%s.png' % name) |
278 | - file = dir.file('_icons/'+name+'.png') |
279 | - icon.copyto(file) |
280 | - |
281 | + file = dir.file('_template/'+name+'.png') |
282 | + # Do not overwite custom images from template |
283 | + if not file.exists(): |
284 | + icon.copyto(file) |
285 | + |
286 | # Set special pages |
287 | if self.index_page: |
288 | indexpage = Page(Path(self.index_page)) |
289 | @@ -191,11 +209,18 @@ |
290 | self.target_dir = None |
291 | self.target_file = None |
292 | self._extension = '.' + format.info['extension'] |
293 | + |
294 | + def template(self, path): |
295 | + if self.target_dir and self.target_file: |
296 | + file = self.target_dir.file('_template/'+path) |
297 | + return self._filepath(file, self.target_file.dir) |
298 | + else: |
299 | + return BaseLinker.template(self, path) |
300 | |
301 | def icon(self, name): |
302 | if self.target_dir and self.target_file: |
303 | - file = self.target_dir.file('_icons/'+name+'.png') |
304 | - return self._filepath(file, self.target_file) |
305 | + file = self.target_dir.file('_template/'+name+'.png') |
306 | + return self._filepath(file, self.target_file.dir) |
307 | else: |
308 | return BaseLinker.icon(self, name) |
309 | |
310 | |
311 | === modified file 'zim/formats/__init__.py' |
312 | --- zim/formats/__init__.py 2011-04-08 18:10:03 +0000 |
313 | +++ zim/formats/__init__.py 2011-05-09 20:14:23 +0000 |
314 | @@ -661,7 +661,7 @@ |
315 | if href and href != link: |
316 | href = self.link(href) # recurs |
317 | else: |
318 | - logg.warn('No URL found for interwiki link: %s', href) |
319 | + logger.warn('No URL found for interwiki link: %s', href) |
320 | link = href |
321 | else: # I dunno, some url ? |
322 | method = 'link_' + type |
323 | @@ -675,7 +675,11 @@ |
324 | def img(self, src): |
325 | '''Returns an url for image file 'src' ''' |
326 | return self.link_file(src) |
327 | - |
328 | + |
329 | + def template(self, path): |
330 | + '''To be overloaded, return an url for template resources''' |
331 | + raise NotImplementedError |
332 | + |
333 | def icon(self, name): |
334 | '''Returns an url for an icon''' |
335 | if not name in self._icons: |
336 | |
337 | === modified file 'zim/templates.py' |
338 | --- zim/templates.py 2011-02-19 16:27:44 +0000 |
339 | +++ zim/templates.py 2011-05-09 20:14:23 +0000 |
340 | @@ -118,7 +118,9 @@ |
341 | logger.info('Loading template from: %s', file) |
342 | if not file.exists(): |
343 | raise AssertionError, 'No such file: %s' % file |
344 | - return Template(file.readlines(), format, name=file.path) |
345 | + resources_dirname = file.basename.rsplit('.', 1)[0] |
346 | + resources = file.dir.subdir(resources_dirname) |
347 | + return Template(file.readlines(), format, name=file.path, resources=resources) |
348 | |
349 | |
350 | class TemplateError(Error): |
351 | @@ -296,11 +298,14 @@ |
352 | class Template(GenericTemplate): |
353 | '''Template class that can process a zim Page object''' |
354 | |
355 | - def __init__(self, input, format, linker=None, name=None): |
356 | + def __init__(self, input, format, linker=None, name=None, resources=None): |
357 | if isinstance(format, basestring): |
358 | format = zim.formats.get_format(format) |
359 | self.format = format |
360 | self.linker = linker |
361 | + if isinstance(resources, basestring): |
362 | + resources = Dir(resources) |
363 | + self.resources = resources |
364 | GenericTemplate.__init__(self, input, name) |
365 | |
366 | def set_linker(self, linker): |
367 | @@ -335,6 +340,7 @@ |
368 | 'pages': pages, |
369 | 'strftime': StrftimeFunction(), |
370 | 'url': TemplateFunction(self.url), |
371 | + 'template': TemplateFunction(self.template_url), |
372 | 'options': options |
373 | } |
374 | |
375 | @@ -371,7 +377,11 @@ |
376 | return linker.link(link) |
377 | else: |
378 | return link |
379 | - |
380 | + |
381 | + def template_url(self, dict, path): |
382 | + if self.linker: |
383 | + return self.linker.template(path) |
384 | + return path |
385 | |
386 | class TemplateTokenList(list): |
387 | '''This class contains a list of TemplateToken objects and strings''' |
388 | |
389 | === modified file 'zim/www.py' |
390 | --- zim/www.py 2011-04-14 20:01:02 +0000 |
391 | +++ zim/www.py 2011-05-09 20:14:23 +0000 |
392 | @@ -177,6 +177,8 @@ |
393 | content = [file.raw()] |
394 | # Will raise FileNotFound when file does not exist |
395 | headers['Content-Type'] = file.get_mimetype() |
396 | + |
397 | + # /+icons/ path is understood due to backward compatibility |
398 | elif path.startswith('/+icons/'): |
399 | # TODO check if favicon is overridden or something |
400 | file = data_file('pixmaps/%s' % path[8:]) |
401 | @@ -188,6 +190,29 @@ |
402 | headers['Content-Type'] = 'image/vnd.microsoft.icon' |
403 | else: |
404 | raise PathNotValidError() |
405 | + elif path.startswith('/+template/'): |
406 | + icon = data_file('pixmaps/%s' % path[11:]) |
407 | + if self.template and self.template.resources: |
408 | + file = self.template.resources.file(path[11:]) |
409 | + else: |
410 | + file = None |
411 | + |
412 | + # icon is checked only if template file does not exist |
413 | + if icon and icon.exists() and (not file or not file.exists()): |
414 | + content = [icon.raw()] |
415 | + # Will raise FileNotFound when file does not exist |
416 | + if path.endswith('.png'): |
417 | + headers['Content-Type'] = 'image/png' |
418 | + elif path.endswith('.ico'): |
419 | + headers['Content-Type'] = 'image/vnd.microsoft.icon' |
420 | + else: |
421 | + raise PathNotValidError() |
422 | + |
423 | + elif file: |
424 | + content = [file.raw()] |
425 | + headers['Content-Type'] = file.get_mimetype() |
426 | + else: |
427 | + raise PageNotFoundError(path) |
428 | else: |
429 | # Must be a page or a namespace (html file or directory path) |
430 | headers.add_header('Content-Type', 'text/html', charset='utf-8') |
431 | @@ -395,7 +420,10 @@ |
432 | self.path = path |
433 | |
434 | def icon(self, name): |
435 | - return url_encode('/+icons/%s.png' % name) |
436 | + return url_encode('/+template/%s.png' % name) |
437 | + |
438 | + def template(self, path): |
439 | + return url_encode('/+template/%s' % path) |
440 | |
441 | def link_page(self, link): |
442 | try: |
Nice patch! Just wondering if "_template" is the right name here for the folder - but don't know a better way (was thinking of "_resources" but not sure). Probably will merge as is.