From efb902f3f293db4aac6e57b9f3a84143d84a90e9 Mon Sep 17 00:00:00 2001 From: Gareth Coles Date: Sun, 6 May 2018 18:21:34 +0100 Subject: [Wiki] Misc improvements, plus a beta search feature --- gunicorn_config.py | 15 +++++++++ pysite/database.py | 15 +++++++-- pysite/views/wiki/edit.py | 10 ++++-- pysite/views/wiki/search.py | 66 ++++++++++++++++++++++++++++++++++++++ static/style.css | 7 ++++ templates/wiki/base.html | 21 +++++++++++- templates/wiki/page_edit.html | 27 ++++++++++++---- templates/wiki/page_move.html | 4 +-- templates/wiki/search.html | 23 +++++++++++++ templates/wiki/search_results.html | 39 ++++++++++++++++++++++ 10 files changed, 212 insertions(+), 15 deletions(-) create mode 100644 pysite/views/wiki/search.py create mode 100644 templates/wiki/search.html create mode 100644 templates/wiki/search_results.html diff --git a/gunicorn_config.py b/gunicorn_config.py index 0e374fdd..c09bee6a 100644 --- a/gunicorn_config.py +++ b/gunicorn_config.py @@ -1,3 +1,10 @@ +import html +import re + +STRIP_REGEX = re.compile(r"<[^<]+?>") +WIKI_TABLE = "wiki" + + def when_ready(server=None): """ server hook that only runs when the gunicorn master process loads """ @@ -24,3 +31,11 @@ def when_ready(server=None): if initialized: tables = ", ".join([f"{table} ({count} items)" for table, count in initialized.items()]) output(f"Initialized the following tables: {tables}") + + output("Adding plain-text version of any wiki articles that don't have one...") + + for article in db.pluck(WIKI_TABLE, "html", "text", "slug"): + if "text" not in article: + article["text"] = html.unescape(STRIP_REGEX.sub("", article["html"]).strip()) + + db.insert(WIKI_TABLE, article, conflict="update") diff --git a/pysite/database.py b/pysite/database.py index 86c8685d..41fbd253 100644 --- a/pysite/database.py +++ b/pysite/database.py @@ -1,8 +1,10 @@ # coding=utf-8 +import html import json import logging import os from typing import Any, Callable, Dict, Iterator, List, Optional, Union +import re import rethinkdb from flask import abort @@ -22,6 +24,9 @@ ALL_TABLES = { "wiki_revisions": "id" } +STRIP_REGEX = re.compile(r"<[^<]+?>") +WIKI_TABLE = "wiki" + class RethinkDB: @@ -55,6 +60,13 @@ class RethinkDB: tables = ", ".join([f"{table} ({count} items)" for table, count in initialized.items()]) self.log.debug(f"Initialized the following tables: {tables}") + # Upgrade wiki articles + for article in self.pluck(WIKI_TABLE, "html", "text", "slug"): + if "text" not in article: + article["text"] = html.unescape(STRIP_REGEX.sub("", article["html"]).strip()) + + self.insert(WIKI_TABLE, article, conflict="update") + def create_tables(self) -> List[str]: """ Creates whichever tables exist in the ALL_TABLES @@ -496,9 +508,6 @@ class RethinkDB: """ Map a function over every document in a table, with the possibility of modifying it - r.table('users').map( - lambda doc: doc.merge({'user_id': doc['id']}).without('id')).run(conn) - As an example, you could do the following to rename the "id" field to "user_id" for all documents in the "users" table. diff --git a/pysite/views/wiki/edit.py b/pysite/views/wiki/edit.py index 2cd4181c..bb39ed2b 100644 --- a/pysite/views/wiki/edit.py +++ b/pysite/views/wiki/edit.py @@ -1,5 +1,7 @@ import datetime import difflib +import html +import re import requests from flask import redirect, request, url_for @@ -11,6 +13,8 @@ from pysite.decorators import csrf, require_roles from pysite.mixins import DBMixin from pysite.rst import render +STRIP_REGEX = re.compile(r"<[^<]+?>") + class EditView(RouteView, DBMixin): path = "/edit/" # "path" means that it accepts slashes @@ -55,11 +59,12 @@ class EditView(RouteView, DBMixin): @csrf def post(self, page): rst = request.form.get("rst") + title = request.form["title"] - if not rst: + if not rst or not not rst.strip(): raise BadRequest() - if not rst.strip(): + if not title or not title.strip(): raise BadRequest() rendered = render(rst) @@ -69,6 +74,7 @@ class EditView(RouteView, DBMixin): "title": request.form["title"], "rst": rst, "html": rendered["html"], + "text": html.unescape(STRIP_REGEX.sub("", rendered["html"]).strip()), "headers": rendered["headers"] } diff --git a/pysite/views/wiki/search.py b/pysite/views/wiki/search.py new file mode 100644 index 00000000..1c920e15 --- /dev/null +++ b/pysite/views/wiki/search.py @@ -0,0 +1,66 @@ +import html +import re + +from flask import redirect, request, url_for +from werkzeug.exceptions import BadRequest + +from pysite.base_route import RouteView +from pysite.decorators import csrf +from pysite.mixins import DBMixin + +STRIP_REGEX = re.compile(r"<[^<]+?>") + + +class SearchView(RouteView, DBMixin): + path = "/search" # "path" means that it accepts slashes + name = "search" + table_name = "wiki" + revision_table_name = "wiki_revisions" + + def get(self): + return self.render("wiki/search.html") + + @csrf + def post(self): + given_query = request.form.get("query") + + if not given_query or not given_query.strip(): + raise BadRequest() + + query = f"({re.escape(given_query)})" + + pages = self.db.filter( + self.table_name, + lambda doc: doc["text"].match(query) + ) + + if len(pages) == 1: + slug = pages[0]["slug"] + return redirect(url_for("wiki.page", page=slug), code=303) + + for obj in pages: + text = obj["text"] + + matches = re.finditer(query, text) + snippets = [] + + for match in matches: + start = match.start() - 50 + + if start < 0: + start = 0 + + end = match.end() + 50 + + if end > len(text): + end = len(text) + + match_text = text[start:end] + match_text = re.sub(query, r"\1", html.escape(match_text)) + + snippets.append(match_text.replace("\n", "
")) + + obj["matches"] = snippets + + pages = sorted(pages, key=lambda d: d["title"]) + return self.render("wiki/search_results.html", pages=pages, query=given_query) diff --git a/static/style.css b/static/style.css index 78f2663b..a857bd4d 100644 --- a/static/style.css +++ b/static/style.css @@ -151,3 +151,10 @@ div.payment-icon img { div.payment-icon { margin-right: 1em; } + +div.quote { + border-left: 3px solid #7289DA; + color: #99AAB5; + padding-left: 20px; + margin-bottom: 1rem; +} \ No newline at end of file diff --git a/templates/wiki/base.html b/templates/wiki/base.html index a71f09e6..9b31d83b 100644 --- a/templates/wiki/base.html +++ b/templates/wiki/base.html @@ -154,12 +154,31 @@ {% endif %}
  • +{#
  • #} +{#  Search#} +{#
  • #}
  •  Special Pages
  • -  Help +  Help
  • + +
  • +
    + {% if query is undefined %} + + {% else %} + + {% endif %} +
    + + + +
    +
  • diff --git a/templates/wiki/page_edit.html b/templates/wiki/page_edit.html index 51ce70db..138292f9 100644 --- a/templates/wiki/page_edit.html +++ b/templates/wiki/page_edit.html @@ -8,7 +8,7 @@ {% block content %}
    - +
    @@ -37,7 +37,7 @@ let csrf_token = "{{ csrf_token() }}"; - document.getElementById("preview").onclick = function(event) { + function do_preview() { let oReq = new XMLHttpRequest(); oReq.addEventListener("load", function() { @@ -63,7 +63,7 @@ let data = editor.getValue(); - if (data.replace("\s", "").length < 1) { + if (data.replace("\s", "").length < 1 || document.getElementById("title").value.length < 1) { document.getElementById("submit").disabled = true; return false; } @@ -76,7 +76,9 @@ oReq.send(JSON.stringify({"data": editor.getValue()})); return false; - }; + } + + document.getElementById("preview").onclick = do_preview; let editor = ace.edit("editor"); let timer; @@ -93,15 +95,26 @@ if (timer !== undefined) { clearTimeout(timer); } - timer = setTimeout(function() {document.getElementById("preview").click()}, 1000); + timer = setTimeout(do_preview, 1000); }); document.getElementById("title").oninput = function() { + if (document.getElementById("title").value.length < 1) { + document.getElementById("submit").disabled = true; + } + document.getElementById("preview-title").textContent = document.getElementById("title").value; - } + + document.getElementById("rst").value = editor.getValue(); + + if (timer !== undefined) { + clearTimeout(timer); + } + timer = setTimeout(do_preview, 1000); + }; function refreshLock(){ - console.log("Refreshing lock") + console.log("Refreshing lock"); let xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState === 4 && this.status === 204) { diff --git a/templates/wiki/page_move.html b/templates/wiki/page_move.html index e2d52807..409c02e7 100644 --- a/templates/wiki/page_move.html +++ b/templates/wiki/page_move.html @@ -14,10 +14,10 @@
    - +
    diff --git a/templates/wiki/search.html b/templates/wiki/search.html new file mode 100644 index 00000000..6b15522e --- /dev/null +++ b/templates/wiki/search.html @@ -0,0 +1,23 @@ +{% extends "wiki/base.html" %} +{% block title %}Wiki | Search{% endblock %} +{% block og_title %}Wiki | Search{% endblock %} +{% block og_description %}Search for pages by content{% endblock %} +{% block content %} +
    +

    + Search +

    + + + +
    +   +
    +
    + +
    + + + +
    +{% endblock %} \ No newline at end of file diff --git a/templates/wiki/search_results.html b/templates/wiki/search_results.html new file mode 100644 index 00000000..08c303e2 --- /dev/null +++ b/templates/wiki/search_results.html @@ -0,0 +1,39 @@ +{% extends "wiki/base.html" %} +{% block title %}Wiki | Search Results{% endblock %} +{% block og_title %}Wiki | Search Results{% endblock %} +{% block og_description %}Search results{% endblock %} +{% block content %} +
    + {% if not pages %} +

    + Search +

    +
    +

    + Sorry, no results were found. Please check your query and try again. +

    +
    + {% else %} +

    + Search Results +

    + + {% for page in pages %} +

    + {{ page.title }} + ({{ page.slug }}) +

    + + {% for snippet in page["matches"] %} +
    + +
    + {{ snippet | safe }} +
    + +
    + {% endfor %} + {% endfor %} + {% endif %} +
    +{% endblock %} \ No newline at end of file -- cgit v1.2.3