From d4c2c7df8856d9049b5d80ffcc456bc125c58033 Mon Sep 17 00:00:00 2001 From: mst Date: Thu, 12 Sep 2024 18:25:02 +0300 Subject: [PATCH] 1.3.0-alpha --- app.py | 26 ++++- config.example.json | 3 +- constants.py | 2 +- functions.py | 23 +++-- requirements.txt | 1 + roadmap.md | 2 + schema.sql | 2 + static/css/style.css | 59 +++++++++++- static/icons/fediverse.svg | 17 ++++ static/icons/tumblr-light.svg | 1 + static/icons/tumblr.svg | 3 + templates/admin/index.html | 168 +++++++++++++++++---------------- templates/asked_questions.html | 74 +++++++++++++++ templates/base.html | 6 +- templates/inbox.html | 14 ++- templates/index.html | 160 +++++++++++++++++++++++++------ templates/view_question.html | 106 ++++++++++++++++++--- 17 files changed, 525 insertions(+), 142 deletions(-) create mode 100644 static/icons/fediverse.svg create mode 100644 static/icons/tumblr-light.svg create mode 100644 static/icons/tumblr.svg create mode 100644 templates/asked_questions.html diff --git a/app.py b/app.py index 3c07244..c3f5ec5 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,10 @@ from flask import Flask, Blueprint, jsonify, request, abort, render_template, flash, session, redirect, url_for +from flask_compress import Compress from dotenv import load_dotenv from mysql.connector import errorcode from functools import wraps import mysql.connector +import urllib import functions as func import os import json @@ -18,6 +20,8 @@ app.secret_key = os.environ.get("APP_SECRET") cfg = func.loadJSON(const.configFile) app.config.from_mapping(cfg) app.config.update(cfg) +# compress to improve page load speed +Compress(app) # -- blueprints -- api_bp = Blueprint('api', const.appName) @@ -123,7 +127,7 @@ def index(): cursor.close() conn.close() - return render_template('index.html', combined=combined, metadata=metadata, getRandomWord=func.getRandomWord, formatRelativeTime=func.formatRelativeTime) + return render_template('index.html', combined=combined, urllib=urllib, trimContent=func.trimContent, metadata=metadata, getRandomWord=func.getRandomWord, formatRelativeTime=func.formatRelativeTime) @app.route('/inbox/', methods=['GET']) @loginRequired @@ -142,7 +146,21 @@ def viewQuestion(question_id): question = func.getQuestion(question_id) answer = func.getAnswer(question_id) metadata = func.generateMetadata(question, answer) - return render_template('view_question.html', question=question, answer=answer, metadata=metadata, formatRelativeTime=func.formatRelativeTime, trimContent=func.trimContent) + return render_template('view_question.html', question=question, urllib=urllib, answer=answer, metadata=metadata, formatRelativeTime=func.formatRelativeTime, trimContent=func.trimContent) + +# TODO: implement this and private questions should be here too +""" +@app.route('/questions/', methods=['GET']) +def seeAskedQuestions(): + conn = func.connectToDb() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT * FROM questions WHERE asker_id=%s ORDER BY creation_date DESC", (asker_id,)) + questions = cursor.fetchall() + + cursor.close() + conn.close() + return render_template('asked_questions.html', questions=questions, formatRelativeTime=func.formatRelativeTime) +""" # -- admin client routes -- @@ -217,6 +235,8 @@ def addQuestion(): from_who = request.form.get('from_who', cfg['anonName']) question = request.form.get('question', '') antispam = request.form.get('antispam', '') + # reserved for version 1.4.0 + # private = request.form.get('private') if not question: abort(400, "Question field must not be empty") @@ -261,7 +281,7 @@ def deleteQuestion(): @api_bp.route('/return_to_inbox/', methods=['POST']) @loginRequired def returnToInbox(): - question_id = request.args.get('question_id', '') + question_id = request.args.get('question_id', '') if not question_id: abort(400, "Missing 'question_id' attribute or 'question_id' is empty") diff --git a/config.example.json b/config.example.json index 8784fad..80257a9 100644 --- a/config.example.json +++ b/config.example.json @@ -8,5 +8,6 @@ "charLimit": "512", "anonName": "Anonymous", "lockInbox": false, - "allowAnonQuestions": true + "allowAnonQuestions": true, + "showQuestionCount": false } diff --git a/constants.py b/constants.py index 3440bc4..bde2d0a 100644 --- a/constants.py +++ b/constants.py @@ -2,6 +2,6 @@ antiSpamFile = 'wordlist.txt' blacklistFile = 'word_blacklist.txt' configFile = 'config.json' appName = 'CatAsk' -version = '1.2.1' +version = '1.3.0' # id (identifier) is to be interpreted as described in https://semver.org/#spec-item-9 version_id = '-alpha' diff --git a/functions.py b/functions.py index 539ffb7..b22bb66 100644 --- a/functions.py +++ b/functions.py @@ -3,6 +3,7 @@ from markupsafe import Markup from bleach.sanitizer import Cleaner from datetime import datetime from pathlib import Path +from mistune import HTMLRenderer, escape import mistune import humanize import mysql.connector @@ -96,7 +97,16 @@ def getRandomWord(): return random.choice(items) def trimContent(var, trim): - return var[:trim] + '...' if len(var) > trim else var + trimmed = var[:trim] + '…' if len(var) >= trim else var + trimmed = trimmed.rstrip() + return trimmed + +class FixAsciiEmojis(HTMLRenderer): + def text(self, text): + if text.startswith('>') and text.endswith('<'): + return text + else: + return escape(text) def renderMarkdown(text): plugins = [ @@ -124,14 +134,15 @@ def renderMarkdown(text): # sanity of whoever will use this software # (after all, not everyone knows markdown syntax) md = mistune.create_markdown( - escape=True, + escape=False, plugins=plugins, - hard_wrap=True + hard_wrap=True, + renderer=FixAsciiEmojis() ) html = md(text) - cleaner = Cleaner(tags=allowed_tags, attributes=allowed_attrs) - clean_html = cleaner.clean(html) - return Markup(clean_html) + # cleaner = Cleaner(tags=allowed_tags, attributes=allowed_attrs) + # clean_html = cleaner.clean(html) + return Markup(html) def generateMetadata(question=None, answer=None): metadata = { diff --git a/requirements.txt b/requirements.txt index bc79440..7ee9a39 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,3 +5,4 @@ humanize mistune bleach pathlib +Flask-Compress diff --git a/roadmap.md b/roadmap.md index 179b0cb..2e16cbb 100644 --- a/roadmap.md +++ b/roadmap.md @@ -1,5 +1,7 @@ # CatAsk stable roadmap +* [x] make stuff more accessible +* [ ] implement private questions * [x] deleting answered questions OR returning them to inbox like retrospring does * [ ] bulk deleting questions from inbox * [ ] blocking askers by ip diff --git a/schema.sql b/schema.sql index e11851c..6fe590f 100644 --- a/schema.sql +++ b/schema.sql @@ -13,6 +13,8 @@ CREATE TABLE IF NOT EXISTS questions ( answered BOOLEAN NOT NULL DEFAULT FALSE, answer_id INT, pinned BOOLEAN NOT NULL DEFAULT FALSE + -- below is reserved for version 1.4.0 + -- private BOOLEAN NOT NULL DEFAULT FALSE ) ENGINE=InnoDB; ALTER TABLE questions diff --git a/static/css/style.css b/static/css/style.css index 79a9fb5..e002012 100644 --- a/static/css/style.css +++ b/static/css/style.css @@ -9,6 +9,7 @@ --bs-font-sans-serif: "Rubik", sans-serif; --bs-link-color-rgb: var(--bs-primary-rgb); --bs-nav-link-color: var(--bs-primary); + --bs-border-radius: .5rem; } [data-bs-theme=light] { @@ -18,15 +19,32 @@ --bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, white); --bs-danger: #dc3545; --bs-danger-bg-subtle: #f8e6e8; + --bs-link-color: var(--bs-primary); + --bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, black); + --bs-basic-btn-hover-bg: color-mix(in srgb, var(--bs-body-bg) 95%, black); + --bs-basic-btn-hover-bg-strong: color-mix(in srgb, var(--bs-body-bg) 90%, black); + --bs-basic-btn-active-bg: color-mix(in srgb, var(--bs-body-bg) 92%, black); + --bs-basic-btn-active-bg-strong: color-mix(in srgb, var(--bs-body-bg) 87%, black); } [data-bs-theme=dark] { + --bs-body-bg: #202020; + --bs-body-bg-rgb: 32, 32, 32; --bs-primary: #7f62f0; --bs-primary-rgb: 127,98,240; --bs-primary-subtle: color-mix(in srgb, var(--bs-primary) 10%, transparent); --bs-danger-bg-subtle: #2c0b0e; - --bs-link-color: color-mix(in srgb, var(--bs-primary) 60%, white); + --bs-link-color: color-mix(in srgb, var(--bs-primary) 65%, white); --bs-link-hover-color: color-mix(in srgb, var(--bs-primary) 80%, white); + --bs-basic-btn-hover-bg:color-mix(in srgb, var(--bs-body-bg) 95%, white); + --bs-basic-btn-hover-bg-strong: color-mix(in srgb, var(--bs-body-bg) 90%, white); + --bs-basic-btn-active-bg: color-mix(in srgb, var(--bs-body-bg) 92%, white); + --bs-basic-btn-active-bg-strong: color-mix(in srgb, var(--bs-body-bg) 87%, white); +} + +[data-bs-theme=light] .light-invert, +[data-bs-theme=dark] .dark-invert { + filter: invert(); } .btn { @@ -37,7 +55,7 @@ } [data-bs-theme=light] .btn-primary { - --bs-btn-bg: color-mix(in srgb, var(--bs-primary) 90%, white); + --bs-btn-bg: var(--bs-primary); --bs-btn-border-color: var(--bs-btn-bg); --bs-btn-hover-bg: color-mix(in srgb, var(--bs-btn-bg) 90%, black); --bs-btn-hover-border-color: var(--bs-btn-hover-bg); @@ -61,14 +79,35 @@ padding: 7px 13px; } -[data-bs-theme=light] .nav-link { - color: color-mix(in srgb, var(--bs-primary) 90%, black); +.btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):hover, .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):focus { + background-color: var(--bs-basic-btn-hover-bg); +} +.btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):active { + background-color: var(--bs-basic-btn-active-bg); +} +.card-footer .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):hover, .card-footer .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):focus { + background-color: var(--bs-basic-btn-hover-bg-strong) !important; + color: var(--bs-body-color) !important; +} +.card-footer .btn:not(.btn-primary, .btn-success, .btn-danger,.btn-outline-danger):active { + background-color: var(--bs-basic-btn-active-bg-strong) !important; +} +.btn-check:checked + .btn, .btn.active, .btn.show, .btn:first-child:active, :not(.btn-check) + .btn:active { + border-color: transparent; } -[data-bs-theme=dark] .nav-link, [data-bs-theme=dark] a { +[data-bs-theme=light] .nav-link, a { color: var(--bs-link-color); } +.nav-underline .nav-link:not(.active):hover { + color: var(--bs-nav-link-color); +} + +/* [data-bs-theme=dark] .nav-link, [data-bs-theme=dark] a { */ + /* color: var(--bs-link-color); */ +/* } */ + a:hover { color: var(--bs-link-hover-color); } @@ -120,6 +159,15 @@ a:hover { border-color: color-mix(in srgb, var(--bs-primary), transparent); } +.list-group-item.active { + --bs-list-group-active-bg: var(--bs-primary); + --bs-list-group-active-border-color: var(--bs-primary); +} + +.card, .dropdown-menu { + box-shadow: var(--bs-box-shadow); +} + .no-arrow.dropdown-toggle::after { border: none; display: none; @@ -141,3 +189,4 @@ a:hover { background-color: var(--bs-primary); border-color: var(--bs-primary); } + diff --git a/static/icons/fediverse.svg b/static/icons/fediverse.svg new file mode 100644 index 0000000..8abb809 --- /dev/null +++ b/static/icons/fediverse.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/static/icons/tumblr-light.svg b/static/icons/tumblr-light.svg new file mode 100644 index 0000000..1b708ba --- /dev/null +++ b/static/icons/tumblr-light.svg @@ -0,0 +1 @@ + diff --git a/static/icons/tumblr.svg b/static/icons/tumblr.svg new file mode 100644 index 0000000..690c126 --- /dev/null +++ b/static/icons/tumblr.svg @@ -0,0 +1,3 @@ + + + diff --git a/templates/admin/index.html b/templates/admin/index.html index fc89a33..a13b7a8 100644 --- a/templates/admin/index.html +++ b/templates/admin/index.html @@ -5,87 +5,93 @@

Admin panel

-

Instance

-
- - -
-
- - -
-
- - -
-
- - -
-

General

-
- - -
-
- - -
-
- - - -
-
- - - -
-
- - - -
-
- -
-
-
-
- -
- -

Blacklisted words for questions; one word per line

- - -
-
+

Instance

+
+ + +

Title of this CatAsk instance

+
+
+ + +

Description of this CatAsk instance

+
+
+ + +

Image that's going to be used in a link preview

+
+
+ + +

Full URL to homepage of this CatAsk instance without a trailing slash

+
+

General

+
+ + +

Max length of a question in characters; questions extending this character limit will not be added to the database

+
+
+ + +

This name will be used for questions asked to you by anonymous users

+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ +
+ +
+
+ +
+ +

Blacklisted words for questions; one word per line

+ + +
+
{% endblock %} {% block scripts %} +{% endblock %} diff --git a/templates/base.html b/templates/base.html index 399afd2..7cbe41d 100644 --- a/templates/base.html +++ b/templates/base.html @@ -42,8 +42,8 @@ Skip to content
-
- {{ const.appName }} v{{ version }}{{ version_id }} + {{ const.appName }} {{ version }}{{ version_id }}
diff --git a/templates/inbox.html b/templates/inbox.html index bfaf165..74dec2a 100644 --- a/templates/inbox.html +++ b/templates/inbox.html @@ -17,15 +17,25 @@ {{ cfg.anonName }} {% endif %} -
{{ formatRelativeTime(str(question.creation_date)) }}
+
+ {# + reserved for version 1.4.0 or later + + {% if question.private %} + Private + {% endif %} + #} + {{ formatRelativeTime(str(question.creation_date)) }} +
{{ question.content | render_markdown }}
+ -
+
+ +
+
+ {% endfor %}
{% endfor %} + {% for item in combined %} + {% for answer in item.answers %} + + {% endfor %} + {% endfor %} {% endif %} {% endblock %} {% block scripts %} +