Merge lp:~thomir-deactivatedaccount/wikkid/wikkid-improvments into lp:wikkid
- wikkid-improvments
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 53 |
Proposed branch: | lp:~thomir-deactivatedaccount/wikkid/wikkid-improvments |
Merge into: | lp:wikkid |
Diff against target: |
802 lines (+532/-24) 18 files modified
Hacking.txt (+2/-1) ToDo.txt (+0/-2) bin/wikkid-serve (+19/-5) wikkid/app.py (+19/-8) wikkid/context.py (+43/-0) wikkid/dispatcher.py (+3/-2) wikkid/fileutils.py (+58/-0) wikkid/formatter/markdownformatter.py (+27/-0) wikkid/formatter/registry.py (+7/-2) wikkid/formatter/textileformatter.py (+26/-0) wikkid/skin/default/missing-directory.html (+19/-0) wikkid/skin/loader.py (+2/-0) wikkid/tests/formatters/__init__.py (+2/-0) wikkid/tests/formatters/test_markdown.py (+166/-0) wikkid/tests/formatters/test_textile.py (+121/-0) wikkid/view/base.py (+2/-1) wikkid/view/missing.py (+11/-0) wikkid/view/wiki.py (+5/-3) |
To merge this branch: | bzr merge lp:~thomir-deactivatedaccount/wikkid/wikkid-improvments |
Related bugs: | |
Related blueprints: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Tim Penhey | Pending | ||
Review via email: mp+40250@code.launchpad.net |
Commit message
Description of the change
Several small changes:
* Added wiki text formatters for markdown and textile. To use these, you must have the python-markdown and python-textile packages installed, respectively.
* User can now specify which wiki text format they want to use by default when running wikkid-serve. The default is still 'rest'.
* User can now specify which network interface to listen on. Default is still 'localhost'
* When requesting a directory that does not exist, we now give a nice page, rather than an error.
- 65. By Thomi Richards
-
Added unit tests for markdown formatter.
- 66. By Thomi Richards
-
Added unit tests for recently added textile formatter.
- 67. By Thomi Richards
-
Implemented several changes, as requested by Tim.
Mainly:
* ExecutionContext is no longer a singleton, and is passed in to all base views.* ExecutionContext class is no longer a singleton. It now derives from the dict
type, and provides attribute-based lookup as well.* Fixed indentation, module imports, docstrings and some other things.
- 68. By Thomi Richards
-
Added simple unicode tests for markdown and textile formatters.
Preview Diff
1 | === modified file 'Hacking.txt' |
2 | --- Hacking.txt 2010-07-12 19:02:14 +0000 |
3 | +++ Hacking.txt 2010-11-12 09:13:57 +0000 |
4 | @@ -29,7 +29,8 @@ |
5 | Currently there is the Bazaar based one, and a simplistic filestore that |
6 | just uses some in-memory dictionaries. |
7 | * **wikkid.formatter**: The different formatters used to render the text files |
8 | - as HTML. Currently there are formatters for Creold, ReST, and Pygments. |
9 | + as HTML. Currently there are formatters for Creold, ReST, textile, markdown |
10 | + and Pygments. |
11 | * **wikkid.interface**: All the zope interface definitions are in this module. |
12 | * **wikkid.model**: The model objects representing the different things that |
13 | you end up hitting through file traversal, like missing pages, directories, |
14 | |
15 | === modified file 'ToDo.txt' |
16 | --- ToDo.txt 2010-07-12 18:34:03 +0000 |
17 | +++ ToDo.txt 2010-11-12 09:13:57 +0000 |
18 | @@ -7,8 +7,6 @@ |
19 | |
20 | * Add in the ability to move pages |
21 | |
22 | -* Add markdown and textile formatters |
23 | - |
24 | * Lock pages as read-only (see also merging) |
25 | Is this really locked, or is it a restricted ACL? |
26 | |
27 | |
28 | === modified file 'bin/wikkid-serve' |
29 | --- bin/wikkid-serve 2010-06-13 11:12:22 +0000 |
30 | +++ bin/wikkid-serve 2010-11-12 09:13:57 +0000 |
31 | @@ -1,4 +1,5 @@ |
32 | #! /usr/bin/python |
33 | +# -*- coding: utf-8 -*- |
34 | # |
35 | # Copyright (C) 2010 Wikkid Developers. |
36 | # |
37 | @@ -23,6 +24,7 @@ |
38 | from wikkid.app import WikkidApp |
39 | from wikkid.filestore.bzr import FileStore |
40 | from wikkid.user.bzr import LocalBazaarUserMiddleware |
41 | +from wikkid.context import ExecutionContext |
42 | |
43 | |
44 | def setup_logging(): |
45 | @@ -39,11 +41,23 @@ |
46 | def main(args): |
47 | |
48 | usage = "Usage: %prog [options] <wiki-branch>" |
49 | - parser = optparse.OptionParser( |
50 | - usage=usage, description="Run a Wikkid Wiki server.", version=version) |
51 | + parser = optparse.OptionParser(usage=usage, |
52 | + description="Run a Wikkid Wiki server.", |
53 | + version=version) |
54 | + parser.add_option('--host', type='string', default='localhost', |
55 | + help='The interface to listen on. Defaults to \'localhost\'') |
56 | parser.add_option('--port', type='int', default=8080, |
57 | help='The port to listen on. Defaults to 8080.') |
58 | + parser.add_option('--default-format', type='string', default='rest', |
59 | + dest='default_formatter', |
60 | + help='Specify the default formatter to use. Defaults to \'rest\'') |
61 | options, args = parser.parse_args(sys.argv[1:]) |
62 | + |
63 | + # create execution context object, and add processed command line args to it |
64 | + ec = ExecutionContext() |
65 | + # kinda ugly, but the only way to do it. |
66 | + ec.add_context(vars(options)) |
67 | + |
68 | if len(args) == 0: |
69 | print "No branch location specified.\n" |
70 | parser.print_usage() |
71 | @@ -61,11 +75,11 @@ |
72 | logger.info('Using: %s', working_tree) |
73 | filestore = FileStore(working_tree) |
74 | |
75 | - app = WikkidApp(filestore) |
76 | + app = WikkidApp(filestore, execution_context=ec) |
77 | app = LocalBazaarUserMiddleware(app, working_tree.branch) |
78 | from wsgiref.simple_server import make_server |
79 | - httpd = make_server('localhost', options.port, app) |
80 | - logger.info('Serving on http://localhost:%s', options.port) |
81 | + httpd = make_server(options.host, options.port, app) |
82 | + logger.info('Serving on http://%s:%s', options.host, options.port) |
83 | try: |
84 | httpd.serve_forever() |
85 | except KeyboardInterrupt: |
86 | |
87 | === modified file 'wikkid/app.py' |
88 | --- wikkid/app.py 2010-07-12 18:21:42 +0000 |
89 | +++ wikkid/app.py 2010-11-12 09:13:57 +0000 |
90 | @@ -1,3 +1,4 @@ |
91 | +# -*- coding: utf-8 -*- |
92 | # |
93 | # Copyright (C) 2010 Wikkid Developers. |
94 | # |
95 | @@ -19,18 +20,26 @@ |
96 | from wikkid.model.factory import ResourceFactory |
97 | from wikkid.skin.loader import Skin |
98 | from wikkid.view.urls import parse_url |
99 | +from wikkid.fileutils import FileIterable |
100 | +from wikkid.context import ExecutionContext |
101 | |
102 | |
103 | def serve_file(filename): |
104 | if os.path.exists(filename): |
105 | basename = urlutils.basename(filename) |
106 | content_type = mimetypes.guess_type(basename)[0] |
107 | - f = open(filename, 'rb') |
108 | - try: |
109 | - return Response( |
110 | - f.read(), content_type=content_type) |
111 | - finally: |
112 | - f.close() |
113 | + |
114 | + res = Response(content_type=content_type, conditional_response=True) |
115 | + res.app_iter = FileIterable(filename) |
116 | + res.content_length = os.path.getsize(filename) |
117 | + res.last_modified = os.path.getmtime(filename) |
118 | + # Todo: is this the best value for the etag? |
119 | + # perhaps md5 would be a better alternative |
120 | + res.etag = '%s-%s-%s' % (os.path.getmtime(filename), |
121 | + os.path.getsize(filename), |
122 | + hash(filename)) |
123 | + return res |
124 | + |
125 | else: |
126 | return HTTPNotFound() |
127 | |
128 | @@ -38,7 +47,9 @@ |
129 | class WikkidApp(object): |
130 | """The main wikkid application.""" |
131 | |
132 | - def __init__(self, filestore, skin_name=None): |
133 | + def __init__(self, filestore, skin_name=None, |
134 | + execution_context = ExecutionContext()): |
135 | + self.execution_context = execution_context |
136 | self.filestore = filestore |
137 | self.resource_factory = ResourceFactory(self.filestore) |
138 | # Need to load the initial templates for the skin. |
139 | @@ -73,7 +84,7 @@ |
140 | resource_path, action = parse_url(path) |
141 | model = self.resource_factory.get_resource_at_path(resource_path) |
142 | try: |
143 | - view = get_view(model, action, request) |
144 | + view = get_view(model, action, request, self.execution_context) |
145 | response = view.render(self.skin) |
146 | except HTTPException, e: |
147 | response = e |
148 | |
149 | === added file 'wikkid/context.py' |
150 | --- wikkid/context.py 1970-01-01 00:00:00 +0000 |
151 | +++ wikkid/context.py 2010-11-12 09:13:57 +0000 |
152 | @@ -0,0 +1,43 @@ |
153 | +# -*- coding: utf-8 -*- |
154 | +# |
155 | +# Copyright (C) 2010 Wikkid Developers. |
156 | +# |
157 | +# This software is licensed under the GNU Affero General Public License |
158 | +# version 3 (see the file LICENSE). |
159 | + |
160 | +"""A means of storing execution context.""" |
161 | + |
162 | + |
163 | +class ExecutionContext(dict): |
164 | + """Store run-time execution context data. |
165 | + |
166 | + To use this class, create an instance of it, and either set key,value |
167 | + pairs just like you would a dictionary, or call add_context(dict), |
168 | + passing in a dictionary of name,value pairs that you want to _append_ |
169 | + to the execution context. |
170 | + |
171 | + To retrieve values, you can either use the classic dictionary methods, |
172 | + or access options as attribute. So, this: |
173 | + |
174 | + ec['someOption'] |
175 | + |
176 | + is equivilent to this: |
177 | + |
178 | + ec.someOption |
179 | + """ |
180 | + |
181 | + def add_context(self, contextDict): |
182 | + if type(contextDict) is not dict: |
183 | + raise TypeError, "Context must be a dictionary type." |
184 | + self.update(contextDict) |
185 | + |
186 | + def __getattr__(self, name): |
187 | + if self.has_key(name): |
188 | + return self[name] |
189 | + else: |
190 | + raise AttributeError, "ExecutionContext instance has no attribute '%s'" % (name) |
191 | + |
192 | + def __setattr__(self, name, value): |
193 | + self[name] = value |
194 | + |
195 | + |
196 | |
197 | === modified file 'wikkid/dispatcher.py' |
198 | --- wikkid/dispatcher.py 2010-06-13 11:41:41 +0000 |
199 | +++ wikkid/dispatcher.py 2010-11-12 09:13:57 +0000 |
200 | @@ -17,12 +17,13 @@ |
201 | |
202 | from zope.interface import providedBy |
203 | |
204 | +from wikkid.context import ExecutionContext |
205 | |
206 | # The view registry needs to map an Interface and a name to a class. |
207 | _VIEW_REGISTRY = {} |
208 | |
209 | |
210 | -def get_view(obj, view_name, request): |
211 | +def get_view(obj, view_name, request, ec=ExecutionContext()): |
212 | """Get the most relevant view for the object for the specified name. |
213 | |
214 | Iterate through the provided interfaces of the object and look in the view |
215 | @@ -32,7 +33,7 @@ |
216 | for interface in interfaces: |
217 | try: |
218 | klass = _VIEW_REGISTRY[(interface, view_name)] |
219 | - instance = klass(obj, request) |
220 | + instance = klass(obj, request, ec) |
221 | instance.initialize() |
222 | return instance |
223 | except KeyError: |
224 | |
225 | === added file 'wikkid/fileutils.py' |
226 | --- wikkid/fileutils.py 1970-01-01 00:00:00 +0000 |
227 | +++ wikkid/fileutils.py 2010-11-12 09:13:57 +0000 |
228 | @@ -0,0 +1,58 @@ |
229 | + # |
230 | +# Copyright (C) 2010 Wikkid Developers. |
231 | +# |
232 | +# This software is licensed under the GNU Affero General Public License |
233 | +# version 3 (see the file LICENSE). |
234 | + |
235 | +"""A method of iterating over file contents |
236 | + |
237 | +This stops us reading the entire file into memory when we serve it. |
238 | +""" |
239 | + |
240 | + |
241 | +class FileIterable(object): |
242 | + """An iterable file-like object.""" |
243 | + def __init__(self, filename, start=None, stop=None): |
244 | + self.filename = filename |
245 | + self.start = start |
246 | + self.stop = stop |
247 | + |
248 | + def __iter__(self): |
249 | + return FileIterator(self.filename, self.start, self.stop) |
250 | + |
251 | + def app_iter_range(self, start, stop): |
252 | + return self.__class__(self.filename, start, stop) |
253 | + |
254 | +class FileIterator(object): |
255 | + """Iterate over a file. |
256 | + |
257 | + FileIterable provides a simple file iterator, optionally allowing the |
258 | + user to specify start and end ranges for the file. |
259 | + """ |
260 | + chunk_size = 4096 |
261 | + def __init__(self, filename, start, stop): |
262 | + self.filename = filename |
263 | + self.fileobj = open(self.filename, 'rb') |
264 | + if start: |
265 | + self.fileobj.seek(start) |
266 | + if stop is not None: |
267 | + self.length = stop - start |
268 | + else: |
269 | + self.length = None |
270 | + |
271 | + def __iter__(self): |
272 | + return self |
273 | + |
274 | + def next(self): |
275 | + if self.length is not None and self.length <= 0: |
276 | + raise StopIteration |
277 | + chunk = self.fileobj.read(self.chunk_size) |
278 | + if not chunk: |
279 | + raise StopIteration |
280 | + if self.length is not None: |
281 | + self.length -= len(chunk) |
282 | + if self.length < 0: |
283 | + # Chop off the extra: |
284 | + chunk = chunk[:self.length] |
285 | + return chunk |
286 | + |
287 | |
288 | === added file 'wikkid/formatter/markdownformatter.py' |
289 | --- wikkid/formatter/markdownformatter.py 1970-01-01 00:00:00 +0000 |
290 | +++ wikkid/formatter/markdownformatter.py 2010-11-12 09:13:57 +0000 |
291 | @@ -0,0 +1,27 @@ |
292 | +# -*- coding: utf-8 -*- |
293 | +# |
294 | +# Copyright (C) 2010 Wikkid Developers. |
295 | +# |
296 | +# This software is licensed under the GNU Affero General Public License |
297 | +# version 3 (see the file LICENSE). |
298 | + |
299 | +"""A text to html formatter using markdown.""" |
300 | + |
301 | +import cgi |
302 | +import markdown |
303 | +from zope.interface import implements |
304 | + |
305 | +from wikkid.interface.formatter import ITextFormatter |
306 | + |
307 | + |
308 | +class MarkdownFormatter(object): |
309 | + """Format source files as HTML using markdown.""" |
310 | + |
311 | + implements(ITextFormatter) |
312 | + |
313 | + def format(self, filename, text): |
314 | + """Format the text. |
315 | + """ |
316 | + md = markdown.Markdown(safe_mode='replace') |
317 | + return md.convert(text) |
318 | + |
319 | |
320 | === modified file 'wikkid/formatter/registry.py' |
321 | --- wikkid/formatter/registry.py 2010-06-09 11:29:38 +0000 |
322 | +++ wikkid/formatter/registry.py 2010-11-12 09:13:57 +0000 |
323 | @@ -1,3 +1,4 @@ |
324 | +# -*- coding: utf-8 -*- |
325 | # |
326 | # Copyright (C) 2010 Wikkid Developers |
327 | # |
328 | @@ -11,6 +12,8 @@ |
329 | from wikkid.formatter.creoleformatter import CreoleFormatter |
330 | from wikkid.formatter.pygmentsformatter import PygmentsFormatter |
331 | from wikkid.formatter.restformatter import RestructuredTextFormatter |
332 | +from wikkid.formatter.markdownformatter import MarkdownFormatter |
333 | +from wikkid.formatter.textileformatter import TextileFormatter |
334 | |
335 | |
336 | class FormatterRegistry(object): |
337 | @@ -18,9 +21,11 @@ |
338 | |
339 | def __init__(self): |
340 | self._formatters = { |
341 | + 'creole': CreoleFormatter(), |
342 | + 'markdown': MarkdownFormatter(), |
343 | + 'pygments': PygmentsFormatter(), |
344 | 'rest': RestructuredTextFormatter(), |
345 | - 'creole': CreoleFormatter(), |
346 | - 'pygments': PygmentsFormatter() |
347 | + 'textile': TextileFormatter(), |
348 | } |
349 | |
350 | def __getitem__(self, formatter): |
351 | |
352 | === added file 'wikkid/formatter/textileformatter.py' |
353 | --- wikkid/formatter/textileformatter.py 1970-01-01 00:00:00 +0000 |
354 | +++ wikkid/formatter/textileformatter.py 2010-11-12 09:13:57 +0000 |
355 | @@ -0,0 +1,26 @@ |
356 | +# -*- coding: utf-8 -*- |
357 | +# |
358 | +# Copyright (C) 2010 Wikkid Developers. |
359 | +# |
360 | +# This software is licensed under the GNU Affero General Public License |
361 | +# version 3 (see the file LICENSE). |
362 | + |
363 | +"""A text to html formatter using textile.""" |
364 | + |
365 | + |
366 | +from textile import textile |
367 | +from zope.interface import implements |
368 | + |
369 | +from wikkid.interface.formatter import ITextFormatter |
370 | + |
371 | + |
372 | +class TextileFormatter(object): |
373 | + """Format source files as HTML using textile.""" |
374 | + |
375 | + implements(ITextFormatter) |
376 | + |
377 | + def format(self, filename, text): |
378 | + """Format the text. |
379 | + """ |
380 | + return textile(text) |
381 | + |
382 | |
383 | === added file 'wikkid/skin/default/missing-directory.html' |
384 | --- wikkid/skin/default/missing-directory.html 1970-01-01 00:00:00 +0000 |
385 | +++ wikkid/skin/default/missing-directory.html 2010-11-12 09:13:57 +0000 |
386 | @@ -0,0 +1,19 @@ |
387 | +{% extends "base.html" %} |
388 | +{% block title %}{{ view.title }}{% endblock %} |
389 | +{% block content %} |
390 | + |
391 | +<p>The directory you are looking for does not exist.<p> |
392 | + |
393 | +<p><a href="{{ canonical_url(context.root_resource, 'listing') }}"> |
394 | + Browse the wiki content |
395 | +</a></p> |
396 | + |
397 | +{% endblock %} |
398 | +{% block commands %} |
399 | +{% if view.user %} |
400 | +<a href="{{ canonical_url(context, 'edit') }}">Edit</a> |
401 | +{% endif %} |
402 | +{% if context.get_dir_name %} |
403 | +<a class="command" href="{{ canonical_url(context.root_resource, 'listing') }}">Browse</a> |
404 | +{% endif %} |
405 | +{% endblock %} |
406 | |
407 | === modified file 'wikkid/skin/loader.py' |
408 | --- wikkid/skin/loader.py 2010-05-12 10:39:13 +0000 |
409 | +++ wikkid/skin/loader.py 2010-11-12 09:13:57 +0000 |
410 | @@ -1,3 +1,4 @@ |
411 | +# -*- coding: utf-8 -*- |
412 | # |
413 | # Copyright (C) 2010 Wikkid Developers. |
414 | # |
415 | @@ -36,6 +37,7 @@ |
416 | 'edit_page': self.env.get_template('edit.html'), |
417 | 'view_directory': self.env.get_template('directory_listing.html'), |
418 | 'missing': self.env.get_template('missing-page.html'), |
419 | + 'missing-dir' : self.env.get_template('missing-directory.html') |
420 | } |
421 | module_location = urlutils.dirname(__file__) |
422 | self.dir_name = urlutils.joinpath(module_location, skin_name) |
423 | |
424 | === modified file 'wikkid/tests/formatters/__init__.py' |
425 | --- wikkid/tests/formatters/__init__.py 2010-05-16 02:40:12 +0000 |
426 | +++ wikkid/tests/formatters/__init__.py 2010-11-12 09:13:57 +0000 |
427 | @@ -15,6 +15,8 @@ |
428 | 'registry', |
429 | 'rest', |
430 | 'creole', |
431 | + 'markdown', |
432 | + 'textile', |
433 | ] |
434 | module_names = ['wikkid.tests.formatters.test_' + name for name in names] |
435 | loader = unittest.TestLoader() |
436 | |
437 | === added file 'wikkid/tests/formatters/test_markdown.py' |
438 | --- wikkid/tests/formatters/test_markdown.py 1970-01-01 00:00:00 +0000 |
439 | +++ wikkid/tests/formatters/test_markdown.py 2010-11-12 09:13:57 +0000 |
440 | @@ -0,0 +1,166 @@ |
441 | +# -*- coding: utf-8 -*- |
442 | +# Copyright (C) 2010 Wikkid Developers. |
443 | +# |
444 | +# This software is licensed under the GNU Affero General Public License |
445 | +# version 3 (see the file LICENSE). |
446 | + |
447 | +"""Tests for the wikkid.formatter.markdownformatter module.""" |
448 | + |
449 | +from textwrap import dedent |
450 | +from BeautifulSoup import BeautifulSoup |
451 | + |
452 | +from wikkid.formatter.markdownformatter import MarkdownFormatter |
453 | +from wikkid.tests import TestCase |
454 | + |
455 | + |
456 | +class TestCreoleFormatter(TestCase): |
457 | + """Tests for the creole formatter.""" |
458 | + |
459 | + def setUp(self): |
460 | + TestCase.setUp(self) |
461 | + self.formatter = MarkdownFormatter() |
462 | + |
463 | + def test_simple_headings(self): |
464 | + # A simple heading and a paragraph. |
465 | + text = dedent("""\ |
466 | + Heading 1 |
467 | + ========= |
468 | + |
469 | + Heading 2 |
470 | + --------- |
471 | + |
472 | + Simple sentence.""") |
473 | + result = self.formatter.format('filename', text) |
474 | + soup = BeautifulSoup(result) |
475 | + self.assertEqual('Heading 1', soup.h1.string) |
476 | + self.assertEqual('Heading 2', soup.h2.string) |
477 | + # we don't care about whitespace on <p> tags, and markdown inserts |
478 | + # newlines. |
479 | + self.assertEqual('Simple sentence.', soup.p.string.strip()) |
480 | + |
481 | + def test_detailed_headings(self): |
482 | + text = dedent("""\ |
483 | + # Heading 1 |
484 | + |
485 | + ## Heading 2 |
486 | + |
487 | + ### Heading 3 |
488 | + |
489 | + #### Heading 4 |
490 | + |
491 | + ##### Heading 5 |
492 | + |
493 | + ###### Heading 6""") |
494 | + result = self.formatter.format('filename', text) |
495 | + soup = BeautifulSoup(result) |
496 | + self.assertEqual('Heading 1', soup.h1.string) |
497 | + self.assertEqual('Heading 2', soup.h2.string) |
498 | + self.assertEqual('Heading 3', soup.h3.string) |
499 | + self.assertEqual('Heading 4', soup.h4.string) |
500 | + self.assertEqual('Heading 5', soup.h5.string) |
501 | + self.assertEqual('Heading 6', soup.h6.string) |
502 | + |
503 | + |
504 | + def test_inline_link(self): |
505 | + # A paragraph containing a wiki word. |
506 | + text = "A link to the [FrontPage](http://127.0.0.1) helps." |
507 | + result = self.formatter.format('filename', text) |
508 | + soup = BeautifulSoup(result) |
509 | + self.assertEqual('http://127.0.0.1', soup.a['href']) |
510 | + |
511 | + def test_reference_link(self): |
512 | + # A paragraph containing a wiki word. |
513 | + text = dedent("""\ |
514 | + This is [an example][id] reference-style link. |
515 | + |
516 | + [id]: http://127.0.0.1 |
517 | + """) |
518 | + result = self.formatter.format('filename', text) |
519 | + soup = BeautifulSoup(result) |
520 | + self.assertEqual('http://127.0.0.1', soup.a['href']) |
521 | + |
522 | + def test_emphasis(self): |
523 | + texts = ('We can have *emphasis* and **strong** as well!', |
524 | + 'We can have _emphasis_ and __strong__ as well!') |
525 | + for text in texts: |
526 | + result = self.formatter.format('filename', text) |
527 | + soup = BeautifulSoup(result) |
528 | + self.assertEqual('emphasis', soup.em.string) |
529 | + self.assertEqual('strong', soup.strong.string) |
530 | + |
531 | + def test_blockquote(self): |
532 | + text = dedent('''\ |
533 | + > This is a block quoted paragraph |
534 | + > that spans multiple lines. |
535 | + ''') |
536 | + result = self.formatter.format('filename', text) |
537 | + soup = BeautifulSoup(result) |
538 | + self.assertTrue(soup.blockquote is not None) |
539 | + |
540 | + def test_lists(self): |
541 | + text = dedent('''\ |
542 | + Some Text. |
543 | + |
544 | + * UL 1 |
545 | + * UL 2 |
546 | + * UL 3 |
547 | + |
548 | + Some More Text! |
549 | + |
550 | + 1. OL 1 |
551 | + 2. OL 2 |
552 | + 7. OL 3 |
553 | + ''') |
554 | + result = self.formatter.format('filename', text) |
555 | + soup = BeautifulSoup(result) |
556 | + |
557 | + self.assertTrue(soup.ul) |
558 | + ulNodes = soup.ul.findAll('li') |
559 | + for i in range(3): |
560 | + self.assertEqual('UL %d' % (i+1), ulNodes[i].string.strip()) |
561 | + |
562 | + self.assertTrue(soup.ol) |
563 | + olNodes = soup.ol.findAll('li') |
564 | + for i in range(3): |
565 | + self.assertEqual('OL %d' % (i+1), olNodes[i].string.strip()) |
566 | + |
567 | + def test_code_blocks(self): |
568 | + text = dedent('''\ |
569 | + Some Normal Text. |
570 | + |
571 | + Some Code inside pre tags |
572 | + |
573 | + More Normal text. |
574 | + ''') |
575 | + result = self.formatter.format('filename', text) |
576 | + soup = BeautifulSoup(result) |
577 | + self.assertEqual(soup.pre.code.string.strip(), 'Some Code inside pre tags') |
578 | + |
579 | + def test_hr(self): |
580 | + # test different HR types: |
581 | + texts = ('* * *', |
582 | + '***', |
583 | + '*********', |
584 | + '- - - -', |
585 | + '------------------') |
586 | + for text in texts: |
587 | + result = self.formatter.format('filename', text) |
588 | + soup = BeautifulSoup(result) |
589 | + self.assertTrue(soup.hr is not None) |
590 | + |
591 | + def test_code(self): |
592 | + text = 'use the `printf()` function' |
593 | + result = self.formatter.format('filename', text) |
594 | + soup = BeautifulSoup(result) |
595 | + self.assertEqual('printf()', soup.code.string) |
596 | + |
597 | + def test_unicode(self): |
598 | + # I have no idea what this says. Taken from: |
599 | + # http://www.humancomp.org/unichtm/calblur8.htm |
600 | + # It's simplified chinese, in utf-8, apparently |
601 | + text = u'个专为语文教学而设计的电脑软件' |
602 | + result = self.formatter.format('format', text) |
603 | + soup = BeautifulSoup(result) |
604 | + self.assertEqual(soup.p.string.strip(), text) |
605 | + |
606 | + |
607 | |
608 | === added file 'wikkid/tests/formatters/test_textile.py' |
609 | --- wikkid/tests/formatters/test_textile.py 1970-01-01 00:00:00 +0000 |
610 | +++ wikkid/tests/formatters/test_textile.py 2010-11-12 09:13:57 +0000 |
611 | @@ -0,0 +1,121 @@ |
612 | +# -*- coding: utf-8 -*- |
613 | +# Copyright (C) 2010 Wikkid Developers. |
614 | +# |
615 | +# This software is licensed under the GNU Affero General Public License |
616 | +# version 3 (see the file LICENSE). |
617 | + |
618 | +"""Tests for the wikkid.formatter.markdownformatter module.""" |
619 | + |
620 | +from textwrap import dedent |
621 | +from BeautifulSoup import BeautifulSoup |
622 | + |
623 | +from wikkid.formatter.textileformatter import TextileFormatter |
624 | +from wikkid.tests import TestCase |
625 | + |
626 | + |
627 | +class TestTextileFormatter(TestCase): |
628 | + """Tests for the creole formatter.""" |
629 | + |
630 | + def setUp(self): |
631 | + TestCase.setUp(self) |
632 | + self.formatter = TextileFormatter() |
633 | + |
634 | + def test_detailed_headings(self): |
635 | + text = dedent("""\ |
636 | + h1. Heading 1 |
637 | + |
638 | + h2. Heading 2 |
639 | + |
640 | + h3. Heading 3 |
641 | + |
642 | + h4. Heading 4 |
643 | + |
644 | + h5. Heading 5 |
645 | + |
646 | + h6. Heading 6""") |
647 | + result = self.formatter.format('filename', text) |
648 | + soup = BeautifulSoup(result) |
649 | + self.assertEqual('Heading 1', soup.h1.string) |
650 | + self.assertEqual('Heading 2', soup.h2.string) |
651 | + self.assertEqual('Heading 3', soup.h3.string) |
652 | + self.assertEqual('Heading 4', soup.h4.string) |
653 | + self.assertEqual('Heading 5', soup.h5.string) |
654 | + self.assertEqual('Heading 6', soup.h6.string) |
655 | + |
656 | + |
657 | + def test_inline_link(self): |
658 | + # A paragraph containing a wiki word. |
659 | + text = 'A link to the "FrontPage":http://127.0.0.1 helps.' |
660 | + result = self.formatter.format('filename', text) |
661 | + soup = BeautifulSoup(result) |
662 | + self.assertEqual('http://127.0.0.1', soup.a['href']) |
663 | + |
664 | + def test_emphasis(self): |
665 | + text = 'We can have _emphasis_ and *strong* as well!' |
666 | + result = self.formatter.format('filename', text) |
667 | + soup = BeautifulSoup(result) |
668 | + self.assertEqual('emphasis', soup.em.string) |
669 | + self.assertEqual('strong', soup.strong.string) |
670 | + |
671 | + def test_blockquote(self): |
672 | + text = dedent('''\ |
673 | + Some Text |
674 | + |
675 | + bq. This is a block quoted paragraph |
676 | + that spans multiple lines. |
677 | + |
678 | + Some more text |
679 | + ''') |
680 | + result = self.formatter.format('filename', text) |
681 | + soup = BeautifulSoup(result) |
682 | + self.assertTrue(soup.blockquote is not None) |
683 | + |
684 | + def test_lists(self): |
685 | + text = dedent('''\ |
686 | + Some Text. |
687 | + |
688 | + * UL 1 |
689 | + * UL 2 |
690 | + * UL 3 |
691 | + |
692 | + Some More Text! |
693 | + |
694 | + # OL 1 |
695 | + # OL 2 |
696 | + # OL 3 |
697 | + ''') |
698 | + result = self.formatter.format('filename', text) |
699 | + soup = BeautifulSoup(result) |
700 | + |
701 | + self.assertTrue(soup.ul) |
702 | + ulNodes = soup.ul.findAll('li') |
703 | + for i in range(3): |
704 | + self.assertEqual('UL %d' % (i+1), ulNodes[i].string.strip()) |
705 | + |
706 | + self.assertTrue(soup.ol) |
707 | + olNodes = soup.ol.findAll('li') |
708 | + for i in range(3): |
709 | + self.assertEqual('OL %d' % (i+1), olNodes[i].string.strip()) |
710 | + |
711 | + def test_code_blocks(self): |
712 | + text = dedent('''\ |
713 | + Some Normal Text. |
714 | + |
715 | + @Some Code inside pre tags@ |
716 | + |
717 | + More Normal text. |
718 | + ''') |
719 | + result = self.formatter.format('filename', text) |
720 | + soup = BeautifulSoup(result) |
721 | + self.assertEqual(soup.code.string.strip(), 'Some Code inside pre tags') |
722 | + |
723 | + def test_unicode(self): |
724 | + # I have no idea what this says. Taken from: |
725 | + # http://www.humancomp.org/unichtm/calblur8.htm |
726 | + # It's simplified chinese, in utf-8, apparently |
727 | + text = u'个专为语文教学而设计的电脑软件' |
728 | + result = self.formatter.format('format', text) |
729 | + soup = BeautifulSoup(result) |
730 | + self.assertEqual(soup.p.string.strip(), text) |
731 | + |
732 | + |
733 | |
734 | === modified file 'wikkid/view/base.py' |
735 | --- wikkid/view/base.py 2010-06-23 10:57:14 +0000 |
736 | +++ wikkid/view/base.py 2010-11-12 09:13:57 +0000 |
737 | @@ -45,7 +45,8 @@ |
738 | |
739 | __metaclass__ = BaseViewMetaClass |
740 | |
741 | - def __init__(self, context, request): |
742 | + def __init__(self, context, request, execution_context): |
743 | + self.execution_context = execution_context |
744 | self.context = context |
745 | self.request = request |
746 | if request is not None: |
747 | |
748 | === modified file 'wikkid/view/missing.py' |
749 | --- wikkid/view/missing.py 2010-05-19 10:00:43 +0000 |
750 | +++ wikkid/view/missing.py 2010-11-12 09:13:57 +0000 |
751 | @@ -1,3 +1,4 @@ |
752 | +# -*- coding: utf-8 -*- |
753 | # |
754 | # Copyright (C) 2010 Wikkid Developers |
755 | # |
756 | @@ -24,6 +25,16 @@ |
757 | def content(self): |
758 | '%s Not found' % self.path |
759 | |
760 | +class MissingDirectory(BaseView): |
761 | + #"""A wiki Directory that does not exist.""" |
762 | + |
763 | + for_interface = IMissingResource |
764 | + name = 'listing' |
765 | + template = 'missing-dir' |
766 | + |
767 | + @property |
768 | + def content(self): |
769 | + '%s Not found' % self.path |
770 | |
771 | class NewWikiPage(BaseEditView): |
772 | """Show the edit page with no existing content.""" |
773 | |
774 | === modified file 'wikkid/view/wiki.py' |
775 | --- wikkid/view/wiki.py 2010-06-17 10:12:31 +0000 |
776 | +++ wikkid/view/wiki.py 2010-11-12 09:13:57 +0000 |
777 | @@ -1,3 +1,4 @@ |
778 | +# -*- coding: utf-8 -*- |
779 | # |
780 | # Copyright (C) 2010 Wikkid Developers |
781 | # |
782 | @@ -11,6 +12,7 @@ |
783 | from wikkid.formatter.registry import get_wiki_formatter |
784 | from wikkid.interface.resource import IWikiTextFile |
785 | from wikkid.view.base import BaseView |
786 | +from wikkid.context import ExecutionContext |
787 | |
788 | |
789 | class WikiPage(BaseView): |
790 | @@ -25,9 +27,9 @@ |
791 | def content(self): |
792 | bytes = self.context.get_bytes() |
793 | # Check the first line of the content to see if it specifies a |
794 | - # formatter. The default is currently ReST, but we should have it |
795 | - # configurable shortly. |
796 | - content, formatter = get_wiki_formatter(bytes, 'rest') |
797 | + # formatter. We get the default formatter from the execution context: |
798 | + content, formatter = get_wiki_formatter(bytes, |
799 | + self.execution_context.get('default_formatter','rest')) |
800 | return formatter.format(self.context.base_name, content) |
801 | |
802 | def _render(self, skin): |