diff options
| -rw-r--r-- | gunicorn_config.py | 15 | ||||
| -rw-r--r-- | pysite/database.py | 15 | ||||
| -rw-r--r-- | pysite/views/wiki/edit.py | 10 | ||||
| -rw-r--r-- | pysite/views/wiki/search.py | 66 | ||||
| -rw-r--r-- | static/style.css | 7 | ||||
| -rw-r--r-- | templates/wiki/base.html | 21 | ||||
| -rw-r--r-- | templates/wiki/page_edit.html | 27 | ||||
| -rw-r--r-- | templates/wiki/page_move.html | 4 | ||||
| -rw-r--r-- | templates/wiki/search.html | 23 | ||||
| -rw-r--r-- | templates/wiki/search_results.html | 39 | 
10 files changed, 212 insertions, 15 deletions
| 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:page>"  # "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"<strong>\1</strong>", html.escape(match_text)) + +                snippets.append(match_text.replace("\n", "<br />")) + +            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 %}                          <li class="uk-nav-divider"></li> +{#                        <li><a href="{{ url_for("wiki.search") }}">#} +{#                            <i class="uk-icon fas fa-fw fa-search"></i>  Search#} +{#                        </a></li>#}                          <li><a href="{{ url_for("wiki.special") }}">                              <i class="uk-icon fas fa-fw fa-ellipsis-h"></i>  Special Pages                          </a></li>                          <li><a href="{{ url_for("wiki.page", page="help") }}"> -                            <i class="fas fa-fw fa-question-circle"></i>  Help +                            <i class="uk-icon fas fa-fw fa-question-circle"></i>  Help                          </a></li> + +                        <li> +                            <form action="{{ url_for("wiki.search") }}" method="post"> +                                {% if query is undefined %} +                                <input type="text" class="uk-input" placeholder="Search (BETA)" id="query" name="query" style="padding-right: 0; margin-top: 5px; border-left: 0; border-right: 0;" required> +                                {% else %} +                                <input type="text" class="uk-input" placeholder="Search (BETA)" id="query" name="query" value="{{ query }}" style="padding-right: 0; margin-top: 5px; border-left: 0; border-right: 0;" required> +                                {% endif %} +                                <br /> +                                <button class="uk-button uk-button-darkish uk-button-small" type="submit" id="search" title="Search" style="width: 100%; border: 0;"> +                                    <i class="uk-icon fas fa-fw fa-search"></i> +                                </button> + +                                <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> +                            </form> +                        </li>                      </ul>                  </div>                  <div class="uk-section" style="flex-grow: 1; margin: 0 1rem 1rem;"> 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 %}      <form uk-grid class="uk-grid-small" action="{{ url_for("wiki.edit", page=page) }}" method="post">          <div class="uk-width-expand"> -            <input name="title" id="title" placeholder="Page Title" value="{{ title }}" class="uk-input" /> +            <input name="title" id="title" placeholder="Page Title" value="{{ title }}" class="uk-input" required />          </div>          <div class="uk-width-auto">              <button class="uk-button uk-button-secondary" type="button" value="Preview" id="preview">Preview</button> @@ -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 @@          <form uk-grid class="uk-grid-small" action="{{ url_for("wiki.move", page=page) }}" method="post">              <input type="text" class="uk-width-1-1 uk-input" placeholder="{{ page }}" id="location" name="location" style="margin-left: 15px;" required>              <div class="uk-width-1-2"> -                <a href="{{ url_for("wiki.page", page=page) }}" class="uk-button uk-button-primary uk-width-1-1" type="button" id="cancel">Cancel</a> +                <a href="{{ url_for("wiki.page", page=page) }}" class="uk-button uk-button-secondary uk-width-1-1" type="button" id="cancel">Cancel</a>              </div>              <div class="uk-width-1-2"> -                <input class="uk-button uk-button-secondary uk-width-1-1" type="submit" id="move" value="Move" /> +                <input class="uk-button uk-button-primary uk-width-1-1" type="submit" id="move" value="Move" />              </div>              <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> 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 %} +    <div class="uk-container uk-container-small"> +        <h2 class="uk-title"> +            Search +        </h2> + +        <form uk-grid class="uk-grid-small" action="{{ url_for("wiki.search") }}" method="post"> +            <input type="text" class="uk-width-1-1 uk-input" placeholder="Search Query" id="query" name="query" style="margin-left: 15px;" required> +            <div class="uk-width-1-4"> +                  +            </div> +            <div class="uk-width-1-2"> +                <input class="uk-button uk-button-primary uk-width-1-1" type="submit" id="search" value="Search" /> +            </div> + +            <input type="hidden" name="csrf_token" value="{{ csrf_token() }}"/> +        </form> +    </div> +{% 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 %} +    <div class="uk-container uk-container-small"> +        {% if not pages %} +            <h2 class="uk-title"> +                Search +            </h2> +            <div class="uk-alert uk-alert-warning uk-text-center"> +                <p> +                    Sorry, no results were found. Please check your query and try again. +                </p> +            </div> +        {% else %} +            <h2 class="uk-title"> +                Search Results +            </h2> + +            {% for page in pages %} +                <h4> +                    <a href="{{ url_for("wiki.page", page=page["slug"]) }}">{{ page.title }}</a> +                    (<span style="font-family: monospace;">{{ page.slug }}</span>) +                </h4> + +                {% for snippet in page["matches"] %} +                    <div class="quote"> +                        <i class="uk-icon far fa-ellipsis-h"></i> +                        <br /> +                        {{ snippet | safe }} +                        <br /> +                        <i class="uk-icon far fa-ellipsis-h"></i> +                    </div> +                {% endfor %} +            {% endfor %} +        {% endif %} +    </div> +{% endblock %}
\ No newline at end of file | 
