@@ -165,6 +165,13 @@ time_format = %(_date_format)s | |||
# a gravatar. | |||
default_avatar = | |||
### | |||
# Wiki | |||
### | |||
[wiki] | |||
default_page = Main Page | |||
time_format = %(_date_format)s | |||
#------------------------------------------------------------------------------# | |||
# List of Enabled Plugins # | |||
#------------------------------------------------------------------------------# | |||
@@ -178,6 +185,7 @@ default_avatar = | |||
# like shown in the plugins section). | |||
blueprints = | |||
rophako.modules.blog | |||
rophako.modules.wiki | |||
rophako.modules.photo | |||
rophako.modules.comment | |||
rophako.modules.emoticons | |||
@@ -107,15 +107,27 @@ def exists(document): | |||
return os.path.isfile(path) | |||
def list_docs(path): | |||
def list_docs(path, recursive=False): | |||
"""List all the documents at the path.""" | |||
path = mkpath("{}/*".format(path)) | |||
root = os.path.join(Config.db.db_root, path) | |||
docs = list() | |||
for item in glob.glob(path): | |||
name = re.sub(r'\.json$', '', item) | |||
name = name.split("/")[-1] | |||
docs.append(name) | |||
for item in sorted(os.listdir(root)): | |||
target = os.path.join(root, item) | |||
db_path = os.path.join(path, item) | |||
# Descend into subdirectories? | |||
if os.path.isdir(target): | |||
if recursive: | |||
docs += [ | |||
os.path.join(item, name) for name in list_docs(db_path) | |||
] | |||
else: | |||
continue | |||
if target.endswith(".json"): | |||
name = re.sub(r'\.json$', '', item) | |||
docs.append(name) | |||
return docs | |||
@@ -0,0 +1,121 @@ | |||
# -*- coding: utf-8 -*- | |||
"""Wiki models.""" | |||
import time | |||
import re | |||
import hashlib | |||
import rophako.jsondb as JsonDB | |||
from rophako.log import logger | |||
def get_page(name): | |||
"""Get a Wiki page. Returns `None` if the page isn't found.""" | |||
name = name.strip("/") # Remove any surrounding slashes. | |||
path = "wiki/pages/{}".format(name) | |||
if not JsonDB.exists(path): | |||
return None | |||
# TODO: case insensitive page names... | |||
db = JsonDB.get(path) | |||
return db | |||
def list_pages(): | |||
"""Get a list of all existing wiki pages.""" | |||
return JsonDB.list_docs("wiki/pages", recursive=True) | |||
def edit_page(name, author, body, note, history=True): | |||
"""Write to a page.""" | |||
name = name.strip("/") # Remove any surrounding slashes. | |||
# Get the old page first. | |||
page = get_page(name) | |||
if not page: | |||
# Initialize the page. | |||
page = dict( | |||
revisions=[], | |||
) | |||
# The new revision to be added. | |||
rev = dict( | |||
id=hashlib.md5(str(int(time.time()))).hexdigest(), | |||
time=int(time.time()), | |||
author=author, | |||
body=body, | |||
note=note or "Updated the page.", | |||
) | |||
# Updating the history? | |||
if history: | |||
page["revisions"].insert(0, rev) | |||
else: | |||
# Replacing the original item. | |||
if len(page["revisions"]): | |||
page["revisions"][0] = rev | |||
else: | |||
page["revisions"].append(rev) | |||
# Write it. | |||
logger.info("Write to Wiki page {}".format(name)) | |||
JsonDB.commit("wiki/pages/{}".format(name), page) | |||
return True | |||
def delete_history(name, revision): | |||
"""Delete a revision entry from the history.""" | |||
name = name.strip("/") | |||
# Get page first. | |||
page = get_page(name) | |||
if not page: | |||
return None | |||
# Revise history. | |||
history = list() | |||
for rev in page["revisions"]: | |||
if rev["id"] == revision: | |||
logger.info("Delete history ID {} from Wiki page {}".format(revision, name)) | |||
continue | |||
history.append(rev) | |||
# Empty history = delete the page. | |||
if len(history) == 0: | |||
logger.info("Deleted last history item; Remove Wiki page {}".format(name)) | |||
return delete_page(name) | |||
page["revisions"] = history | |||
JsonDB.commit("wiki/pages/{}".format(name), page) | |||
return True | |||
def delete_page(name): | |||
"""Completely delete a wiki page.""" | |||
name = name.strip("/") | |||
path = "wiki/pages/{}".format(name) | |||
if JsonDB.exists(path): | |||
logger.info("Delete Wiki page {}".format(name)) | |||
JsonDB.delete(path) | |||
return True | |||
def name_to_url(name): | |||
"""Convert a Wiki page name into a URL safe version. | |||
All non-alphanumerics are replaced with dashes, multiple dashes in a row | |||
are flattened down to one, and any preceeding or trailing dashes are also | |||
stripped off.""" | |||
name = re.sub(r'[^A-Za-z0-9/]', '-', name).replace('--', '-').strip('-') | |||
return name | |||
def url_to_name(url): | |||
"""Convert a URL to a page name, for 404'd links. | |||
Turns a link like /wiki/New-Page-Name into 'New Page Name'""" | |||
return url.replace("-", " ") |
@@ -0,0 +1,203 @@ | |||
# -*- coding: utf-8 -*- | |||
"""Endpoints for the wiki.""" | |||
from flask import Blueprint, g, request, redirect, url_for, flash | |||
import rophako.model.user as User | |||
import rophako.model.wiki as Wiki | |||
import rophako.model.emoticons as Emoticons | |||
from rophako.utils import (template, render_markdown, pretty_time, | |||
login_required) | |||
from rophako.settings import Config | |||
mod = Blueprint("wiki", __name__, url_prefix="/wiki") | |||
@mod.route("/") | |||
def index(): | |||
"""Wiki index. Redirects to the default page from the config.""" | |||
default = Wiki.name_to_url(Config.wiki.default_page) | |||
return redirect(url_for("wiki.view_page", name=default)) | |||
@mod.route("/_pages") | |||
def list_pages(): | |||
"""Wiki page list.""" | |||
g.info["pages"] = [ | |||
{"name": name, "link": Wiki.name_to_url(name)} \ | |||
for name in Wiki.list_pages() | |||
] | |||
return template("wiki/list.html") | |||
@mod.route("/<path:name>") | |||
def view_page(name): | |||
"""Show a specific wiki page.""" | |||
name = Wiki.url_to_name(name) | |||
# Look up the page. | |||
page = Wiki.get_page(name) | |||
if not page: | |||
# Page doesn't exist... yet! | |||
g.info["title"] = Wiki.url_to_name(name) | |||
return template("wiki/missing.html"), 404 | |||
# Which revision to show? | |||
version = request.args.get("revision", None) | |||
if version: | |||
# Find this one. | |||
rev = None | |||
for item in page["revisions"]: | |||
if item["id"] == version: | |||
rev = item | |||
break | |||
if rev is None: | |||
flash("That revision was not found for this page.") | |||
rev = page["revisions"][0] | |||
else: | |||
# Show the latest one. | |||
rev = page["revisions"][0] | |||
# Render it! | |||
g.info["link"] = Wiki.name_to_url(name) | |||
g.info["title"] = name | |||
g.info["rendered_body"] = render_markdown(rev["body"]) | |||
g.info["rendered_body"] = Emoticons.render(g.info["rendered_body"]) | |||
g.info["pretty_time"] = pretty_time(Config.wiki.time_format, rev["time"]) | |||
# Author info | |||
g.info["author"] = User.get_user(uid=rev["author"]) | |||
return template("wiki/page.html") | |||
@mod.route("/<path:name>/_revisions") | |||
def history(name): | |||
"""Page history.""" | |||
name = Wiki.url_to_name(name) | |||
# Look up the page. | |||
page = Wiki.get_page(name) | |||
if not page: | |||
flash("Wiki page not found.") | |||
return redirect(url_for(".index")) | |||
authors = dict() | |||
history = list() | |||
for rev in page["revisions"]: | |||
uid = rev["author"] | |||
if not uid in authors: | |||
authors[uid] = User.get_user(uid=uid) | |||
history.append(dict( | |||
id=rev["id"], | |||
author=authors[uid], | |||
note=rev["note"], | |||
pretty_time=pretty_time(Config.wiki.time_format, rev["time"]), | |||
)) | |||
g.info["link"] = Wiki.name_to_url(name) | |||
g.info["title"] = name | |||
g.info["history"] = history | |||
return template("wiki/history.html") | |||
@mod.route("/_edit", methods=["GET", "POST"]) | |||
@login_required | |||
def edit(): | |||
"""Wiki page editor.""" | |||
title = request.args.get("name", "") | |||
body = "" | |||
history = True # Update History box is always checked by default | |||
note = request.args.get("note", "") | |||
# Editing an existing page? | |||
page = Wiki.get_page(title) | |||
if page: | |||
head = page["revisions"][0] | |||
body = head["body"] | |||
if request.method == "POST": | |||
# Submitting the form. | |||
action = request.form.get("action", "preview") | |||
title = request.form.get("name", "") | |||
body = request.form.get("body", "") | |||
history = request.form.get("history", "false") == "true" | |||
note = request.form.get("note", "") | |||
if action == "preview": | |||
# Just previewing it. | |||
g.info["preview"] = True | |||
# Render markdown | |||
g.info["rendered_body"] = render_markdown(body) | |||
# Render emoticons. | |||
g.info["rendered_body"] = Emoticons.render(g.info["rendered_body"]) | |||
elif action == "publish": | |||
# Publishing! Validate inputs. | |||
invalid = False | |||
if len(title) == 0: | |||
invalid = True | |||
flash("You must have a page title.") | |||
if len(body) == 0: | |||
invalid = True | |||
flash("You must have a page body.") | |||
if not invalid: | |||
# Update the page. | |||
Wiki.edit_page( | |||
author=g.info["session"]["uid"], | |||
name=title, | |||
body=body, | |||
note=note, | |||
history=history, | |||
) | |||
return redirect(url_for("wiki.view_page", | |||
name=Wiki.name_to_url(title) | |||
)) | |||
g.info["title"] = title | |||
g.info["body"] = body | |||
g.info["note"] = note | |||
g.info["history"] = history | |||
return template("wiki/edit.html") | |||
@mod.route("/_delete_history/<path:name>/<revision>", methods=["GET", "POST"]) | |||
@login_required | |||
def delete_revision(name, revision): | |||
"""Delete a wiki page revision from history.""" | |||
link = name | |||
name = Wiki.url_to_name(name) | |||
if request.method == "POST": | |||
Wiki.delete_history(name, revision) | |||
flash("Revision deleted.") | |||
return redirect(url_for("wiki.view_page", name=Wiki.name_to_url(name))) | |||
g.info["confirm_url"] = url_for("wiki.delete_revision", name=link, revision=revision) | |||
g.info["title"] = name | |||
g.info["type"] = "revision" | |||
return template("wiki/delete.html") | |||
@mod.route("/_delete_page/<path:name>", methods=["GET", "POST"]) | |||
@login_required | |||
def delete_page(name): | |||
"""Delete a wiki page entirely.""" | |||
link = name | |||
name = Wiki.url_to_name(name) | |||
if request.method == "POST": | |||
Wiki.delete_page(name) | |||
flash("Page completely deleted.") | |||
return redirect(url_for("wiki.index")) | |||
g.info["confirm_url"] = url_for("wiki.delete_page", name=link) | |||
g.info["title"] = name | |||
g.info["type"] = "page" | |||
return template("wiki/delete.html") |
@@ -0,0 +1,14 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Confirm Deletion{% endblock %} | |||
{% block content %} | |||
<h1>Delete {% if type == "revision" %}Revision{% else %}Page{% endif %}: {{ title }}</h1> | |||
Are you sure you want to delete this {{ type }}?<p> | |||
<form name="confirm" action="{{ confirm_url }}" method="POST"> | |||
<input type="hidden" name="token" value="{{ csrf_token() }}"> | |||
<button type="submit" name="confirm" value="true">Confirm Deletion</button> | |||
</form> | |||
{% endblock %} |
@@ -0,0 +1,39 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Edit Wiki{% endblock %} | |||
{% block content %} | |||
{% if preview %} | |||
<h1>Preview: {{ subject }}</h1> | |||
{{ rendered_body|safe }} | |||
<hr> | |||
{% endif %} | |||
<h1>Edit Wiki</h1> | |||
<form name="editor" action="{{ url_for('wiki.edit') }}" method="POST"> | |||
<input type="hidden" name="token" value="{{ csrf_token() }}"> | |||
<strong>Title:</strong><br> | |||
<input type="text" class="form-control" size="80" name="name" value="{{ title }}"><p> | |||
<strong>Body:</strong><br> | |||
<textarea class="form-control input-lg" cols="80" rows="20" name="body">{{ body }}</textarea><br> | |||
Markdown syntax. | |||
<a href="{{ url_for('emoticons.index') }}" target="_blank">Emoticon reference</a> (opens in new window)<p> | |||
<strong>Revision Note (optional):</strong><br> | |||
<input type="text" size="80" name="note" value="{{ note }}"><p> | |||
<strong>Options:</strong><br> | |||
<label> | |||
<input type="checkbox" name="history" value="true"{% if history %} checked{% endif %}> Add this revision to the page history | |||
(if unchecked, it will replace the most recent version with this version) | |||
</label><p> | |||
<button type="submit" class="btn btn-default" name="action" value="preview">Preview</button> | |||
<button type="submit" class="btn btn-primary" name="action" value="publish">Publish</button> | |||
</form> | |||
{% endblock %} |
@@ -0,0 +1,40 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Wiki{% endblock %} | |||
{% block content %} | |||
<h1>History: {{ title }}</h1> | |||
<table class="table"> | |||
<thead> | |||
<tr> | |||
<th>Date</th> | |||
<th>Edited By</th> | |||
<th>Revision Note</th> | |||
{% if session["login"] %} | |||
<th>Delete?</th> | |||
{% endif %} | |||
</tr> | |||
</thead> | |||
<tbody> | |||
{% for item in history %} | |||
<tr> | |||
<td>{{ item["pretty_time"] }}</td> | |||
<td>{{ item["author"]["name"] }}</td> | |||
<td><a href="{{ url_for('wiki.view_page', name=link, revision=item['id']) }}">{{ item["note"] }}</a></td> | |||
{% if session["login"] %} | |||
<td><a href="{{ url_for('wiki.delete_revision', name=link, revision=item['id']) }}">Delete</a></td> | |||
{% endif %} | |||
</tr> | |||
{% endfor %} | |||
</tbody> | |||
</table> | |||
{% if session["login"] %} | |||
<p> | |||
<strong>Admin Actions:</strong> | |||
<ul> | |||
<li><a href="{{ url_for('wiki.delete_page', name=link) }}">Delete Page</a></li> | |||
</ul> | |||
{% endif %} | |||
{% endblock %} |
@@ -0,0 +1,19 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Wiki Pages{% endblock %} | |||
{% block content %} | |||
{% if session["login"] %} | |||
<div class="right"> | |||
[ <a href="{{ url_for('wiki.edit') }}">New Page</a> ] | |||
</div> | |||
{% endif %} | |||
<h1>Wiki Pages</h1> | |||
<ul> | |||
{% for page in pages %} | |||
<li><a href="{{ url_for('wiki.view_page', name=page['link']) }}">{{ page['name'] }}</a></li> | |||
{% endfor %} | |||
</ul> | |||
{% endblock %} |
@@ -0,0 +1,15 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}{{ title }}{% endblock %} | |||
{% block content %} | |||
<h1>{{ title }}</h1> | |||
This wiki page does not exist yet (that means it's a 404 error!) | |||
{% if session["login"] %} | |||
<ul> | |||
<li><a href="{{ url_for('wiki.edit', name=title, note='Created initial page.') }}">Create This Page</a></li> | |||
</ul> | |||
{% endif %} | |||
{% endblock %} |
@@ -0,0 +1,21 @@ | |||
{% extends "layout.html" %} | |||
{% block title %}Wiki{% endblock %} | |||
{% block content %} | |||
<div class="right"> | |||
Last edited by {{ author["name"] }} on {{ pretty_time }} | |||
[ <a href="{{ url_for('wiki.history', name=link) }}">History</a> | |||
| <a href="{{ url_for('wiki.list_pages') }}">Index</a> | |||
{% if session["login"] %} | |||
| <a href="{{ url_for('wiki.edit', name=title) }}">Edit</a> | |||
| <a href="{{ url_for('wiki.edit') }}">New Page</a> | |||
{% endif %} | |||
] | |||
</div> | |||
<h1>{{ title }}</h1> | |||
{{ rendered_body|safe }} | |||
{% endblock %} |