10 changed files with 498 additions and 6 deletions
@ -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 %} |
Loading…
Reference in new issue