diff options
| author | 2018-04-18 16:59:29 +0200 | |
|---|---|---|
| committer | 2018-04-18 16:59:29 +0200 | |
| commit | 2f61514e999c08361b29bf9f0c1f1fb044d3f6e0 (patch) | |
| tree | 433471571ba608ea5d0bbe89c99e75a260920630 | |
| parent | Adding Pinkie Pie to the hiphoppers list (diff) | |
| parent | Pass can_edit into revisions list (diff) | |
Merge branch 'master' of github.com:discord-python/site
| -rw-r--r-- | app_test.py | 21 | ||||
| -rw-r--r-- | pysite/constants.py | 6 | ||||
| -rw-r--r-- | pysite/database.py | 3 | ||||
| -rw-r--r-- | pysite/database/table_init/wiki_revisions.json | 1 | ||||
| -rw-r--r-- | pysite/views/wiki/edit.py | 133 | ||||
| -rw-r--r-- | pysite/views/wiki/history/compare.py | 69 | ||||
| -rw-r--r-- | pysite/views/wiki/history/show.py | 42 | ||||
| -rw-r--r-- | static/js/revision_diff.js | 75 | ||||
| -rw-r--r-- | templates/wiki/base.html | 23 | ||||
| -rw-r--r-- | templates/wiki/compare_revision.html | 13 | ||||
| -rw-r--r-- | templates/wiki/page_edit.html | 20 | ||||
| -rw-r--r-- | templates/wiki/page_in_use.html | 13 | ||||
| -rw-r--r-- | templates/wiki/revision_list.html | 36 | 
13 files changed, 445 insertions, 10 deletions
| diff --git a/app_test.py b/app_test.py index 1e46a73d..20293db9 100644 --- a/app_test.py +++ b/app_test.py @@ -114,6 +114,26 @@ class RootEndpoint(SiteTest):          response = self.client.get("/500")          self.assertEqual(response.status_code, 500) +    def test_wiki_edit(self): +        """Test that the wiki edit page redirects to login""" +        response = self.client.get("/edit/page", "http://wiki.pytest.local") +        self.assertEqual(response.status_code, 302) + +    def test_wiki_edit_post_empty_request(self): +        """Empty request should redirect to login""" +        response = self.client.post("/edit/page", "http://wiki.pytest.local") +        self.assertEqual(response.status_code, 302) + +    def test_wiki_history(self): +        """Test the history show""" +        response = self.client.get("/history/show/blahblah-non-existant-page", "http://wiki.pytest.local") +        self.assertEqual(response.status_code, 404) # Test that unknown routes 404 + +    def test_wiki_diff(self): +        """Test whether invalid revision IDs error""" +        response = self.client.get("/history/compare/ABC/XYZ", "http://wiki.pytest.local") +        self.assertEqual(response.status_code, 404) # Test that unknown revisions 404 +  class ApiEndpoints(SiteTest):      """ Test cases for the api subdomain """ @@ -236,7 +256,6 @@ class StaffEndpoints(SiteTest):          from pysite.views.staff.index import StaffView          sv = StaffView()          result = sv.get() -        print(repr(result.data))          self.assertEqual(result.status_code, 302)  # TODO: Do this correctly          response = self.client.get('/', app.config['STAFF_SUBDOMAIN']) 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\">"World"</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\">"Hello {}"</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\">"hello"</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\">"World"</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\">"Hello {}"</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\">"Hello world, in Ruby!"</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\">"Hello world"</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\">"hello"</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\">"World"</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\">"Hello {}"</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\">"Hello world"</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\">"Hello world"</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 diff --git a/static/js/revision_diff.js b/static/js/revision_diff.js new file mode 100644 index 00000000..9d818c6b --- /dev/null +++ b/static/js/revision_diff.js @@ -0,0 +1,75 @@ +"use strict"; + +(function() { +let buttons = document.querySelectorAll("td input"); // Fetch all radio buttons +let id_reg = /compare-(before|after)-([\w|-]+)/; // Matches compare-after/before-ID + + +function getRevisionId(element){ +    let e = element.id.match(id_reg); // Match ID with RegExp +    return [e[1], e[2]]; // e is in format of [full id, after/before, ID] we only want ID & mode +} + +function getRevision(id) { +    let e = revisions.filter((x) => { +        return x.id === id; // Filter through all revisions to find the selected one (revisions in declared in the template) +    }); +    return e[0]; +} + +function radioButtonChecked(element) { +    console.log("change detected"); +    let id = getRevisionId(element); +    let rev = getRevision(id[1]); +    if (id[0] === "after"){ +        document.querySelector(`#compare-before-${id[1]}`).checked = false; // Deselect the opposite checkbox to the one which has been checked +                                                                            // because we don't want checking of the same revision + +        buttons.forEach(function(e){ +            if (getRevisionId(e)[0] === "after" && e.id !== element.id) {  // Deselect all checkboxes in the same row +                e.checked = false; +            } +        }) +    } else { // This else does the same as above but for the before column +        document.querySelector(`#compare-after-${id[1]}`).checked = false; +        buttons.forEach(function(e){ +            if (getRevisionId(e)[0] === "before" && e.id !== element.id) { +                e.checked = false; +            } + +            if (getRevisionId(e)[0] === "after") { // This makes sure that you do not compare a new revision with an old one +                let tmprev = getRevision(getRevisionId(e)[1]) +                console.log(tmprev); +                if (tmprev.date <= rev.date) { +                    document.querySelector(`#${e.id}`).setAttribute("disabled", "") +                } else { +                    document.querySelector(`#${e.id}`).removeAttribute("disabled") +                } +            } +        }); +    } + +    let bef, aft; + +    buttons.forEach((button) => { // Find the selected posts +        let id = getRevisionId(button); +        if (button.checked && id[0] === "before") { +            bef = id[1]; +        } + +        if (button.checked && id[0] === "after") { +            aft = id[1]; +        } +    }) + +    document.getElementById("compare-submit").href = `/history/compare/${bef}/${aft}` // Switch the buttons HREF to point to the correct compare URL + +} + +buttons.forEach(function(button){ +    button.checked = false; // Some browsers remember if a button is checked. +    button.onchange = function() { +        radioButtonChecked(button); +    } +}); +})(); diff --git a/templates/wiki/base.html b/templates/wiki/base.html index b2545977..87686a83 100644 --- a/templates/wiki/base.html +++ b/templates/wiki/base.html @@ -86,6 +86,29 @@                              </li>                          {% endif %} +                        {% if current_page != "history.show" %} +                            {% if current_page == "history.compare" %} +                                <li> +                                    <a href="{{ url_for("wiki.history.show", page=slug) }}"> +                                        <i class="uk-icon fas fa-fw fa-arrow-left"></i>  Back +                                    </a> +                                </li> +                            {% else %} +                                <li> +                                    <a href="{{ url_for("wiki.history.show", page=page) }}"> +                                        <i class="uk-icon fas fa-fw fa-history"></i>  Revisions +                                    </a> +                                </li> +                            {% endif %} +                        {% else %} +                            <li> +                                <a href="{{ url_for("wiki.page", page=page) }}"> +                                    <i class="uk-icon fas fa-fw fa-arrow-left"></i>  Back +                                </a> +                            </li> +                        {% endif %} + +                          {% if current_page != "source" %}                              <li>                                  <a href="{{ url_for("wiki.source", page=page) }}"> diff --git a/templates/wiki/compare_revision.html b/templates/wiki/compare_revision.html new file mode 100644 index 00000000..34ab61bc --- /dev/null +++ b/templates/wiki/compare_revision.html @@ -0,0 +1,13 @@ +{% extends "wiki/base.html" %} +{% block title %}Wiki | Comparing {{ title }}{% endblock %} +{% block og_title %}Wiki | Comparing {{ title }}{% endblock %} +{% block og_description %}{% endblock %} +{% block content %} +    <div class="uk-container uk-container-small"> +        <h2 class="uk-title"> +            Revision comparison for {{ title }} +        </h2> + +        {{ diff | safe }} +    </div> +{% endblock %} diff --git a/templates/wiki/page_edit.html b/templates/wiki/page_edit.html index b59a8912..a2d709e2 100644 --- a/templates/wiki/page_edit.html +++ b/templates/wiki/page_edit.html @@ -99,5 +99,23 @@          document.getElementById("title").oninput = function() {              document.getElementById("preview-title").textContent = document.getElementById("title").value;          } + +        function refreshLock(){ +            console.log("Refreshing lock") +            let xhttp = new XMLHttpRequest(); +            xhttp.onreadystatechange = function() { +                if (this.readyState === 4 && this.status === 204) { +                    console.log("Lock refreshed") +                } else if(this.readyState === 4 && this.status !== 204) { +                    console.log("Could not refresh lock") +                } +            }; + +            xhttp.open("PATCH", document.location.pathname, true); +            xhttp.send(); +        } + +        // Lock refreshing +        setInterval(refreshLock, (60 * 4) * 1000)      </script> -{% endblock %}
\ No newline at end of file +{% endblock %} diff --git a/templates/wiki/page_in_use.html b/templates/wiki/page_in_use.html new file mode 100644 index 00000000..4110ee17 --- /dev/null +++ b/templates/wiki/page_in_use.html @@ -0,0 +1,13 @@ +{% extends "wiki/base.html" %} +{% block title %}Wiki Error{% endblock %} +{% block og_title %}Wiki Error{% endblock %} +{% block og_description %}{% endblock %} +{% block content %} +    <div class="uk-container uk-container-small"> +        <div uk-alert class="uk-alert-warning"> +            <a class="uk-alert-close" uk-close></a> +            <h3>The page you requested is currently being edited</h3> +            <p>Please try again in a little bit when the lock has expired.</p> +        </div> +    </div> +{% endblock %} diff --git a/templates/wiki/revision_list.html b/templates/wiki/revision_list.html new file mode 100644 index 00000000..6eb5b6a8 --- /dev/null +++ b/templates/wiki/revision_list.html @@ -0,0 +1,36 @@ +{% extends "wiki/base.html" %} +{% block title %}Wiki | Revisions to {{ page }}{% endblock %} +{% block og_title %}Wiki | Revisions to {{ page }}{% endblock %} +{% block og_description %}{% endblock %} +{% block content %} +    <div class="uk-container"> +        <h2>Wiki page revisions</h2> +        <table class="uk-table uk-table-hover"> +            <thead> +            <tr> +                <th>Page title</th> +                <th>Date</th> +                <th>User ID</th> +                <th>Compare before</th> +                <th>Compare after</th> +            </tr> +            </thead> +            <tbody> +            {% for revision in revisions %} +                <tr> +                    <td>{{ revision["post"]["title"] }}</td> +                    <td>{{ revision["pretty_time"] }}</td> +                    <td>{{ revision['user'] }}</td> +                    <td><input type="radio" id="compare-before-{{ revision['id'] }}" class="uk-radio"></td> +                    <td><input type="radio" id="compare-after-{{ revision['id'] }}" class="uk-radio"></td> +                </tr> +            {% endfor %} +            </tbody> +        </table> +        <a href="#" id="compare-submit" class="uk-button uk-button-primary">Compare selections</a> +    </div> +    <script> +        let revisions = {{ revisions | tojson }} +    </script> +    <script src="{{ static_file("js/revision_diff.js") }}"></script> +{% endblock %} | 
