Zim

Merge lp:~fenryxo/zim/template-resources into lp:~jaap.karssenberg/zim/pyzim

Proposed by Jiří Janoušek
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
Reviewer Review Type Date Requested Status
Jaap Karssenberg Approve
Review via email: mp+60421@code.launchpad.net

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.html → ~/foo/Template/*). It is especially useful for HTML templates.

* Template files are linked by template function template() (e.g. `<img src="[% template('line.png') %]">`).
* 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.

To post a comment you must log in.
Revision history for this message
Jaap Karssenberg (jaap.karssenberg) wrote :

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.

Revision history for this message
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?

Revision history for this message
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

Revision history for this message
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://code.launchpad.net/~janousek.jiri/zim/template-resources/+merge/60421
> You are the owner of lp:~janousek.jiri/zim/template-resources.
>

Revision history for this message
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

Revision history for this message
Jaap Karssenberg (jaap.karssenberg) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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'
94Binary 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'
97Binary 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'
99Binary 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'
101Binary 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: