Mercurial > wikked
changeset 55:494f3c4660ed
Various changes:
- Debug Flask app is also serving wiki assets (images, etc.) in `/files`.
- Now using Mustache to render pages when there are template parameters.
- Added a more user-friendly error message for circular includes.
- Fixed initialization problems where the wiki would always initially use the
default Flask logger before we got a change to configure it.
- Fixed some problems with queries.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Mon, 04 Feb 2013 21:19:10 -0800 |
parents | 9dfbc2a40b71 |
children | 444ce19d421b |
files | manage.py tests/test_page.py wikked/formatter.py wikked/page.py wikked/templates/circular_include_error.html wikked/templates/page_error.html wikked/web.py |
diffstat | 7 files changed, 173 insertions(+), 73 deletions(-) [+] |
line wrap: on
line diff
--- a/manage.py Sat Feb 02 20:16:54 2013 -0800 +++ b/manage.py Mon Feb 04 21:19:10 2013 -0800 @@ -2,6 +2,7 @@ # Configure a simpler log format. from wikked import settings settings.LOG_FORMAT = "[%(levelname)s]: %(message)s" +settings.UPDATE_WIKI_ON_START = False # Create the app and the wiki. from wikked.web import app, wiki @@ -65,8 +66,9 @@ def get(url): """ Gets a page that matches the given URL. """ - page = wiki.getPage(url) - print page.text + with conn_scope(wiki.db): + page = wiki.getPage(url) + print page.text if __name__ == "__main__":
--- a/tests/test_page.py Sat Feb 02 20:16:54 2013 -0800 +++ b/tests/test_page.py Mon Feb 04 21:19:10 2013 -0800 @@ -85,6 +85,13 @@ second2 = Page(self.wiki, 'sub_dir/second-sibling') self.assertEqual(['sub_dir/second'], second2.local_links) + def testGenericUrl(self): + self.wiki = self._getWikiFromStructure({ + 'foo.txt': "URL: [[url:/blah/boo/image.png]]" + }) + foo = Page(self.wiki, 'foo') + self.assertEqual("URL: /files/blah/boo/image.png", foo.formatted_text) + def testPageInclude(self): self.wiki = self._getWikiFromStructure({ 'Foo.txt': "A test page.\n{{include: trans-desc}}\n",
--- a/wikked/formatter.py Sat Feb 02 20:16:54 2013 -0800 +++ b/wikked/formatter.py Mon Feb 04 21:19:10 2013 -0800 @@ -2,6 +2,7 @@ import os.path import re import types +import pystache def get_meta_name_and_modifiers(name): @@ -46,7 +47,7 @@ def urldir(self): return os.path.dirname(self.url) - def getAbsoluteUrl(self, url): + def getAbsoluteUrl(self, url, do_slugify=True): if url.startswith('/'): # Absolute page URL. abs_url = url[1:] @@ -56,7 +57,7 @@ # on Windows, so we need to convert that back. raw_abs_url = os.path.join(self.urldir, url) abs_url = os.path.normpath(raw_abs_url).replace('\\', '/') - if self.slugify is not None: + if do_slugify and self.slugify is not None: abs_url = self.slugify(abs_url) return abs_url @@ -102,8 +103,8 @@ def _processWikiMeta(self, ctx, text): def repl(m): - meta_name = str(m.group(1)).lower() - meta_value = str(m.group(3)) + meta_name = str(m.group('name')).lower() + meta_value = str(m.group('value')) if meta_value is not None and len(meta_value) > 0: if meta_name not in ctx.meta: ctx.meta[meta_name] = meta_value @@ -121,36 +122,58 @@ return self._processQuery(ctx, meta_modifier, meta_value) return '' - text = re.sub(r'^\{\{((__|\+)?[a-zA-Z][a-zA-Z0-9_\-]+):\s*(.*)\}\}\s*$', repl, text, flags=re.MULTILINE) + # Single line meta. + text = re.sub( + r'^\{\{(?P<name>(__|\+)?[a-zA-Z][a-zA-Z0-9_\-]+):\s*(?P<value>.*)\}\}\s*$', + repl, + text, + flags=re.MULTILINE) + # Multi-line meta. + text = re.sub( + r'^\{\{(?P<name>(__|\+)?[a-zA-Z][a-zA-Z0-9_\-]+):\s*(?P<value>.*)^\}\}\s*$', + repl, + text, + flags=re.MULTILINE | re.DOTALL) return text def _processWikiLinks(self, ctx, text): s = self - # [[display name|Whatever/PageName]] + # [[url:Something/Blah.ext]] def repl1(m): - return s._formatWikiLink(ctx, m.group(1), m.group(2)) - text = re.sub(r'\[\[([^\|\]]+)\|([^\]]+)\]\]', repl1, text) + url = m.group(1).strip() + if url.startswith('/'): + return '/files' + url + abs_url = os.path.join('/files', ctx.urldir, url) + abs_url = os.path.normpath(abs_url).replace('\\', '/') + return abs_url + text = re.sub(r'\[\[url\:([^\]]+)\]\]', repl1, text) + + # [[display name|Whatever/PageName]] + def repl2(m): + return s._formatWikiLink(ctx, m.group(1).strip(), m.group(2).strip()) + text = re.sub(r'\[\[([^\|\]]+)\|([^\]]+)\]\]', repl2, text) # [[Namespace/PageName]] - def repl2(m): + def repl3(m): a, b = m.group(1, 2) url = b if a is None else (a + b) return s._formatWikiLink(ctx, b, url) - text = re.sub(r'\[\[([^\]]+/)?([^\]]+)\]\]', repl2, text) + text = re.sub(r'\[\[([^\]]+/)?([^\]]+)\]\]', repl3, text) return text def _processInclude(self, ctx, modifier, value): + # Includes are run on the fly. pipe_idx = value.find('|') if pipe_idx < 0: - included_url = ctx.getAbsoluteUrl(value) + included_url = ctx.getAbsoluteUrl(value.strip()) parameters = '' else: - included_url = ctx.getAbsoluteUrl(value[:pipe_idx]) - parameters = value[pipe_idx + 1:] + included_url = ctx.getAbsoluteUrl(value[:pipe_idx].strip()) + parameters = value[pipe_idx + 1:].replace('\n', '') ctx.included_pages.append(included_url) - # Includes are run on the fly. + url_attr = ' data-wiki-url="%s"' % included_url mod_attr = '' if modifier: @@ -159,10 +182,26 @@ def _processQuery(self, ctx, modifier, query): # Queries are run on the fly. + # But we pre-process arguments that reference other pages, + # so that we get the absolute URLs right away. + processed_args = '' + arg_pattern = r"(^|\|)\s*(?P<name>[a-zA-Z][a-zA-Z0-9_\-]+)\s*="\ + r"(?P<value>[^\|]+)" + for m in re.finditer(arg_pattern, query): + name = str(m.group('name')).strip() + value = str(m.group('value')).strip() + if re.match(r'^\[\[.*\]\]$', value): + url = value[2:-2] + abs_url = ctx.getAbsoluteUrl(url) + value = '[[%s]]' % abs_url + if len(processed_args) > 0: + processed_args += '|' + processed_args += '%s=%s' % (name, value) + mod_attr = '' if modifier: mod_attr = ' data-wiki-mod="%s"' % modifier - return '<div class="wiki-query"%s>%s</div>\n' % (mod_attr, query) + return '<div class="wiki-query"%s>%s</div>\n' % (mod_attr, processed_args) def _formatWikiLink(self, ctx, display, url): abs_url = ctx.getAbsoluteUrl(url) @@ -226,11 +265,11 @@ `include` or `query`. """ default_parameters = { - 'header': "<ul>", - 'footer': "</ul>", - 'item': "<li><a class=\"wiki-link\" data-wiki-url=\"{{url}}\">" + - "{{title}}</a></li>", - 'empty': "<p>No page matches the query.</p>" + '__header': "<ul>\n", + '__footer': "</ul>\n", + '__item': "<li><a class=\"wiki-link\" data-wiki-url=\"{{url}}\">" + + "{{title}}</a></li>\n", + '__empty': "<p>No page matches the query.</p>\n" } def __init__(self, page, ctx=None): @@ -268,7 +307,7 @@ self.output.text = re.sub(r'^<div class="wiki-(?P<name>[a-z]+)"' r'(?P<opts>( data-wiki-([a-z]+)="([^"]+)")*)' r'>(?P<value>.*)</div>$', - repl, + repl, self.page.formatted_text, flags=re.MULTILINE) return self.output @@ -288,10 +327,10 @@ parameters = None if args: parameters = {} - arg_pattern = r"(^|\|)(?P<name>[a-zA-Z][a-zA-Z0-9_\-]+)=(?P<value>[^\|]+)" + arg_pattern = r"(^|\|)\s*(?P<name>[a-zA-Z][a-zA-Z0-9_\-]+)\s*=(?P<value>[^\|]+)" for m in re.finditer(arg_pattern, args): key = str(m.group('name')).lower() - parameters[key] = str(m.group('value')) + parameters[key] = m.group('value').strip() # Re-run the resolver on the included page to get its final # formatted text. @@ -317,14 +356,14 @@ # Parse the query. parameters = dict(self.default_parameters) meta_query = {} - arg_pattern = r"(^|\|)(?P<name>[a-zA-Z][a-zA-Z0-9_\-]+)="\ + arg_pattern = r"(^|\|)\s*(?P<name>[a-zA-Z][a-zA-Z0-9_\-]+)\s*="\ r"(?P<value>[^\|]+)" for m in re.finditer(arg_pattern, query): key = m.group('name').lower() - if key not in parameters: - meta_query[key] = m.group('value') + if key in parameters: + parameters[key] = str(m.group('value')) else: - parameters[key] = m.group('value') + meta_query[key] = str(m.group('value')) # Find pages that match the query, excluding any page # that is in the URL trail. @@ -338,33 +377,44 @@ # No match: return the 'empty' template. if len(matched_pages) == 0: - return parameters['empty'] + return self._valueOrPageText(parameters['__empty']) # Combine normal templates to build the output. - text = parameters['header'] + text = self._valueOrPageText(parameters['__header']) for p in matched_pages: tokens = { 'url': p.url, 'title': p.title } - text += self._renderTemplate(parameters['item'], tokens) - text += parameters['footer'] + tokens.update(p.local_meta) + text += self._renderTemplate( + self._valueOrPageText(parameters['__item']), + tokens) + text += self._valueOrPageText(parameters['__footer']) return text + def _valueOrPageText(self, value): + if re.match(r'^\[\[.*\]\]$', value): + page = self.wiki.getPage(value[2:-2]) + return page.text + return value + def _isPageMatch(self, page, name, value, level=0): # Check the page's local meta properties. actual = page.local_meta.get(name) - if ((type(actual) is list and value in actual) or - (actual == value)): + if (actual is not None and + ((type(actual) is list and value in actual) or + (actual == value))): return True # If this is an include, also look for 'include-only' # meta properties. if level > 0: actual = page.local_meta.get('+' + name) - if ((type(actual) is list and value in actual) or - (actual == value)): + if (actual is not None and + ((type(actual) is list and value in actual) or + (actual == value))): return True # Recurse into included pages. @@ -373,8 +423,9 @@ if self._isPageMatch(p, name, value, level + 1): return True + return False + def _renderTemplate(self, text, parameters): - for token, value in parameters.iteritems(): - text = text.replace('{{%s}}' % token, value) - return text + renderer = pystache.Renderer(search_dirs=[]) + return renderer.render(text, parameters)
--- a/wikked/page.py Sat Feb 02 20:16:54 2013 -0800 +++ b/wikked/page.py Mon Feb 04 21:19:10 2013 -0800 @@ -3,7 +3,8 @@ import re import datetime import unicodedata -from formatter import PageFormatter, FormattingContext, PageResolver +import pystache +from formatter import PageFormatter, FormattingContext, PageResolver, CircularIncludeError class Page(object): @@ -129,13 +130,31 @@ if self._ext_meta is not None: return - r = PageResolver(self) - out = r.run() - self._ext_meta = {} - self._ext_meta['text'] = out.text - self._ext_meta['meta'] = out.meta - self._ext_meta['links'] = out.out_links - self._ext_meta['includes'] = out.included_pages + try: + r = PageResolver(self) + out = r.run() + self._ext_meta = {} + self._ext_meta['text'] = out.text + self._ext_meta['meta'] = out.meta + self._ext_meta['links'] = out.out_links + self._ext_meta['includes'] = out.included_pages + except CircularIncludeError as cie: + template_path = os.path.join( + os.path.dirname(__file__), + 'templates', + 'circular_include_error.html' + ) + with open(template_path, 'r') as f: + template = pystache.compile(f.read()) + self._ext_meta = { + 'text': template({ + 'message': str(cie), + 'url_trail': cie.url_trail + }), + 'meta': {}, + 'links': [], + 'includes': [] + } @staticmethod def title_to_url(title):
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/templates/circular_include_error.html Mon Feb 04 21:19:10 2013 -0800 @@ -0,0 +1,9 @@ +<div class="wiki-error"> + <p>{{message}}</p> + <p>Here are the URLs we followed:</p> + <ul> + {{#url_trail}} + <li>{{.}}</li> + {{/url_trail}} + </ul> +</div>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/templates/page_error.html Mon Feb 04 21:19:10 2013 -0800 @@ -0,0 +1,1 @@ +<div class="wiki-error">{{message}}</div>
--- a/wikked/web.py Sat Feb 02 20:16:54 2013 -0800 +++ b/wikked/web.py Mon Feb 04 21:19:10 2013 -0800 @@ -1,3 +1,4 @@ +import os from flask import Flask, abort, g from wiki import Wiki, WikiParameters @@ -7,37 +8,19 @@ app.config.from_envvar('WIKKED_SETTINGS', silent=True) -def create_wiki(): - params = WikiParameters(root=app.config.get('WIKI_ROOT')) - params.logger = app.logger - wiki = Wiki(params) - wiki.start() - return wiki - -wiki = create_wiki() +# Find the wiki root. +wiki_root = app.config.get('WIKI_ROOT') +if not wiki_root: + wiki_root = os.getcwd() -# Set the wiki as a request global, and open/close the database. -@app.before_request -def before_request(): - if getattr(wiki, 'db', None): - wiki.db.open() - g.wiki = wiki - - -@app.teardown_request -def teardown_request(exception): - if wiki is not None: - if getattr(wiki, 'db', None): - wiki.db.close() - - -# Make is serve static content in DEBUG mode. +# Make the app serve static content and wiki assets in DEBUG mode. if app.config['DEBUG']: from werkzeug import SharedDataMiddleware import os app.wsgi_app = SharedDataMiddleware(app.wsgi_app, { - '/': os.path.join(os.path.dirname(__file__), 'static') + '/': os.path.join(os.path.dirname(__file__), 'static'), + '/files': os.path.join(wiki_root) }) @@ -51,6 +34,22 @@ app.logger.addHandler(handler) +# Set the wiki as a request global, and open/close the database. +# NOTE: this must happen before the login extension is registered +# because it will also add a `before_request` callback, and +# that will call our authentication handler that needs +# access to the context instance for the wiki. +@app.before_request +def before_request(): + wiki.db.open() + g.wiki = wiki + + +@app.teardown_request +def teardown_request(exception): + wiki.db.close() + + # Login extension. def user_loader(username): return g.wiki.auth.getUser(username) @@ -67,5 +66,17 @@ app.bcrypt = Bcrypt(app) +# Create the wiki. +def create_wiki(update_on_start=True): + params = WikiParameters(root=wiki_root) + params.logger = app.logger + wiki = Wiki(params) + wiki.start(update_on_start) + return wiki + +wiki = create_wiki(bool(app.config.get('UPDATE_WIKI_ON_START'))) + + # Import the views. import views +