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
+