From 6fdf0ae0eaf496a258dd0d8ddb3f01085cdf508a Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sun, 25 Jan 2015 01:32:38 -0800 Subject: [PATCH] Add Wiki plugin to Rophako --- defaults.ini | 8 + rophako/jsondb.py | 24 ++- rophako/model/wiki.py | 121 +++++++++++ rophako/modules/wiki/__init__.py | 203 ++++++++++++++++++ .../modules/wiki/templates/wiki/delete.html | 14 ++ rophako/modules/wiki/templates/wiki/edit.html | 39 ++++ .../modules/wiki/templates/wiki/history.html | 40 ++++ rophako/modules/wiki/templates/wiki/list.html | 19 ++ .../modules/wiki/templates/wiki/missing.html | 15 ++ rophako/modules/wiki/templates/wiki/page.html | 21 ++ 10 files changed, 498 insertions(+), 6 deletions(-) create mode 100644 rophako/model/wiki.py create mode 100644 rophako/modules/wiki/__init__.py create mode 100644 rophako/modules/wiki/templates/wiki/delete.html create mode 100644 rophako/modules/wiki/templates/wiki/edit.html create mode 100644 rophako/modules/wiki/templates/wiki/history.html create mode 100644 rophako/modules/wiki/templates/wiki/list.html create mode 100644 rophako/modules/wiki/templates/wiki/missing.html create mode 100644 rophako/modules/wiki/templates/wiki/page.html diff --git a/defaults.ini b/defaults.ini index 5f43759..9d557af 100644 --- a/defaults.ini +++ b/defaults.ini @@ -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 diff --git a/rophako/jsondb.py b/rophako/jsondb.py index ba1028a..dd9ba6f 100644 --- a/rophako/jsondb.py +++ b/rophako/jsondb.py @@ -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 diff --git a/rophako/model/wiki.py b/rophako/model/wiki.py new file mode 100644 index 0000000..5e43237 --- /dev/null +++ b/rophako/model/wiki.py @@ -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("-", " ") diff --git a/rophako/modules/wiki/__init__.py b/rophako/modules/wiki/__init__.py new file mode 100644 index 0000000..85aab63 --- /dev/null +++ b/rophako/modules/wiki/__init__.py @@ -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("/") +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("//_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//", 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/", 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") diff --git a/rophako/modules/wiki/templates/wiki/delete.html b/rophako/modules/wiki/templates/wiki/delete.html new file mode 100644 index 0000000..43bc4c8 --- /dev/null +++ b/rophako/modules/wiki/templates/wiki/delete.html @@ -0,0 +1,14 @@ +{% extends "layout.html" %} +{% block title %}Confirm Deletion{% endblock %} +{% block content %} + +

Delete {% if type == "revision" %}Revision{% else %}Page{% endif %}: {{ title }}

+ +Are you sure you want to delete this {{ type }}?

+ +

+ + +
+ +{% endblock %} diff --git a/rophako/modules/wiki/templates/wiki/edit.html b/rophako/modules/wiki/templates/wiki/edit.html new file mode 100644 index 0000000..a32d51f --- /dev/null +++ b/rophako/modules/wiki/templates/wiki/edit.html @@ -0,0 +1,39 @@ +{% extends "layout.html" %} +{% block title %}Edit Wiki{% endblock %} +{% block content %} + +{% if preview %} +

Preview: {{ subject }}

+ + {{ rendered_body|safe }} + +
+{% endif %} + +

Edit Wiki

+ +
+ + + Title:
+

+ + Body:
+
+ Markdown syntax. + Emoticon reference (opens in new window)

+ + Revision Note (optional):
+

+ + Options:
+

+ + + +

+ +{% endblock %} diff --git a/rophako/modules/wiki/templates/wiki/history.html b/rophako/modules/wiki/templates/wiki/history.html new file mode 100644 index 0000000..d3d781d --- /dev/null +++ b/rophako/modules/wiki/templates/wiki/history.html @@ -0,0 +1,40 @@ +{% extends "layout.html" %} +{% block title %}Wiki{% endblock %} +{% block content %} + +

History: {{ title }}

+ + + + + + + + {% if session["login"] %} + + {% endif %} + + + + {% for item in history %} + + + + + {% if session["login"] %} + + {% endif %} + + {% endfor %} + +
DateEdited ByRevision NoteDelete?
{{ item["pretty_time"] }}{{ item["author"]["name"] }}{{ item["note"] }}Delete
+ +{% if session["login"] %} +

+ Admin Actions: +

+{% endif %} + +{% endblock %} diff --git a/rophako/modules/wiki/templates/wiki/list.html b/rophako/modules/wiki/templates/wiki/list.html new file mode 100644 index 0000000..4283d9b --- /dev/null +++ b/rophako/modules/wiki/templates/wiki/list.html @@ -0,0 +1,19 @@ +{% extends "layout.html" %} +{% block title %}Wiki Pages{% endblock %} +{% block content %} + +{% if session["login"] %} +
+ [ New Page ] +
+{% endif %} + +

Wiki Pages

+ + + +{% endblock %} diff --git a/rophako/modules/wiki/templates/wiki/missing.html b/rophako/modules/wiki/templates/wiki/missing.html new file mode 100644 index 0000000..68c3900 --- /dev/null +++ b/rophako/modules/wiki/templates/wiki/missing.html @@ -0,0 +1,15 @@ +{% extends "layout.html" %} +{% block title %}{{ title }}{% endblock %} +{% block content %} + +

{{ title }}

+ +This wiki page does not exist yet (that means it's a 404 error!) + +{% if session["login"] %} + +{% endif %} + +{% endblock %} diff --git a/rophako/modules/wiki/templates/wiki/page.html b/rophako/modules/wiki/templates/wiki/page.html new file mode 100644 index 0000000..0ba93c7 --- /dev/null +++ b/rophako/modules/wiki/templates/wiki/page.html @@ -0,0 +1,21 @@ +{% extends "layout.html" %} +{% block title %}Wiki{% endblock %} +{% block content %} + +
+ Last edited by {{ author["name"] }} on {{ pretty_time }} + + [ History + | Index + {% if session["login"] %} + | Edit + | New Page + {% endif %} + ] +
+ +

{{ title }}

+ +{{ rendered_body|safe }} + +{% endblock %}