aboutsummaryrefslogtreecommitdiffstats
path: root/pysite
diff options
context:
space:
mode:
authorGravatar Leon Sandøy <[email protected]>2018-04-18 16:59:29 +0200
committerGravatar Leon Sandøy <[email protected]>2018-04-18 16:59:29 +0200
commit2f61514e999c08361b29bf9f0c1f1fb044d3f6e0 (patch)
tree433471571ba608ea5d0bbe89c99e75a260920630 /pysite
parentAdding Pinkie Pie to the hiphoppers list (diff)
parentPass can_edit into revisions list (diff)
Merge branch 'master' of github.com:discord-python/site
Diffstat (limited to 'pysite')
-rw-r--r--pysite/constants.py6
-rw-r--r--pysite/database.py3
-rw-r--r--pysite/database/table_init/wiki_revisions.json1
-rw-r--r--pysite/views/wiki/edit.py133
-rw-r--r--pysite/views/wiki/history/compare.py69
-rw-r--r--pysite/views/wiki/history/show.py42
6 files changed, 246 insertions, 8 deletions
diff --git a/pysite/constants.py b/pysite/constants.py
index 737f3a7a..ca18a288 100644
--- a/pysite/constants.py
+++ b/pysite/constants.py
@@ -80,3 +80,9 @@ DATADOG_PORT = int(environ.get("DATADOG_PORT") or 0)
# CSRF
CSRF = CSRFProtect()
+
+# GitHub Token
+GITHUB_TOKEN = environ.get("GITHUB_TOKEN") or None
+
+# Audit Webhook
+WIKI_AUDIT_WEBHOOK = environ.get("WIKI_AUDIT_WEBHOOK") or None
diff --git a/pysite/database.py b/pysite/database.py
index 18a9dc0d..8903fbf4 100644
--- a/pysite/database.py
+++ b/pysite/database.py
@@ -18,7 +18,8 @@ ALL_TABLES = {
"oauth_data": "id",
"tags": "tag_name",
"users": "user_id",
- "wiki": "slug"
+ "wiki": "slug",
+ "wiki_revisions": "id"
}
diff --git a/pysite/database/table_init/wiki_revisions.json b/pysite/database/table_init/wiki_revisions.json
new file mode 100644
index 00000000..09675889
--- /dev/null
+++ b/pysite/database/table_init/wiki_revisions.json
@@ -0,0 +1 @@
+[{"date":1523552033.199194,"id":"1e701916-6504-4c93-950a-09c274610447","post":{"headers":[],"html":"<div class=\"document\">\n<p>lol dab</p>\n</div>\n","rst":"lol dab","slug":"lol","title":"Test test"},"slug":"lol","user":"165023948638126080"},{"date":1523563438.478759,"id":"0237eae4-01d9-4b63-a644-9b9cdcd77f02","post":{"headers":[],"html":"<div class=\"document\">\n<p>Hello world.</p>\n<p>Python is good.</p>\n<p>Thank you.</p>\n<dl class=\"field-list simple\">\n<dt>dabward</dt>\n<dd><p></p></dd>\n</dl>\n<pre class=\"code rust literal-block\"><code><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">myvar</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"nb\">String</span>::<span class=\"n\">from</span><span class=\"p\">(</span><span class=\"s\">&quot;World&quot;</span><span class=\"p\">);</span><span class=\"w\">\n</span><span class=\"n\">println</span><span class=\"o\">!</span><span class=\"p\">(</span><span class=\"s\">&quot;Hello {}&quot;</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">myvar</span><span class=\"p\">)</span></code></pre>\n<p>lol</p>\n</div>\n","rst":"Hello world.\r\n\r\nPython is good.\r\n\r\nThank you.\r\n\r\n:dabward:\r\n\r\n.. code:: rust\r\n\r\n let myvar = String::from(\"World\");\r\n println!(\"Hello {}\", myvar)\r\n \r\nlol","title":"My Lovely Page"},"slug":"mypage","user":"165023948638126080"},{"date":1523552320.439601,"id":"4d9c0dee-56a0-40f7-893d-0722176f650c","post":{"headers":[{"id":"#hello","title":"Hello"},{"id":"#world","title":"World"}],"html":"<div class=\"document\">\n\n<div class=\"section\" id=\"hello\">\n<h3><a class=\"toc-backref\" href=\"#id1\">Hello</a></h3>\n<p>lol dab</p>\n</div>\n<div class=\"section\" id=\"world\">\n<h3><a class=\"toc-backref\" href=\"#id2\">World</a></h3>\n<pre class=\"code python literal-block\"><code><span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">&quot;hello&quot;</span><span class=\"p\">)</span></code></pre>\n</div>\n</div>\n","rst":"Hello\r\n-----\r\n\r\nlol dab\r\n\r\nWorld\r\n------\r\n\r\n.. code:: python\r\n\r\n print(\"hello\")","title":"Test test"},"slug":"lol","user":"165023948638126080"},{"date":1523563634.73207,"id":"9db92d5f-8b8c-4e23-9636-6c0961017efc","post":{"headers":[],"html":"<div class=\"document\">\n<p>Hello world.</p>\n<p>Python is good.</p>\n<p>Thank you.</p>\n<dl class=\"field-list simple\">\n<dt>dabward</dt>\n<dd><p></p></dd>\n</dl>\n<pre class=\"code rust literal-block\"><code><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">myvar</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"nb\">String</span>::<span class=\"n\">from</span><span class=\"p\">(</span><span class=\"s\">&quot;World&quot;</span><span class=\"p\">);</span><span class=\"w\">\n</span><span class=\"n\">println</span><span class=\"o\">!</span><span class=\"p\">(</span><span class=\"s\">&quot;Hello {}&quot;</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">myvar</span><span class=\"p\">)</span></code></pre>\n<p>lol</p>\n<pre class=\"code ruby literal-block\"><code><span class=\"nb\">puts</span> <span class=\"s2\">&quot;Hello world, in Ruby!&quot;</span></code></pre>\n</div>\n","rst":"Hello world.\r\n\r\nPython is good.\r\n\r\nThank you.\r\n\r\n:dabward:\r\n\r\n.. code:: rust\r\n\r\n let myvar = String::from(\"World\");\r\n println!(\"Hello {}\", myvar)\r\n \r\nlol\r\n\r\n.. code:: ruby\r\n\r\n puts \"Hello world, in Ruby!\"","title":"My Lovely Page"},"slug":"mypage","user":"165023948638126080"},{"date":1523626337.532685,"id":"191fdfa4-c4d3-4d4c-8a9d-043c5b2a9130","post":{"headers":[],"html":"<div class=\"document\">\n<p>Testing testing</p>\n<p>1..2..3..</p>\n<pre class=\"code python literal-block\"><code><span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">&quot;Hello world&quot;</span><span class=\"p\">)</span></code></pre>\n</div>\n","rst":"Testing testing\r\n\r\n1..2..3..\r\n\r\n.. code:: python\r\n\r\n print(\"Hello world\")","title":"Test test"},"slug":"home","user":"165023948638126080"},{"date":1523626279.682973,"id":"40b4d131-929d-42c9-b486-11a202135959","post":{"headers":[],"html":"<div class=\"document\">\n<p>Testing testing</p>\n<p>1..2..3..</p>\n</div>\n","rst":"Testing testing\r\n\r\n1..2..3..","title":"Test test"},"slug":"home","user":"165023948638126080"},{"date":1523554013.463373,"id":"665c6946-5beb-4522-a5e5-715dd119e601","post":{"headers":[{"id":"#hello","title":"Hello"},{"id":"#world","title":"World"},{"id":"#hahahaha","title":"Hahahaha"}],"html":"<div class=\"document\">\n\n<div class=\"section\" id=\"hello\">\n<h3><a class=\"toc-backref\" href=\"#id1\">Hello</a></h3>\n<p>lol dab</p>\n</div>\n<div class=\"section\" id=\"world\">\n<h3><a class=\"toc-backref\" href=\"#id2\">World</a></h3>\n<pre class=\"code python literal-block\"><code><span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">&quot;hello&quot;</span><span class=\"p\">)</span></code></pre>\n</div>\n<div class=\"section\" id=\"hahahaha\">\n<h3><a class=\"toc-backref\" href=\"#id3\">Hahahaha</a></h3>\n<p>Lmfao</p>\n</div>\n</div>\n","rst":"Hello\r\n-----\r\n\r\nlol dab\r\n\r\nWorld\r\n------\r\n\r\n.. code:: python\r\n\r\n print(\"hello\")\r\n \r\nHahahaha\r\n--------\r\nLmfao","title":"Test test"},"slug":"lol","user":"165023948638126080"},{"date":1523563086.517109,"id":"fc7a58ca-820e-4227-a31f-650f8c4ff142","post":{"headers":[],"html":"<div class=\"document\">\n<p>Hello world.</p>\n<p>Python is good.</p>\n<p>Thank you.</p>\n<dl class=\"field-list simple\">\n<dt>dabward</dt>\n<dd><p></p></dd>\n</dl>\n<pre class=\"code rust literal-block\"><code><span class=\"kd\">let</span><span class=\"w\"> </span><span class=\"n\">myvar</span><span class=\"w\"> </span><span class=\"o\">=</span><span class=\"w\"> </span><span class=\"nb\">String</span>::<span class=\"n\">from</span><span class=\"p\">(</span><span class=\"s\">&quot;World&quot;</span><span class=\"p\">);</span><span class=\"w\">\n</span><span class=\"n\">println</span><span class=\"o\">!</span><span class=\"p\">(</span><span class=\"s\">&quot;Hello {}&quot;</span><span class=\"p\">,</span><span class=\"w\"> </span><span class=\"n\">myvar</span><span class=\"p\">)</span></code></pre>\n</div>\n","rst":"Hello world.\r\n\r\nPython is good.\r\n\r\nThank you.\r\n\r\n:dabward:\r\n\r\n.. code:: rust\r\n\r\n let myvar = String::from(\"World\");\r\n println!(\"Hello {}\", myvar)","title":"My Lovely Page"},"slug":"mypage","user":"165023948638126080"},{"date":1523626516.035445,"id":"fd3e093d-f6ca-49a6-9204-655580301626","post":{"headers":[],"html":"<div class=\"document\">\n<p>Testing testing</p>\n<p>1..2..3..</p>\n<pre class=\"code python literal-block\"><code><span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">&quot;Hello world&quot;</span><span class=\"p\">)</span></code></pre>\n<p>hahaha</p>\n<p>lit</p>\n</div>\n","rst":"Testing testing\r\n\r\n1..2..3..\r\n\r\n.. code:: python\r\n\r\n print(\"Hello world\")\r\n \r\nhahaha\r\n\r\nlit\r\n","title":"Test test"},"slug":"home","user":"165023948638126080"},{"date":1523562803.740303,"id":"771834af-de66-49a4-b48c-447703c26ad7","post":{"headers":[],"html":"<div class=\"document\">\n<p>Hello world.</p>\n<p>JavaScript is bad.</p>\n<p>Thank you.</p>\n</div>\n","rst":"Hello world.\r\n\r\nJavaScript is bad.\r\n\r\nThank you.","title":"My Lovely Page"},"slug":"mypage","user":"165023948638126080"},{"date":1523552057.061374,"id":"4916abe5-5619-4c0d-a6cf-86ba3605fa42","post":{"headers":[{"id":"#hello","title":"Hello"}],"html":"<div class=\"document\">\n\n<div class=\"section\" id=\"hello\">\n<h3><a class=\"toc-backref\" href=\"#id1\">Hello</a></h3>\n<p>lol dab</p>\n</div>\n</div>\n","rst":"Hello\r\n-----\r\n\r\nlol dab","slug":"lol","title":"Test test"},"slug":"lol","user":"165023948638126080"},{"date":1523626419.4753,"id":"4e682aba-fa0d-40a7-96b7-a72994880acb","post":{"headers":[],"html":"<div class=\"document\">\n<p>Testing testing</p>\n<p>1..2..3..</p>\n<pre class=\"code python literal-block\"><code><span class=\"k\">print</span><span class=\"p\">(</span><span class=\"s2\">&quot;Hello world&quot;</span><span class=\"p\">)</span></code></pre>\n<p>hahaha</p>\n</div>\n","rst":"Testing testing\r\n\r\n1..2..3..\r\n\r\n.. code:: python\r\n\r\n print(\"Hello world\")\r\n \r\nhahaha","title":"Test test"},"slug":"home","user":"165023948638126080"},{"date":1523562845.147068,"id":"d5317a3e-0054-4d6d-833f-27739383931d","post":{"headers":[],"html":"<div class=\"document\">\n<p>Hello world.</p>\n<p>Python is good.</p>\n<p>Thank you.</p>\n<dl class=\"field-list simple\">\n<dt>dabward</dt>\n<dd><p></p></dd>\n</dl>\n</div>\n","rst":"Hello world.\r\n\r\nPython is good.\r\n\r\nThank you.\r\n\r\n:dabward:","title":"My Lovely Page"},"slug":"mypage","user":"165023948638126080"}] \ No newline at end of file
diff --git a/pysite/views/wiki/edit.py b/pysite/views/wiki/edit.py
index 9eb50c9e..9cff9da0 100644
--- a/pysite/views/wiki/edit.py
+++ b/pysite/views/wiki/edit.py
@@ -1,10 +1,13 @@
# coding=utf-8
-from flask import request, url_for
+import datetime
+import difflib
+
+import requests
+from flask import redirect, request, url_for
from werkzeug.exceptions import BadRequest
-from werkzeug.utils import redirect
from pysite.base_route import RouteView
-from pysite.constants import EDITOR_ROLES
+from pysite.constants import DEBUG_MODE, EDITOR_ROLES, GITHUB_TOKEN, WIKI_AUDIT_WEBHOOK
from pysite.decorators import csrf, require_roles
from pysite.mixins import DBMixin
from pysite.rst import render
@@ -14,6 +17,7 @@ class EditView(RouteView, DBMixin):
path = "/edit/<path:page>" # "path" means that it accepts slashes
name = "edit"
table_name = "wiki"
+ revision_table_name = "wiki_revisions"
@require_roles(*EDITOR_ROLES)
def get(self, page):
@@ -24,16 +28,37 @@ class EditView(RouteView, DBMixin):
obj = self.db.get(self.table_name, page)
if obj:
- rst = obj["rst"]
- title = obj["title"]
- preview = obj["html"]
+ rst = obj.get("rst", "")
+ title = obj.get("title", "")
+ preview = obj.get("html", preview)
+
+ if obj.get("lock_expiry") and obj.get("lock_user") != self.user_data.get("user_id"):
+ lock_time = datetime.datetime.fromtimestamp(obj["lock_expiry"])
+ if datetime.datetime.utcnow() < lock_time:
+ return self.render("wiki/page_in_use.html", page=page)
+
+ lock_expiry = datetime.datetime.utcnow() + datetime.timedelta(minutes=5)
+
+ if not DEBUG_MODE: # If we are in debug mode we have no user logged in, therefore we can skip locking
+ self.db.insert(
+ self.table_name,
+ {
+ "slug": page,
+ "lock_expiry": lock_expiry.timestamp(),
+ "lock_user": self.user_data.get("user_id")
+ },
+ conflict="update"
+ )
return self.render("wiki/page_edit.html", page=page, rst=rst, title=title, preview=preview)
@require_roles(*EDITOR_ROLES)
@csrf
def post(self, page):
- rst = request.form["rst"]
+ rst = request.form.get("rst")
+
+ if not rst:
+ raise BadRequest()
if not rst.strip():
raise BadRequest()
@@ -48,10 +73,104 @@ class EditView(RouteView, DBMixin):
"headers": rendered["headers"]
}
+ self.audit_log(page, obj)
+
self.db.insert(
self.table_name,
obj,
conflict="replace"
)
+ # Add the post to the revisions table
+ revision_payload = {
+ "slug": page,
+ "post": obj,
+ "date": datetime.datetime.utcnow().timestamp(),
+ "user": self.user_data.get("user_id")
+ }
+
+ del revision_payload["post"]["slug"]
+
+ self.db.insert(self.revision_table_name, revision_payload)
+
return redirect(url_for("wiki.page", page=page), code=303) # Redirect, ensuring a GET
+
+ @require_roles(*EDITOR_ROLES)
+ @csrf
+ def patch(self, page):
+ current = self.db.get(self.table_name, page)
+ if not current:
+ return "", 404
+
+ if current.get("lock_expiry"): # If there is a lock present
+
+ # If user patching is not the user with the lock end here
+ if current["lock_user"] != self.user_data.get("user_id"):
+ return "", 400
+ new_lock = datetime.datetime.utcnow() + datetime.timedelta(minutes=5) # New lock time, 5 minutes in future
+ self.db.insert(self.table_name, {
+ "slug": page,
+ "lock_expiry": new_lock.timestamp()
+ }, conflict="update") # Update with new lock time
+ return "", 204
+
+ def audit_log(self, page, obj):
+ if WIKI_AUDIT_WEBHOOK: # If the audit webhook is not configured there is no point processing the diff
+ before = self.db.get(self.table_name, page)
+ if not before: # If this is a new page, before will be None
+ before = []
+ else:
+ if before.get("rst") is None:
+ before = []
+ else:
+ before = before["rst"].splitlines(keepends=True)
+ if len(before) == 0:
+ pass
+ else:
+ if not before[-1].endswith("\n"):
+ before[-1] += "\n" # difflib sometimes messes up if a newline is missing on last line
+
+ after = obj['rst'].splitlines(keepends=True) or [""]
+
+ if not after[-1].endswith("\n"):
+ after[-1] += "\n" # Does the same thing as L57
+
+ diff = difflib.unified_diff(before, after, fromfile="before.rst", tofile="after.rst")
+ diff = "".join(diff)
+
+ gist_payload = {
+ "description": f"Changes to: {obj['title']}",
+ "public": False,
+ "files": {
+ "changes.md": {
+ "content": f"```diff\n{diff}\n```"
+ }
+ }
+ }
+
+ headers = {
+ "Authorization": f"token {GITHUB_TOKEN}",
+ "User-Agent": "Discord Python Wiki (https://github.com/discord-python)"
+ }
+
+ gist = requests.post("https://api.github.com/gists",
+ json=gist_payload,
+ headers=headers)
+
+ audit_payload = {
+ "username": "Wiki Updates",
+ "embeds": [
+ {
+ "title": "Page Edit",
+ "description": f"**{obj['title']}** was edited by **{self.user_data.get('username')}**"
+ f".\n\n[View diff]({gist.json().get('html_url')})",
+ "color": 4165079,
+ "timestamp": datetime.datetime.utcnow().isoformat(),
+ "thumbnail": {
+ "url": "https://pythondiscord.com/static/logos/logo_discord.png"
+ }
+ }
+ ]
+ }
+
+ requests.post(WIKI_AUDIT_WEBHOOK, json=audit_payload)
diff --git a/pysite/views/wiki/history/compare.py b/pysite/views/wiki/history/compare.py
new file mode 100644
index 00000000..d42a6b36
--- /dev/null
+++ b/pysite/views/wiki/history/compare.py
@@ -0,0 +1,69 @@
+# coding=utf-8
+import difflib
+
+from pygments import highlight
+from pygments.formatters import HtmlFormatter
+from pygments.lexers import DiffLexer
+from werkzeug.exceptions import BadRequest, NotFound
+
+from pysite.base_route import RouteView
+from pysite.constants import DEBUG_MODE, EDITOR_ROLES
+from pysite.mixins import DBMixin
+
+
+class CompareView(RouteView, DBMixin):
+ path = "/history/compare/<string:first_rev>/<string:second_rev>"
+ name = "history.compare"
+
+ table_name = "wiki_revisions"
+ table_primary_key = "id"
+
+ def get(self, first_rev, second_rev):
+ before = self.db.get(self.table_name, first_rev)
+ after = self.db.get(self.table_name, second_rev)
+
+ if not (before and after):
+ raise NotFound()
+
+ if before["date"] > after["date"]: # Check whether the before was created after the after
+ raise BadRequest()
+
+ if before["id"] == after["id"]: # The same revision has been requested
+ raise BadRequest()
+
+ before_text = before["post"]["rst"]
+ after_text = after["post"]["rst"]
+
+ if not before_text.endswith("\n"):
+ before_text += "\n"
+
+ if not after_text.endswith("\n"):
+ after_text += "\n"
+
+ before_text = before_text.splitlines(keepends=True)
+ after_text = after_text.splitlines(keepends=True)
+
+ if not before["slug"] == after["slug"]:
+ raise BadRequest() # The revisions are not from the same post
+
+ diff = difflib.unified_diff(before_text, after_text, fromfile=f"{first_rev}.rst", tofile=f"{second_rev}.rst")
+ diff = "".join(diff)
+ diff = highlight(diff, DiffLexer(), HtmlFormatter())
+ return self.render("wiki/compare_revision.html",
+ title=after["post"]["title"],
+ diff=diff, slug=before["slug"],
+ can_edit=self.is_staff())
+
+ def is_staff(self):
+ if DEBUG_MODE:
+ return True
+ if not self.logged_in:
+ return False
+
+ roles = self.user_data.get("roles", [])
+
+ for role in roles:
+ if role in EDITOR_ROLES:
+ return True
+
+ return False
diff --git a/pysite/views/wiki/history/show.py b/pysite/views/wiki/history/show.py
new file mode 100644
index 00000000..e18b017e
--- /dev/null
+++ b/pysite/views/wiki/history/show.py
@@ -0,0 +1,42 @@
+# coding=utf-8
+import datetime
+
+from werkzeug.exceptions import NotFound
+
+from pysite.base_route import RouteView
+from pysite.constants import DEBUG_MODE, EDITOR_ROLES
+from pysite.mixins import DBMixin
+
+
+class RevisionsListView(RouteView, DBMixin):
+ path = "/history/show/<string:page>"
+ name = "history.show"
+
+ table_name = "wiki_revisions"
+ table_primary_key = "id"
+
+ def get(self, page):
+ results = self.db.filter(self.table_name, lambda revision: revision["slug"] == page)
+ if len(results) == 0:
+ raise NotFound()
+
+ for result in results:
+ ts = datetime.datetime.fromtimestamp(result["date"])
+ result["pretty_time"] = ts.strftime("%d %b %Y")
+
+ results = sorted(results, key=lambda revision: revision["date"], reverse=True)
+ return self.render("wiki/revision_list.html", page=page, revisions=results, can_edit=self.is_staff()), 200
+
+ def is_staff(self):
+ if DEBUG_MODE:
+ return True
+ if not self.logged_in:
+ return False
+
+ roles = self.user_data.get("roles", [])
+
+ for role in roles:
+ if role in EDITOR_ROLES:
+ return True
+
+ return False