]> git.ipfire.org Git - ipfire.org.git/commitdiff
Merge branch 'master' of ssh://git.ipfire.org/pub/git/ipfire.org
authorMichael Tremer <michael.tremer@ipfire.org>
Mon, 12 Nov 2018 23:21:50 +0000 (23:21 +0000)
committerMichael Tremer <michael.tremer@ipfire.org>
Mon, 12 Nov 2018 23:21:50 +0000 (23:21 +0000)
17 files changed:
Makefile.am
src/backend/base.py
src/backend/blog.py
src/backend/wiki.py [new file with mode: 0644]
src/scss/_variables.scss
src/scss/style.scss
src/templates/base.html
src/templates/wiki/404.html [new file with mode: 0644]
src/templates/wiki/base.html [new file with mode: 0644]
src/templates/wiki/edit.html [new file with mode: 0644]
src/templates/wiki/modules/list.html [new file with mode: 0644]
src/templates/wiki/modules/navbar.html [new file with mode: 0644]
src/templates/wiki/page.html [new file with mode: 0644]
src/templates/wiki/recent-changes.html [new file with mode: 0644]
src/templates/wiki/revisions.html [new file with mode: 0644]
src/web/__init__.py
src/web/wiki.py [new file with mode: 0644]

index 6867394d74e91cb811b6f345f617dea282cf29c6..b6c8025c3462c09bdcce6e42afaea4577f491e7a 100644 (file)
@@ -66,6 +66,7 @@ backend_PYTHON = \
        src/backend/talk.py \
        src/backend/tracker.py \
        src/backend/util.py \
+       src/backend/wiki.py \
        src/backend/zeiterfassung.py
 
 backenddir = $(pythondir)/ipfire
@@ -88,7 +89,8 @@ web_PYTHON = \
        src/web/newsletter.py \
        src/web/nopaste.py \
        src/web/people.py \
-       src/web/ui_modules.py
+       src/web/ui_modules.py \
+       src/web/wiki.py
 
 webdir = $(backenddir)/web
 
@@ -251,6 +253,22 @@ templates_static_DATA = \
 
 templates_staticdir = $(templatesdir)/static
 
+templates_wiki_DATA = \
+       src/templates/wiki/404.html \
+       src/templates/wiki/base.html \
+       src/templates/wiki/edit.html \
+       src/templates/wiki/page.html \
+       src/templates/wiki/recent-changes.html \
+       src/templates/wiki/revisions.html
+
+templates_wikidir = $(templatesdir)/wiki
+
+templates_wiki_modules_DATA = \
+       src/templates/wiki/modules/list.html \
+       src/templates/wiki/modules/navbar.html
+
+templates_wiki_modulesdir = $(templates_wikidir)/modules
+
 # ------------------------------------------------------------------------------
 
 SCSS_FILES = \
index 0507d5cd86073b36d570e8482af7f90b1536600d..9e4ae21b2578626d39758f2362e4e7afb0414b7b 100644 (file)
@@ -17,6 +17,7 @@ from . import settings
 from . import talk
 
 from . import blog
+from . import wiki
 from . import zeiterfassung
 
 DEFAULT_CONFIG = io.StringIO("""
@@ -54,6 +55,7 @@ class Backend(object):
                self.talk = talk.Talk(self)
 
                self.blog = blog.Blog(self)
+               self.wiki = wiki.Wiki(self)
                self.zeiterfassung = zeiterfassung.ZeiterfassungClient(self)
 
        def read_config(self, configfile):
index 3ffcaea1618707828e49e09f135dc913e0788596..034fd4c1d5579697384460ebf04b8720aa7a8438 100644 (file)
@@ -150,7 +150,15 @@ class Blog(misc.Object):
        def _render_text(self, text, lang="markdown"):
                if lang == "markdown":
                        return markdown2.markdown(text, link_patterns=link_patterns,
-                               extras=["footnotes", "link-patterns", "wiki-tables"])
+                               extras=[
+                                       "code-friendly",
+                                       "cuddled-lists",
+                                       "fenced-code-blocks",
+                                       "footnotes",
+                                       "header-ids",
+                                       "link-patterns",
+                                       "tables",
+                               ])
 
                elif lang == "textile":
                        return textile.textile(text)
diff --git a/src/backend/wiki.py b/src/backend/wiki.py
new file mode 100644 (file)
index 0000000..4d1e8f8
--- /dev/null
@@ -0,0 +1,185 @@
+#!/usr/bin/python3
+
+import logging
+import os.path
+import re
+
+from . import misc
+from .decorators import *
+
+class Wiki(misc.Object):
+       def _get_pages(self, query, *args):
+               res = self.db.query(query, *args)
+
+               for row in res:
+                       yield Page(self.backend, row.id, data=row)
+
+       def _get_page(self, query, *args):
+               res = self.db.get(query, *args)
+
+               if res:
+                       return Page(self.backend, res.id, data=res)
+
+       def get_page_title(self, page, default=None):
+               doc = self.get_page(page)
+               if doc:
+                       return doc.title
+
+               return default
+
+       def get_page(self, page, revision=None):
+               page = Page.sanitise_page_name(page)
+               assert page
+
+               if revision:
+                       return self._get_page("SELECT * FROM wiki WHERE page = %s \
+                               AND timestamp = %s", page, revision)
+               else:
+                       return self._get_page("SELECT * FROM wiki WHERE page = %s \
+                               ORDER BY timestamp DESC LIMIT 1", page)
+
+       def get_recent_changes(self, limit=None):
+               return self._get_pages("SELECT * FROM wiki \
+                       WHERE timestamp >= NOW() - INTERVAL '4 weeks' \
+                       ORDER BY timestamp DESC LIMIT %s", limit)
+
+       def create_page(self, page, author, content, changes=None, address=None):
+               page = Page.sanitise_page_name(page)
+
+               return self._get_page("INSERT INTO wiki(page, author_uid, markdown, changes, address) \
+                       VALUES(%s, %s, %s, %s, %s) RETURNING *", page, author.uid, content or None, changes, address)
+
+       def delete_page(self, page, author, **kwargs):
+               # Do nothing if the page does not exist
+               if not self.get_page(page):
+                       return
+
+               # Just creates a blank last version of the page
+               self.create_page(page, author=author, content=None, **kwargs)
+
+       @staticmethod
+       def _split_url(url):
+               parts = list(e for e in url.split("/") if e)
+
+               num_parts = len(parts)
+               for i in range(num_parts):
+                       yield "/".join(parts[:i])
+
+       def make_breadcrumbs(self, url):
+               for part in self._split_url(url):
+                       title = self.get_page_title(part, os.path.basename(part))
+
+                       yield ("/%s" % part, title)
+
+
+class Page(misc.Object):
+       def init(self, id, data=None):
+               self.id = id
+               self.data = data
+
+       def __lt__(self, other):
+               if isinstance(other, self.__class__):
+                       if self.page == other.page:
+                               return self.timestamp < other.timestamp
+
+                       return self.page < other.page
+
+       @staticmethod
+       def sanitise_page_name(page):
+               if not page:
+                       return "/"
+
+               # Make sure that the page name does NOT end with a /
+               if page.endswith("/"):
+                       page = page[:-1]
+
+               # Make sure the page name starts with a /
+               if not page.startswith("/"):
+                       page = "/%s" % page
+
+               # Remove any double slashes
+               page = page.replace("//", "/")
+
+               return page
+
+       @property
+       def url(self):
+               return self.page
+
+       @property
+       def page(self):
+               return self.data.page
+
+       @property
+       def title(self):
+               return self._title or self.page[1:]
+
+       @property
+       def _title(self):
+               if not self.markdown:
+                       return
+
+               # Find first H1 headline in markdown
+               markdown = self.markdown.splitlines()
+
+               m = re.match(r"^# (.*)( #)?$", markdown[0])
+               if m:
+                       return m.group(1)
+
+       @lazy_property
+       def author(self):
+               if self.data.author_uid:
+                       return self.backend.accounts.get_by_uid(self.data.author_uid)
+
+       def _render(self, text):
+               logging.debug("Rendering %s" % self)
+
+               # Borrow this from the blog
+               return self.backend.blog._render_text(text, lang="markdown")
+
+       @property
+       def markdown(self):
+               return self.data.markdown
+
+       @property
+       def html(self):
+               return self.data.html or self._render(self.markdown)
+
+       @property
+       def timestamp(self):
+               return self.data.timestamp
+
+       def was_deleted(self):
+               return self.markdown is None
+
+       @lazy_property
+       def breadcrumbs(self):
+               return self.backend.wiki.make_breadcrumbs(self.page)
+
+       def get_latest_revision(self):
+               revisions = self.get_revisions()
+
+               # Return first object
+               for rev in revisions:
+                       return rev
+
+       def get_revisions(self):
+               return self.backend.wiki._get_pages("SELECT * FROM wiki \
+                       WHERE page = %s ORDER BY timestamp DESC", self.page)
+
+       @property
+       def changes(self):
+               return self.data.changes
+
+       # Sidebar
+
+       @lazy_property
+       def sidebar(self):
+               parts = self.page.split("/")
+
+               while parts:
+                       sidebar = self.backend.wiki.get_page(os.path.join(*parts, "sidebar"))
+                       if sidebar:
+                               return sidebar
+
+                       parts.pop()
index 5488a0df560258dedaad36b52b9f20548a03cc4b..c4ed83fa2b2dbc4491b0833eff3d11f3e9083100 100644 (file)
@@ -88,3 +88,6 @@ $btn-padding-y:                               .5rem;
 
 // Progress
 $progress-height:                      1rem * $line-height-base;
+
+// Breadcrumbs
+$breadcrumb-bg:                                white;
index 1cb803216d4516c0350912da7b71f73a645e36b7..e6522a0a02eb8c81363cd64779529492b302fc73 100644 (file)
@@ -22,6 +22,7 @@
 @import "../bootstrap/scss/navbar";
 @import "../bootstrap/scss/card";
 @import "../bootstrap/scss/pagination";
+@import "../bootstrap/scss/breadcrumb";
 @import "../bootstrap/scss/badge";
 @import "../bootstrap/scss/alert";
 @import "../bootstrap/scss/progress";
@@ -239,6 +240,47 @@ section {
        }
 }
 
+.wiki-content {
+       h1, h2, h3, h4, h5, h6 {
+               font-weight: $headings-font-weight;
+               line-height: $headings-line-height;
+               margin-bottom: 0.25rem;
+       }
+
+       h1 {
+               font-size: $h4-font-size;
+       }
+
+       h2 {
+               font-size: $h5-font-size;
+       }
+
+       h3, h4, h5, h6 {
+               font-size: $h6-font-size;
+       }
+
+       img {
+               @include img-fluid;
+
+               // Center all images
+               display: block;
+               margin-left: auto;
+               margin-right: auto;
+
+               // Add some extra margin to the top & bottom
+               padding: ($spacer * 2) 0 ($spacer * 2) 0;
+       }
+
+       blockquote {
+               @extend .blockquote;
+       }
+
+       table {
+               @extend .table;
+               @extend .table-sm;
+       }
+}
+
 hr.divider {
        border-color: rgba($dark, .15);
        margin-top: 2rem;
index 4f529f397013249770ed7514b7e3a80251b636a8..767d2f42c42286f46a42129eeb46d671512f0bdf 100644 (file)
@@ -29,6 +29,8 @@
                                                {{ _("Mirrors") }}
                                        {% elif hostname == "people.ipfire.org" %}
                                                {{ _("People") }}
+                                       {% elif hostname == "wiki.ipfire.org" %}
+                                               {{ _("Wiki") }}
                                        {% end %}
                                </a>
 
                                                                </form>
                                                        </div>
                                                {% end %}
+                                       {% elif hostname == "wiki.ipfire.org" %}
+                                               <button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbar"
+                                                               aria-controls="navbar" aria-expanded="false" aria-label="Toggle navigation">
+                                                       <span class="fas fa-bars"></span>
+                                               </button>
+
+                                               <div class="collapse navbar-collapse" id="navbar">
+                                                       <ul class="navbar-nav ml-auto mr-3">
+                                                               <li class="nav-item">
+                                                                       <a class="nav-link {% if request.path == "/recent-changes" %}active{% end %}" href="/recent-changes">
+                                                                               {{ _("Recent Changes") }}
+                                                                       </a>
+                                                               </li>
+                                                       </ul>
+
+                                                       <form class="form-inline my-2 my-lg-0" action="/search" method="GET">
+                                                               <input class="form-control form-control-sm" type="search" name="q"
+                                                                       placeholder="{{ _("Search...") }}" aria-label="{{ _("Search") }}" value="{% try %}{{ q }}{% except %}{% end %}">
+                                                       </form>
+
+                                                       <a class="btn btn-primary ml-lg-2" href="https://www.ipfire.org/donate">
+                                                               <span class="fas fa-heart"></span> {{ _("Donate") }}
+                                                       </a>
+                                               </div>
                                        {% end %}
                                {% end block %}
                        </div>
                                                                                        <div class="col">
                                                                                                <ul class="list-unstyled">
                                                                                                        <li>
-                                                                                                               <a href="https://www.ipfire.org/features">{{ _("Features") }}</a>
+                                                                                                               <a href="/features">{{ _("Features") }}</a>
                                                                                                        </li>
 
                                                                                                        <li>
-                                                                                                               <a href="https://downloads.ipfire.org">{{ _("Download") }}</a>
+                                                                                                               <a href="/download">{{ _("Download") }}</a>
                                                                                                        </li>
 
                                                                                                        <li>
-                                                                                                               <a href="https://www.ipfire.org/support">{{ _("Support") }}</a>
+                                                                                                               <a href="/support">{{ _("Support") }}</a>
                                                                                                        </li>
                                                                                                </ul>
                                                                                        </div>
                                                                                                        </li>
 
                                                                                                        <li>
-                                                                                                               <a href="https://www.ipfire.org/legal">{{ _("Legal") }}</a>
+                                                                                                               <a href="/legal">{{ _("Legal") }}</a>
                                                                                                        </li>
                                                                                                </ul>
                                                                                        </div>
                                                                        </div>
 
                                                                        <div class="col-12 col-lg-4 text-center mb-4">
-                                                                               <a class="btn btn-primary btn-lg px-4 my-4" href="https://www.ipfire.org/donate">
+                                                                               <a class="btn btn-primary btn-lg px-4 my-4" href="/donate">
                                                                                        <span class="fas fa-heart"></span> {{ _("Donate") }}
                                                                                </a>
 
                                                <div class="container d-flex justify-content-between">
                                                        &copy; {{ year }} - IPFire - {{ _("The Open Source Firewall") }}
 
-                                                       {% if not current_user and hostname in ("blog.ipfire.org", "nopaste.ipfire.org") %}
+                                                       {% if not current_user and hostname in ("blog.ipfire.org", "nopaste.ipfire.org", "wiki.ipfire.org") %}
                                                                <a href="/login">{{ _("Login") }}</a>
                                                        {% elif current_user %}
                                                                <span>
diff --git a/src/templates/wiki/404.html b/src/templates/wiki/404.html
new file mode 100644 (file)
index 0000000..b09d76f
--- /dev/null
@@ -0,0 +1,25 @@
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Page Not Found") }}{% end block %}
+
+{% block container %}
+    <section>
+        <div class="container">
+            <div class="row justify-content-center mt-5">
+                <div class="col col-md-7">
+                    <h5 class="mb-0">{{ _("Error 404") }}</h5>
+
+                    <h2>{{ _("This Page Does Not Exist") }}</h2>
+
+                    <p>
+                        {{ _("This wiki page does not exist, yet.") }}
+                    </p>
+
+                    <a class="btn btn-primary btn-block" href="{{ request.path }}?action=edit">
+                        {{ _("Create Now") }}
+                    </a>
+                </div>
+            </div>
+        </div>
+    </section>
+{% end block %}
diff --git a/src/templates/wiki/base.html b/src/templates/wiki/base.html
new file mode 100644 (file)
index 0000000..dd35246
--- /dev/null
@@ -0,0 +1,13 @@
+{% extends "../base.html" %}
+
+{% block content %}
+       <div class="row">
+               <div class="col-12 col-lg-3 d-none d-lg-block">
+                       {% block sidebar %}{% end block %}
+               </div>
+
+               <div class="col-12 col-lg-9">
+                       {% block main %}{% end block %}
+               </div>
+       </div>
+{% end block %}
diff --git a/src/templates/wiki/edit.html b/src/templates/wiki/edit.html
new file mode 100644 (file)
index 0000000..34a71e4
--- /dev/null
@@ -0,0 +1,35 @@
+{% extends "base.html" %}
+
+{% block title %}{% if page %}{{ _("Edit %s") % page.title }}{% else %}{{ _("Create A New Page") }}{% end %}{% end block %}
+
+{% block main %}
+       <div class="card">
+               <div class="card-body">
+                       <h4 class="card-title">
+                               {% if page %}{{ _("Edit %s") % page.title }}{% else %}{{ _("Create A New Page") }}{% end %}
+                       </h4>
+
+                       <form action="" method="POST">
+                               {% raw xsrf_form_html() %}
+
+                               <input type="hidden" name="page" value="{{ request.path }}">
+
+                               <div class="form-group">
+                                       <textarea class="form-control" rows="16" name="content" placeholder="{{ _("Text") }}"
+                                               >{% if page and page.markdown %}{{ page.markdown }}{% end %}</textarea>
+                               </div>
+
+                               <div class="form-group row">
+                                       <label class="col-sm-3 col-form-label">{{ _("What has changed?") }}</label>
+                                       <div class="col-sm-9">
+                                               <input type="text" class="form-control" name="changes" required>
+                                       </div>
+                               </div>
+
+                               <button type="submit" class="btn btn-primary btn-block">
+                                       {% if page %}{{ _("Save Page") }}{% else %}{{ _("Create Page") }}{% end %}
+                               </button>
+                       </form>
+               </div>
+       </div>
+{% end block %}
diff --git a/src/templates/wiki/modules/list.html b/src/templates/wiki/modules/list.html
new file mode 100644 (file)
index 0000000..f7a67c1
--- /dev/null
@@ -0,0 +1,24 @@
+{% for page in pages %}
+       <strong class="mb-0">
+               {% if show_breadcrumbs %}
+                       {% for url, title in page.breadcrumbs %}
+                               <a href="{{ url }}">{{ title }}</a> /
+                       {% end %}
+               {% end %}
+
+               <a href="{{ page.url }}{% if link_revision %}?revision={{ page.timestamp.isoformat() }}{% end %}">{{ page.title }}</a>
+       </strong>
+
+       <p class="text-muted small">
+               {{ locale.format_date(page.timestamp, shorter=True, relative=False) }}
+
+               {% if page.author %}
+                       {{ _("by") }}
+                       <a href="/users/{{ page.author.uid }}">{{ page.author }}</a>
+               {% end %}
+
+               {% if show_changes and page.changes %}
+                       &bull; {{ page.changes }}
+               {% end %}
+       </p>
+{% end %}
diff --git a/src/templates/wiki/modules/navbar.html b/src/templates/wiki/modules/navbar.html
new file mode 100644 (file)
index 0000000..06d05ec
--- /dev/null
@@ -0,0 +1,15 @@
+<ol class="breadcrumb">
+       {% for page, title in breadcrumbs %}
+               <li class="breadcrumb-item">
+                       <a href="{{ page }}">{{ title }}</a>
+               </li>
+       {% end %}
+
+       <li class="breadcrumb-item {% if not suffix %}active{% end %}">
+               {{ page_title }}
+       </li>
+
+       {% if suffix %}
+               <li class="breadcrumb-item active">{{ suffix }}</li>
+       {% end %}
+</ol>
diff --git a/src/templates/wiki/page.html b/src/templates/wiki/page.html
new file mode 100644 (file)
index 0000000..17f24f4
--- /dev/null
@@ -0,0 +1,35 @@
+{% extends "base.html" %}
+
+{% block title %}{{ page.title }}{% end block %}
+
+{% block sidebar %}
+       {% if page.sidebar %}
+               <div class="wiki-content">
+                       {% raw page.sidebar.html %}
+               </div>
+       {% end %}
+{% end block %}
+
+{% block main %}
+       {% if not request.path == "/" %}
+               {% module WikiNavbar(page) %}
+       {% end %}
+
+       <div class="card mb-3">
+               <div class="card-body wiki-content">
+                       {% raw page.html %}
+               </div>
+       </div>
+
+       <p class="small text-right">
+               {{ _("Last modified %s") % locale.format_date(page.timestamp) }}
+
+               {% if page.author %}
+                       &bull;
+
+                       <a href="/users/{{ page.author.uid }}">
+                               {{ page.author }}
+                       </a>
+               {% end %}
+       </p>
+{% end block %}
diff --git a/src/templates/wiki/recent-changes.html b/src/templates/wiki/recent-changes.html
new file mode 100644 (file)
index 0000000..9cb939b
--- /dev/null
@@ -0,0 +1,21 @@
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Recent Changes") }}{% end block %}
+
+{% block content %}
+       <section>
+               <div class="container">
+                       <div class="row">
+                               <div class="col col-lg-6">
+                                       <h1>{{ _("Recent Changes") }}</h1>
+                               </div>
+                       </div>
+               </div>
+       </section>
+
+       <div class="card">
+               <div class="card-body">
+                       {% module WikiList(recent_changes, show_changes=True) %}
+               </div>
+       </div>
+{% end block %}
diff --git a/src/templates/wiki/revisions.html b/src/templates/wiki/revisions.html
new file mode 100644 (file)
index 0000000..524813d
--- /dev/null
@@ -0,0 +1,17 @@
+{% extends "page.html" %}
+
+{% block title %}{{ _("Revisions of %s") % page.title }}{% end block %}
+
+{% block main %}
+       {% if not request.path == "/" %}
+               {% module WikiNavbar(page) %}
+       {% end %}
+
+       <div class="card mb-3">
+               <div class="card-body">
+            <h4 class="card-title">{{ _("Revisions of %s") % page.title }}</h4>
+
+                       {% module WikiList(page.get_revisions(), show_breadcrumbs=False, link_revision=True) %}
+               </div>
+       </div>
+{% end block %}
index d7c2407bf93bf9a5b2769c103bf6f11701595a2b..f961c3bfa91dfd59285875739d8ff40570241c28 100644 (file)
@@ -27,6 +27,7 @@ from . import newsletter
 from . import nopaste
 from . import people
 from . import ui_modules
+from . import wiki
 
 class Application(tornado.web.Application):
        def __init__(self, config, **kwargs):
@@ -88,6 +89,10 @@ class Application(tornado.web.Application):
                                "FireinfoDeviceAndGroupsTable"
                                                       : fireinfo.DeviceAndGroupsTableModule,
 
+                               # Wiki
+                               "WikiNavbar"           : wiki.WikiNavbarModule,
+                               "WikiList"             : wiki.WikiListModule,
+
                                # Misc
                                "Map"                  : ui_modules.MapModule,
                                "ProgressBar"          : ui_modules.ProgressBarModule,
@@ -275,6 +280,18 @@ class Application(tornado.web.Application):
                        (r"/users/(\w+)/sip", people.SIPHandler),
                ]  + authentication_handlers)
 
+               # wiki.ipfire.org
+               self.add_handlers(r"wiki(\.dev)?\.ipfire\.org",
+                       authentication_handlers + [
+
+                       # Handlers
+                       (r"/recent\-changes", wiki.RecentChangesHandler),
+                       (r"/search", wiki.SearchHandler),
+
+                       # Render pages
+                       (r"([A-Za-z0-9\-_\/]+)?", wiki.PageHandler),
+               ])
+
                # ipfire.org
                self.add_handlers(r"ipfire\.org", [
                        (r".*", tornado.web.RedirectHandler, { "url" : "https://www.ipfire.org" })
diff --git a/src/web/wiki.py b/src/web/wiki.py
new file mode 100644 (file)
index 0000000..12aa9c0
--- /dev/null
@@ -0,0 +1,106 @@
+#!/usr/bin/python3
+
+import tornado.web
+
+from . import auth
+from . import base
+from . import ui_modules
+
+class PageHandler(auth.CacheMixin, base.BaseHandler):
+       @property
+       def action(self):
+               return self.get_argument("action", None)
+
+       def write_error(self, status_code, **kwargs):
+               # Render a custom page for 404
+               if status_code == 404:
+                       self.render("wiki/404.html", **kwargs)
+                       return
+
+               # Otherwise raise this to one layer above
+               super().write_error(status_code, **kwargs)
+
+       @tornado.web.removeslash
+       def get(self, page):
+               # Check if we are asked to render a certain revision
+               revision = self.get_argument("revision", None)
+
+               # Fetch the wiki page
+               page = self.backend.wiki.get_page(page, revision=revision)
+
+               # Edit
+               if self.action == "edit":
+                       if not self.current_user:
+                               raise tornado.web.HTTPError(401)
+
+                       # Empty page if it was deleted
+                       if page and page.was_deleted():
+                               page = None
+
+                       # Render page
+                       self.render("wiki/edit.html", page=page)
+                       return
+
+               # Revisions
+               elif self.action == "revisions":
+                       self.render("wiki/revisions.html", page=page)
+                       return
+
+               # If the page does not exist, we send 404
+               if not page or page.was_deleted():
+                       raise tornado.web.HTTPError(404)
+
+               # Fetch the latest revision
+               latest_revision = page.get_latest_revision()
+
+               # Render page
+               self.render("wiki/page.html", page=page, latest_revision=latest_revision)
+
+       @tornado.web.authenticated
+       def post(self, page):
+               content = self.get_argument("content", None)
+               changes = self.get_argument("changes")
+
+               # Create a new page in the database
+               with self.db.transaction():
+                       page = self.backend.wiki.create_page(page,
+                               self.current_user, content, changes=changes, address=self.get_remote_ip())
+
+               # Redirect back
+               if page.was_deleted():
+                       self.redirect("/")
+               else:
+                       self.redirect(page.url)
+
+
+class SearchHandler(auth.CacheMixin, base.BaseHandler):
+       @base.blacklisted
+       def get(self):
+               q = self.get_argument("q")
+
+               pages = self.backend.wiki.search(q, limit=50)
+               if not pages:
+                       raise tornado.web.HTTPError(404, "Nothing found")
+
+               self.render("wiki/search-results.html", q=q, pages=pages)
+
+
+class RecentChangesHandler(auth.CacheMixin, base.BaseHandler):
+       def get(self):
+               recent_changes = self.backend.wiki.get_recent_changes(limit=50)
+
+               self.render("wiki/recent-changes.html", recent_changes=recent_changes)
+
+
+class WikiListModule(ui_modules.UIModule):
+       def render(self, pages, link_revision=False, show_breadcrumbs=True, show_changes=False):
+               return self.render_string("wiki/modules/list.html", link_revision=link_revision,
+                       pages=pages, show_breadcrumbs=show_breadcrumbs, show_changes=show_changes)
+
+
+class WikiNavbarModule(ui_modules.UIModule):
+       def render(self, page, suffix=None):
+               breadcrumbs = self.backend.wiki.make_breadcrumbs(page.url)
+
+               return self.render_string("wiki/modules/navbar.html",
+                       breadcrumbs=breadcrumbs, page_title=page.title, suffix=suffix)