1.2.0-alpha
45
app.py
|
@ -99,8 +99,15 @@ def render_markdown(text):
|
||||||
def index():
|
def index():
|
||||||
conn = func.connectToDb()
|
conn = func.connectToDb()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute("SELECT * FROM questions WHERE answered=%s ORDER BY creation_date DESC", (True,))
|
|
||||||
questions = cursor.fetchall()
|
cursor.execute("SELECT * FROM questions WHERE answered=%s AND pinned=%s ORDER BY creation_date DESC", (True, True))
|
||||||
|
pinned_questions = cursor.fetchall()
|
||||||
|
|
||||||
|
cursor.execute("SELECT * FROM questions WHERE answered=%s AND pinned=%s ORDER BY creation_date DESC", (True, False))
|
||||||
|
non_pinned_questions = cursor.fetchall()
|
||||||
|
|
||||||
|
questions = pinned_questions + non_pinned_questions
|
||||||
|
|
||||||
cursor.execute("SELECT * FROM answers ORDER BY creation_date DESC")
|
cursor.execute("SELECT * FROM answers ORDER BY creation_date DESC")
|
||||||
answers = cursor.fetchall()
|
answers = cursor.fetchall()
|
||||||
|
|
||||||
|
@ -123,7 +130,7 @@ def index():
|
||||||
def inbox():
|
def inbox():
|
||||||
conn = func.connectToDb()
|
conn = func.connectToDb()
|
||||||
cursor = conn.cursor(dictionary=True)
|
cursor = conn.cursor(dictionary=True)
|
||||||
cursor.execute("SELECT * FROM questions WHERE answered=%s", (False,))
|
cursor.execute("SELECT * FROM questions WHERE answered=%s ORDER BY creation_date DESC", (False,))
|
||||||
questions = cursor.fetchall()
|
questions = cursor.fetchall()
|
||||||
|
|
||||||
cursor.close()
|
cursor.close()
|
||||||
|
@ -146,13 +153,13 @@ def login():
|
||||||
admin_password = request.form.get('admin_password')
|
admin_password = request.form.get('admin_password')
|
||||||
if admin_password == os.environ.get('ADMIN_PASSWORD'):
|
if admin_password == os.environ.get('ADMIN_PASSWORD'):
|
||||||
session['logged_in'] = True
|
session['logged_in'] = True
|
||||||
return redirect(url_for('admin.index'))
|
return redirect(url_for('index'))
|
||||||
else:
|
else:
|
||||||
flash("Wrong password", 'danger')
|
flash("Wrong password", 'danger')
|
||||||
return redirect(url_for('admin.login'))
|
return redirect(url_for('admin.login'))
|
||||||
else:
|
else:
|
||||||
if logged_in:
|
if logged_in:
|
||||||
return redirect('admin.index')
|
return redirect('index')
|
||||||
else:
|
else:
|
||||||
return render_template('admin/login.html')
|
return render_template('admin/login.html')
|
||||||
|
|
||||||
|
@ -276,6 +283,34 @@ def returnToInbox():
|
||||||
|
|
||||||
return {'message': 'Successfully returned question to inbox.'}, 200
|
return {'message': 'Successfully returned question to inbox.'}, 200
|
||||||
|
|
||||||
|
@api_bp.route('/pin_question/', methods=['POST'])
|
||||||
|
def pinQuestion():
|
||||||
|
question_id = request.args.get('question_id', '')
|
||||||
|
if not question_id:
|
||||||
|
abort(400, "Missing 'question_id' attribute or 'question_id' is empty")
|
||||||
|
|
||||||
|
conn = func.connectToDb()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE questions SET pinned=%s WHERE id=%s", (True, question_id))
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {'message': 'Successfully pinned question.'}, 200
|
||||||
|
|
||||||
|
@api_bp.route('/unpin_question/', methods=['POST'])
|
||||||
|
def unpinQuestion():
|
||||||
|
question_id = request.args.get('question_id', '')
|
||||||
|
if not question_id:
|
||||||
|
abort(400, "Missing 'question_id' attribute or 'question_id' is empty")
|
||||||
|
|
||||||
|
conn = func.connectToDb()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute("UPDATE questions SET pinned=%s WHERE id=%s", (False, question_id))
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
return {'message': 'Successfully unpinned question.'}, 200
|
||||||
|
|
||||||
@api_bp.route('/add_answer/', methods=['POST'])
|
@api_bp.route('/add_answer/', methods=['POST'])
|
||||||
@loginRequired
|
@loginRequired
|
||||||
def addAnswer():
|
def addAnswer():
|
||||||
|
|
|
@ -2,6 +2,6 @@ antiSpamFile = 'wordlist.txt'
|
||||||
blacklistFile = 'word_blacklist.txt'
|
blacklistFile = 'word_blacklist.txt'
|
||||||
configFile = 'config.json'
|
configFile = 'config.json'
|
||||||
appName = 'CatAsk'
|
appName = 'CatAsk'
|
||||||
version = '1.1.0'
|
version = '1.2.0'
|
||||||
# id (identifier) is to be interpreted as described in https://semver.org/#spec-item-9
|
# id (identifier) is to be interpreted as described in https://semver.org/#spec-item-9
|
||||||
version_id = '-alpha'
|
version_id = '-alpha'
|
||||||
|
|
11
functions.py
|
@ -110,9 +110,12 @@ def renderMarkdown(text):
|
||||||
'i',
|
'i',
|
||||||
'br',
|
'br',
|
||||||
's',
|
's',
|
||||||
'del'
|
'del',
|
||||||
|
'a'
|
||||||
]
|
]
|
||||||
allowed_attrs = {}
|
allowed_attrs = {
|
||||||
|
'a': 'href'
|
||||||
|
}
|
||||||
# hard_wrap=True means that newlines will be
|
# hard_wrap=True means that newlines will be
|
||||||
# converted into <br> tags
|
# converted into <br> tags
|
||||||
#
|
#
|
||||||
|
@ -141,8 +144,8 @@ def generateMetadata(question=None, answer=None):
|
||||||
# if question is specified, generate metadata for that question
|
# if question is specified, generate metadata for that question
|
||||||
if question and answer:
|
if question and answer:
|
||||||
metadata.update({
|
metadata.update({
|
||||||
'title': trimContent(f"{question['content']}", 150) + " | " + cfg['instance']['title'],
|
'title': trimContent(question['content'], 150) + " | " + cfg['instance']['title'],
|
||||||
'description': trimContent(f"{answer['content']}", 150),
|
'description': trimContent(answer['content'], 150),
|
||||||
'url': cfg['instance']['fullBaseUrl'] + url_for('viewQuestion', question_id=question['id']),
|
'url': cfg['instance']['fullBaseUrl'] + url_for('viewQuestion', question_id=question['id']),
|
||||||
'image': cfg['instance']['image']
|
'image': cfg['instance']['image']
|
||||||
})
|
})
|
||||||
|
|
|
@ -11,7 +11,8 @@ CREATE TABLE IF NOT EXISTS questions (
|
||||||
creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
creation_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
content TEXT NOT NULL,
|
content TEXT NOT NULL,
|
||||||
answered BOOLEAN NOT NULL DEFAULT FALSE,
|
answered BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
answer_id INT
|
answer_id INT,
|
||||||
|
pinned BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
) ENGINE=InnoDB;
|
) ENGINE=InnoDB;
|
||||||
|
|
||||||
ALTER TABLE questions
|
ALTER TABLE questions
|
||||||
|
|
|
@ -25,7 +25,7 @@
|
||||||
--bs-primary-rgb: 127,98,240;
|
--bs-primary-rgb: 127,98,240;
|
||||||
--bs-primary-subtle: color-mix(in srgb, var(--bs-primary) 10%, transparent);
|
--bs-primary-subtle: color-mix(in srgb, var(--bs-primary) 10%, transparent);
|
||||||
--bs-danger-bg-subtle: #2c0b0e;
|
--bs-danger-bg-subtle: #2c0b0e;
|
||||||
--bs-link-color: color-mix(in srgb, var(--bs-primary) 70%, white);
|
--bs-link-color: color-mix(in srgb, var(--bs-primary) 60%, white);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn {
|
.btn {
|
||||||
|
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 686 B After Width: | Height: | Size: 710 B |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.5 KiB |
|
@ -8,6 +8,9 @@
|
||||||
id="svg9"
|
id="svg9"
|
||||||
sodipodi:docname="catask.svg"
|
sodipodi:docname="catask.svg"
|
||||||
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
inkscape:version="1.3.2 (091e20ef0f, 2023-11-25)"
|
||||||
|
inkscape:export-filename="catask-16.png"
|
||||||
|
inkscape:export-xdpi="4.0421052"
|
||||||
|
inkscape:export-ydpi="4.0421052"
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
@ -22,8 +25,8 @@
|
||||||
inkscape:pagecheckerboard="0"
|
inkscape:pagecheckerboard="0"
|
||||||
inkscape:deskcolor="#d1d1d1"
|
inkscape:deskcolor="#d1d1d1"
|
||||||
inkscape:zoom="0.48026316"
|
inkscape:zoom="0.48026316"
|
||||||
inkscape:cx="346.68493"
|
inkscape:cx="316.49315"
|
||||||
inkscape:cy="368.54794"
|
inkscape:cy="533.04109"
|
||||||
inkscape:window-width="1280"
|
inkscape:window-width="1280"
|
||||||
inkscape:window-height="962"
|
inkscape:window-height="962"
|
||||||
inkscape:window-x="0"
|
inkscape:window-x="0"
|
||||||
|
@ -42,7 +45,7 @@
|
||||||
d="M372.86 340.836C361.458 364.034 337.594 380 310 380H69.9999C42.4057 380 18.5407 364.033 7.13956 340.835C0.0540733 303.832 2.43122 265.23 21.5492 228.243C22.3857 226.625 24.2343 225.797 25.9907 226.281C61.5026 236.067 97.5669 264.887 131.247 340.717C131.983 342.374 133.752 343.367 135.546 343.105C160.424 339.463 177.96 339.001 190 339.001C202.04 339.001 219.576 339.463 244.454 343.105C246.248 343.367 248.017 342.375 248.753 340.717C282.433 264.887 318.497 236.067 354.009 226.281C355.765 225.797 357.614 226.625 358.45 228.243C377.568 265.23 379.945 303.833 372.86 340.836Z"
|
d="M372.86 340.836C361.458 364.034 337.594 380 310 380H69.9999C42.4057 380 18.5407 364.033 7.13956 340.835C0.0540733 303.832 2.43122 265.23 21.5492 228.243C22.3857 226.625 24.2343 225.797 25.9907 226.281C61.5026 236.067 97.5669 264.887 131.247 340.717C131.983 342.374 133.752 343.367 135.546 343.105C160.424 339.463 177.96 339.001 190 339.001C202.04 339.001 219.576 339.463 244.454 343.105C246.248 343.367 248.017 342.375 248.753 340.717C282.433 264.887 318.497 236.067 354.009 226.281C355.765 225.797 357.614 226.625 358.45 228.243C377.568 265.23 379.945 303.833 372.86 340.836Z"
|
||||||
fill="#DA75FF"
|
fill="#DA75FF"
|
||||||
id="path1"
|
id="path1"
|
||||||
style="fill:#e59bff;fill-opacity:1" />
|
style="fill:#caacff;fill-opacity:1" />
|
||||||
<mask
|
<mask
|
||||||
id="path-3-inside-1_603_9"
|
id="path-3-inside-1_603_9"
|
||||||
fill="white">
|
fill="white">
|
||||||
|
@ -77,12 +80,14 @@
|
||||||
gradientUnits="userSpaceOnUse">
|
gradientUnits="userSpaceOnUse">
|
||||||
<stop
|
<stop
|
||||||
stop-color="#CE95FF"
|
stop-color="#CE95FF"
|
||||||
id="stop4" />
|
id="stop4"
|
||||||
|
offset="0"
|
||||||
|
style="stop-color:#9a69ce;stop-opacity:1;" />
|
||||||
<stop
|
<stop
|
||||||
offset="1"
|
offset="1"
|
||||||
stop-color="#8B52BC"
|
stop-color="#8B52BC"
|
||||||
id="stop5"
|
id="stop5"
|
||||||
style="stop-color:#6a3a93;stop-opacity:1;" />
|
style="stop-color:#6f42c1;stop-opacity:1;" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint1_linear_603_9"
|
id="paint1_linear_603_9"
|
||||||
|
@ -95,13 +100,13 @@
|
||||||
<stop
|
<stop
|
||||||
stop-color="#CE5AFF"
|
stop-color="#CE5AFF"
|
||||||
id="stop6"
|
id="stop6"
|
||||||
offset="0"
|
offset="0.2899459"
|
||||||
style="stop-color:#ffffff;stop-opacity:1;" />
|
style="stop-color:#ffffff;stop-opacity:1;" />
|
||||||
<stop
|
<stop
|
||||||
offset="1"
|
offset="1"
|
||||||
stop-color="#C12FFF"
|
stop-color="#C12FFF"
|
||||||
id="stop7"
|
id="stop7"
|
||||||
style="stop-color:#d973ff;stop-opacity:1;" />
|
style="stop-color:#caacff;stop-opacity:1;" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="paint2_linear_603_9"
|
id="paint2_linear_603_9"
|
||||||
|
|
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.8 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 9.9 KiB After Width: | Height: | Size: 9.7 KiB |
Before Width: | Height: | Size: 686 B After Width: | Height: | Size: 710 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
@ -42,7 +42,7 @@
|
||||||
<input type="hidden" id="lockInbox" name="lockInbox" value="{{ cfg.lockInbox }}">
|
<input type="hidden" id="lockInbox" name="lockInbox" value="{{ cfg.lockInbox }}">
|
||||||
<label for="_lockInbox" class="form-check-label">Lock inbox and don't allow new questions</label>
|
<label for="_lockInbox" class="form-check-label">Lock inbox and don't allow new questions</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-check mb-3">
|
<div class="form-check mb-2">
|
||||||
<input
|
<input
|
||||||
class="form-check-input"
|
class="form-check-input"
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
|
@ -53,6 +53,17 @@
|
||||||
<input type="hidden" id="allowAnonQuestions" name="allowAnonQuestions" value="{{ cfg.allowAnonQuestions }}">
|
<input type="hidden" id="allowAnonQuestions" name="allowAnonQuestions" value="{{ cfg.allowAnonQuestions }}">
|
||||||
<label for="_allowAnonQuestions" class="form-check-label">Allow anonymous questions</label>
|
<label for="_allowAnonQuestions" class="form-check-label">Allow anonymous questions</label>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-check mb-3">
|
||||||
|
<input
|
||||||
|
class="form-check-input"
|
||||||
|
type="checkbox"
|
||||||
|
name="_showQuestionCount"
|
||||||
|
id="_showQuestionCount"
|
||||||
|
value="{{ cfg.showQuestionCount }}"
|
||||||
|
{% if cfg.showQuestionCount == true %}checked{% endif %}>
|
||||||
|
<input type="hidden" id="showQuestionCount" name="showQuestionCount" value="{{ cfg.showQuestionCount }}">
|
||||||
|
<label for="_showQuestionCount" class="form-check-label">Show question count in homepage</label>
|
||||||
|
</div>
|
||||||
<div class="form-group mb-3">
|
<div class="form-group mb-3">
|
||||||
<button type="submit" class="btn btn-primary mt-3">
|
<button type="submit" class="btn btn-primary mt-3">
|
||||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||||
|
|
|
@ -41,21 +41,21 @@
|
||||||
<body class="ms-2 me-2 mb-2">
|
<body class="ms-2 me-2 mb-2">
|
||||||
<a class="visually-hidden-focusable" href="#main-content">Skip to content</a>
|
<a class="visually-hidden-focusable" href="#main-content">Skip to content</a>
|
||||||
<div class="container-fluid">
|
<div class="container-fluid">
|
||||||
{% if logged_in %}
|
<div class="d-flex {% if logged_in %}justify-content-between {% endif %}align-items-center mt-3 {% if logged_in %}mb-3{% endif %}">
|
||||||
<div class="d-flex justify-content-between align-items-center mt-3 {% if logged_in %}mb-3{% endif %}">
|
|
||||||
<ul class="nav nav-underline position-relative {% if not logged_in %}mb-3{% endif %}">
|
<ul class="nav nav-underline position-relative {% if not logged_in %}mb-3{% endif %}">
|
||||||
<li class="nav-item d-flex align-items-center"><a href="{{ url_for('index') }}"><img src="{{ url_for('static', filename='icons/catask.svg') }}" width="32" height="32"></a></li>
|
<li class="nav-item d-flex align-items-center"><a href="{{ url_for('index') }}"><img src="{{ url_for('static', filename='icons/catask.svg') }}" width="32" height="32"></a></li>
|
||||||
<li class="nav-item"><a class="nav-link {{ homeLink }}" id="home-link" href="{{ url_for('index') }}">Home</a>
|
<li class="nav-item"><a class="nav-link {{ homeLink }}" id="home-link" href="{{ url_for('index') }}">Home</a>
|
||||||
|
{% if logged_in %}
|
||||||
<li class="nav-item"><a class="nav-link {{ inboxLink }}" id="inbox-link" href="{{ url_for('inbox') }}">Inbox</a>
|
<li class="nav-item"><a class="nav-link {{ inboxLink }}" id="inbox-link" href="{{ url_for('inbox') }}">Inbox</a>
|
||||||
<li class="nav-item"><a class="nav-link {{ adminLink }}" id="admin-link" href="{{ url_for('admin.index') }}">Admin</a></li>
|
<li class="nav-item"><a class="nav-link {{ adminLink }}" id="admin-link" href="{{ url_for('admin.index') }}">Admin</a></li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
|
{% if logged_in %}
|
||||||
<ul class="nav nav-underline m-0">
|
<ul class="nav nav-underline m-0">
|
||||||
<li><a class="nav-link" href="{{ url_for('admin.logout') }}">Logout</a></li>
|
<li><a class="nav-link" href="{{ url_for('admin.logout') }}">Logout</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
|
||||||
<div class="mt-3 mb-3" aria-hidden="true"></div>
|
|
||||||
{% endif %}
|
|
||||||
{% with messages = get_flashed_messages(with_categories=True) %}
|
{% with messages = get_flashed_messages(with_categories=True) %}
|
||||||
{% if messages %}
|
{% if messages %}
|
||||||
{% for category, message in messages %}
|
{% for category, message in messages %}
|
||||||
|
|
|
@ -3,49 +3,60 @@
|
||||||
{% set inboxLink = 'active' %}
|
{% set inboxLink = 'active' %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{% if questions != [] %}
|
{% if questions != [] %}
|
||||||
{% for question in questions %}
|
<h3 class="fs-4">{{ len(questions) }} <span class="fw-light">question(s)</span></h3>
|
||||||
<div class="card mb-3 mt-3 alert-placeholder" id="question-{{ question.id }}">
|
<div class="row">
|
||||||
<div class="card-header">
|
{% for question in questions %}
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="col-sm-8 m-auto">
|
||||||
<h5 class="card-title mt-1 mb-1">{% if question.from_who %}{{ question.from_who }}{% else %}Anonymous{% endif %}</h5>
|
<div class="card mb-3 mt-3 alert-placeholder" id="question-{{ question.id }}">
|
||||||
<h6 class="card-subtitle fw-light text-body-secondary" data-bs-toggle="tooltip" data-bs-title="{{ question.creation_date }}" data-bs-placement="top">{{ formatRelativeTime(str(question.creation_date)) }}</h6>
|
<div class="card-header">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<h5 class="card-title mt-1 mb-1">
|
||||||
|
{% if question.from_who %}
|
||||||
|
{{ question.from_who }}
|
||||||
|
{% else %}
|
||||||
|
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }}
|
||||||
|
{% endif %}
|
||||||
|
</h5>
|
||||||
|
<h6 class="card-subtitle fw-light text-body-secondary" data-bs-toggle="tooltip" data-bs-title="{{ question.creation_date }}" data-bs-placement="top">{{ formatRelativeTime(str(question.creation_date)) }}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form hx-post="{{ url_for('api.addAnswer', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">
|
||||||
|
<div class="form-group d-sm-grid d-md-block gap-2">
|
||||||
|
<textarea class="form-control mb-2" required name="answer" id="answer-{{ question.id }}" placeholder="Write your answer..."></textarea>
|
||||||
|
<div class="d-flex flex-sm-column flex-md-row-reverse gap-2">
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
||||||
|
<span class="visually-hidden" role="status">Loading...</span>
|
||||||
|
Answer
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#question-{{ question.id }}-modal">Delete</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
<div class="modal fade" id="question-{{ question.id }}-modal" tabindex="-1" aria-labelledby="q-{{ question.id }}-modal-label" aria-hidden="true">
|
||||||
</div>
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
<div class="card-body">
|
<div class="modal-content">
|
||||||
<form hx-post="{{ url_for('api.addAnswer', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">
|
<div class="modal-header">
|
||||||
<div class="form-group d-sm-grid d-md-block gap-2">
|
<h1 class="modal-title fs-5" id="q-{{ question.id }}-modal-label">Confirmation</h1>
|
||||||
<textarea class="form-control mb-2" required name="answer" id="answer-{{ question.id }}" placeholder="Write your answer..."></textarea>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
<div class="d-flex flex-sm-column flex-md-row-reverse gap-2">
|
</div>
|
||||||
<button type="submit" class="btn btn-primary">
|
<div class="modal-body">
|
||||||
<span class="spinner-border spinner-border-sm htmx-indicator" aria-hidden="true"></span>
|
<p>Are you sure you want to delete this question?</p>
|
||||||
<span class="visually-hidden" role="status">Loading...</span>
|
</div>
|
||||||
Answer
|
<div class="modal-footer">
|
||||||
</button>
|
<button type="button" class="btn" data-bs-dismiss="modal">Cancel</button>
|
||||||
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#question-{{ question.id }}-modal">Delete</button>
|
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Confirm</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal fade" id="question-{{ question.id }}-modal" tabindex="-1" aria-labelledby="q-{{ question.id }}-modal-label" aria-hidden="true">
|
|
||||||
<div class="modal-dialog modal-dialog-centered">
|
|
||||||
<div class="modal-content">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h1 class="modal-title fs-5" id="q-{{ question.id }}-modal-label">Confirmation</h1>
|
|
||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<p>Are you sure you want to delete this question?</p>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn" data-bs-dismiss="modal">Cancel</button>
|
|
||||||
<button type="button" class="btn btn-danger" data-bs-dismiss="modal" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Confirm</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
|
||||||
{% else %}
|
{% else %}
|
||||||
<h2 class="text-center mt-5">Inbox is currently empty.</h2>
|
<h2 class="text-center mt-5">Inbox is currently empty.</h2>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
|
@ -40,6 +40,10 @@
|
||||||
</div>
|
</div>
|
||||||
{% if combined %}
|
{% if combined %}
|
||||||
<div class="col-sm-8">
|
<div class="col-sm-8">
|
||||||
|
{% if cfg.showQuestionCount == true %}
|
||||||
|
<h3 class="fs-4">{{ len(combined) }} <span class="fw-light">question(s)</span></h3>
|
||||||
|
{% endif %}
|
||||||
|
<div id="top-response-container"></div>
|
||||||
{% for item in combined %}
|
{% for item in combined %}
|
||||||
<div class="card mt-3 mb-3" id="question-{{ item.question.id }}">
|
<div class="card mt-3 mb-3" id="question-{{ item.question.id }}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
|
@ -51,28 +55,39 @@
|
||||||
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }}
|
<i class="bi bi-incognito" data-bs-toggle="tooltip" data-bs-title="This question was asked anonymously" data-bs-placement="top"></i> {{ cfg.anonName }}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</h5>
|
</h5>
|
||||||
<h6 class="card-subtitle fw-light text-body-secondary" data-bs-toggle="tooltip" data-bs-title="{{ item.question.creation_date }}" data-bs-placement="top">{{ formatRelativeTime(str(item.question.creation_date)) }}</h6>
|
<h6 class="card-subtitle fw-light text-body-secondary" data-bs-toggle="tooltip" data-bs-title="{{ item.question.creation_date.strftime("%B %d, %Y %H:%M") }}" data-bs-placement="top">{{ formatRelativeTime(str(item.question.creation_date)) }}</h6>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-text markdown-content">{{ item.question.content | render_markdown }}</div>
|
<div class="card-text markdown-content">{{ item.question.content | render_markdown }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<a href="{{ url_for('viewQuestion', question_id=item.question.id) }}" class="text-decoration-none text-reset">
|
{% for answer in item.answers %}
|
||||||
{% for answer in item.answers %}
|
<div class="markdown-content">{{ answer.content | render_markdown }}</div>
|
||||||
<div class="markdown-content">{{ answer.content }}</div>
|
</div>
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
<div class="card-footer pt-0 pb-0 ps-3 pe-2 text-body-secondary d-flex justify-content-between align-items-center">
|
<div class="card-footer pt-0 pb-0 ps-3 pe-2 text-body-secondary d-flex justify-content-between align-items-center">
|
||||||
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
<div>
|
||||||
|
<span class="fs-6" data-bs-toggle="tooltip" data-bs-title="{{ answer.creation_date.strftime("%B %d, %Y %H:%M") }}">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
||||||
|
{% if item.question.pinned %}
|
||||||
|
<span class="ms-1"><i class="bi bi-pin"></i> <span class="fw-medium">Pinned</span></span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<div class="dropdown">
|
<div class="d-flex align-items-center">
|
||||||
<button class="btn btn-sm pt-2 pb-2 no-arrow text-body-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i style="font-size: 1.2rem;" class="bi bi-three-dots"></i></button>
|
<a href="{{ url_for('viewQuestion', question_id=item.question.id) }}" class="btn pt-2 pb-2 text-body-secondary" data-bs-toggle="tooltip" data-bs-title="View question"><i class="bi bi-box-arrow-up-right"></i></a>
|
||||||
<ul class="dropdown-menu">
|
<div class="dropdown">
|
||||||
<li><button class="dropdown-item" onclick="copy({{ item.question.id }})">Copy link</button></li>
|
<button class="btn btn-sm pt-2 pb-2 no-arrow text-body-secondary dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i style="font-size: 1.2rem;" class="bi bi-three-dots"></i></button>
|
||||||
{% if logged_in %}
|
<ul class="dropdown-menu">
|
||||||
<li><button class="bg-hover-danger text-danger dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=item.question.id) }}" hx-target="#question-{{ item.question.id }}" hx-swap="none">Return to inbox</button></li>
|
<li><button class="dropdown-item" onclick="copy({{ item.question.id }})">Copy link</button></li>
|
||||||
{% endif %}
|
{% if logged_in %}
|
||||||
</ul>
|
{% if not item.question.pinned %}
|
||||||
</div>
|
<li><button class="dropdown-item" hx-post="{{ url_for('api.pinQuestion', question_id=item.question.id) }}" hx-target="#top-response-container" hx-swap="none">Pin</button></li>
|
||||||
|
{% else %}
|
||||||
|
<li><button class="dropdown-item" hx-post="{{ url_for('api.unpinQuestion', question_id=item.question.id) }}" hx-target="#top-response-container" hx-swap="none">Unpin</button></li>
|
||||||
|
{% endif %}
|
||||||
|
<li><button class="bg-hover-danger text-danger dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=item.question.id) }}" hx-target="#question-{{ item.question.id }}" hx-swap="none">Return to inbox</button></li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
{% block title %}"{{ trimContent(question.content, 15) }}" - "{{ trimContent(answer.content, 15) }}"{% endblock %}
|
{% block title %}"{{ trimContent(question.content, 15) }}" - "{{ trimContent(answer.content, 15) }}"{% endblock %}
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-6 m-auto">
|
<div class="col-sm-8 m-auto">
|
||||||
<div class="card mt-2 mb-2" id="question-{{ question.id }}">
|
<div class="card mt-2 mb-2" id="question-{{ question.id }}">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<div class="d-flex justify-content-between align-items-center">
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
@ -12,7 +12,7 @@
|
||||||
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
<div class="card-text markdown-content">{{ question.content | render_markdown }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="mb-0">{{ answer.content }}</p>
|
<div class="markdown-content">{{ answer.content | render_markdown }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer pt-0 pb-0 ps-3 pe-2 text-body-secondary d-flex justify-content-between align-items-center">
|
<div class="card-footer pt-0 pb-0 ps-3 pe-2 text-body-secondary d-flex justify-content-between align-items-center">
|
||||||
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
|
||||||
|
|