0.1.1 - small fixes + installation instructions

This commit is contained in:
mystieneko 2024-08-28 12:20:25 +03:00
parent 046bcf740e
commit 07e35c9db5
10 changed files with 164 additions and 136 deletions

6
.env.example Normal file
View file

@ -0,0 +1,6 @@
DB_HOST = 127.0.0.1
DB_NAME = catask
DB_USER =
DB_PASS =
ADMIN_PASSWORD =
APP_SECRET =

View file

@ -1,2 +1,24 @@
# catask-server
# CatAsk
## Installation
Clone this repository: `git clone https://git.gay/mst/catask.git`
### VPS-specific
Go into the cloned repository, create a virtual environment and activate it:
```python -m venv venv && . venv/bin/activate```
Install required packages:
```pip install -r requirements.txt```
### Shared hosting-specific
If your shared hosting provider supports [WSGI](https://w.wiki/_vTN2), [FastCGI](https://w.wiki/9EeQ), or something similar, use it (technically any CGI protocol could work)
## Post-installation
Create a MySQL/MariaDB database and connect MDFlare to it (in `.env` file)
Then init the database: `flask init-db`
If that doesn't work, try importing schema.sql into the created database manually
Rename `.env.example` to `.env`, configure all values there, and edit variable `fullBaseUrl` in `config.py` file
Then run it:
`flask run` or `gunicorn -w 4 app:app` (if you have gunicorn installed) or `python3 app.py`
If you want it to be accessible on a specific host address, specify a `--host` option (e.g. `--host 0.0.0.0`) to `flask run`
## Usage
Works on `127.0.0.1:5000` (`flask run` or `python app.py`) or `127.0.0.1:8000` (`gunicorn -w 4 app:app`), may work in a production environment

3
app.py
View file

@ -15,7 +15,8 @@ logged_in = False
load_dotenv()
app = Flask(cfg.appName)
app.config["SECRET_KEY"] = os.environ.get("APP_SECRET")
print(os.environ.get("APP_SECRET"))
app.secret_key = os.environ.get("APP_SECRET")
# -- blueprints --
api_bp = Blueprint('api', cfg.appName)

View file

@ -1,3 +1,3 @@
antiSpamFile = 'wordlist.txt'
blacklistFile = 'word_blacklist.txt'
version = '0.1.0'
version = '0.1.1'

View file

@ -1,3 +1,4 @@
flask
python-dotenv
mysql-connector-python==8.2.0
humanize

View file

@ -1,5 +1,5 @@
{% extends 'base.html' %}
{% block title %}Admin Login{% endblock %}
{% block content %}
<div class="card">
<div class="card-body">

View file

@ -12,41 +12,40 @@
<title>{% block title %}{% endblock %} - {{ cfg.appName }}</title>
</head>
<body class="m-2">
<div class="container-fluid">
{% if logged_in %}
<div class="d-flex justify-content-between">
{% endif %}
<ul class="nav nav-underline mt-2 mb-2">
<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 {{ adminLink }}" id="admin-link" href="{{ url_for('admin.index') }}">Admin</a></li>
{%- endif -%}
</ul>
{% if logged_in %}
<ul class="nav nav-underline mt-2 mb-2">
<li><a class="nav-link" href="{{ url_for('admin.logout') }}">Logout</a></li>
</ul>
</div>
{% endif %}
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible" role="alert">
<div>{{ message }}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
<div class="container-fluid">
{% if logged_in %}
<div class="d-flex justify-content-between">
{% endif %}
<ul class="nav nav-underline mt-2 mb-2">
<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 {{ adminLink }}" id="admin-link" href="{{ url_for('admin.index') }}">Admin</a></li>
{%- endif -%}
</ul>
{% if logged_in %}
<ul class="nav nav-underline mt-2 mb-2">
<li><a class="nav-link" href="{{ url_for('admin.logout') }}">Logout</a></li>
</ul>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
<script src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
{% block scripts %}
{% endblock %}
</div>
<footer class="py-3 my-4">
<p class="text-center text-body-secondary">CatAsk v{{ version }}</p>
</footer>
{% endif %}
{% with messages = get_flashed_messages(with_categories=True) %}
{% if messages %}
{% for category, message in messages %}
<div class="alert alert-{{ category }} alert-dismissible" role="alert">
<div>{{ message }}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
{% endif %}
{% endwith %}
{% block content %}{% endblock %}
</div>
<script src="{{ url_for('static', filename='js/htmx.min.js') }}"></script>
<script src="{{ url_for('static', filename='js/bootstrap.min.js') }}"></script>
{% block scripts %}{% endblock %}
<footer class="py-3 my-4">
<p class="text-center text-body-secondary">CatAsk v{{ version }}</p>
</footer>
</body>
</html>

View file

@ -5,20 +5,20 @@
{% if questions != [] %}
{% for question in questions %}
<div class="card mb-2 mt-2 alert-placeholder" id="question-{{ question.id }}">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mt-0">{% if question.from_who %}{{ question.from_who }}{% else %}Anonymous{% endif %}</h5>
<h6 class="card-subtitle fw-light text-body-secondary">{{ formatRelativeTime(str(question.creation_date)) }}</h6>
</div>
<p>{{ question.content }}</span>
<form hx-post="{{ url_for('api.addAnswer', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">
<div class="form-group d-grid gap-2">
<textarea class="form-control" required name="answer" id="answer-{{ question.id }}" placeholder="Write your answer..."></textarea>
<button type="submit" class="btn btn-primary">Answer</button>
<button type="button" class="btn btn-outline-danger" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Delete</button>
</div>
</form>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mt-0">{% if question.from_who %}{{ question.from_who }}{% else %}Anonymous{% endif %}</h5>
<h6 class="card-subtitle fw-light text-body-secondary">{{ formatRelativeTime(str(question.creation_date)) }}</h6>
</div>
<p>{{ question.content }}</p>
<form hx-post="{{ url_for('api.addAnswer', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">
<div class="form-group d-grid gap-2">
<textarea class="form-control" required name="answer" id="answer-{{ question.id }}" placeholder="Write your answer..."></textarea>
<button type="submit" class="btn btn-primary">Answer</button>
<button type="button" class="btn btn-outline-danger" hx-delete="{{ url_for('api.deleteQuestion', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Delete</button>
</div>
</form>
</div>
</div>
{% endfor %}
{% else %}
@ -30,10 +30,10 @@
const appendAlert = (elementId, message, type) => {
const alertPlaceholder = document.getElementById(elementId);
const alertHtml = `
<div class="alert alert-${type} alert-dismissible" role="alert">
<div>${message}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
<div class="alert alert-${type} alert-dismissible" role="alert">
<div>${message}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
`;
alertPlaceholder.outerHTML = alertHtml;

View file

@ -3,52 +3,51 @@
{% set homeLink = 'active' %}
{% block content %}
<div class="mt-3 mb-5">
<h2>Ask a question</h2>
<form hx-post="{{ url_for('api.addQuestion') }}" id="question-form" hx-target="#response-container" hx-swap="none">
<div class="form-group d-grid gap-2">
<input class="form-control" type="text" name="from_who" id="from_who" placeholder="Name (optional)">
<textarea class="form-control" required name="question" id="question" placeholder="Write your question..."></textarea>
<label for="antispam">Anti-spam: please enter the word <code>{{ getRandomWord().upper() }}</code> in lowercase</label>
<input class="form-control" type="text" required name="antispam" id="antispam">
<button type="submit" class="btn btn-primary">Ask</button>
</div>
</form>
<div id="response-container" class="mt-3"></div>
<h2>Ask a question</h2>
<form hx-post="{{ url_for('api.addQuestion') }}" id="question-form" hx-target="#response-container" hx-swap="none">
<div class="form-group d-grid gap-2">
<input class="form-control" type="text" name="from_who" id="from_who" placeholder="Name (optional)">
<textarea class="form-control" required name="question" id="question" placeholder="Write your question..."></textarea>
<label for="antispam">Anti-spam: please enter the word <code>{{ getRandomWord().upper() }}</code> in lowercase</label>
<input class="form-control" type="text" required name="antispam" id="antispam">
<button type="submit" class="btn btn-primary">Ask</button>
</div>
</form>
<div id="response-container" class="mt-3"></div>
</div>
{% for item in combined %}
<div class="card mt-2 mb-2" id="question-{{ item.question.id }}">
<div class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mt-1 mb-1">{% if item.question.from_who %}{{ item.question.from_who }}{% else %}Anonymous{% endif %}</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>
</div>
<p class="card-text">{{ item.question.content }}</p>
</div>
<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 %}
<p class="mb-0">{{ answer.content }}</p>
</div>
</a>
<div class="card-footer text-body-secondary d-flex justify-content-between align-items-center">
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
{% endfor %}
<div class="dropdown">
<a class="text-reset btn-sm icon-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i style="font-size: 1.2rem;" class="bi bi-three-dots"></i></a>
<ul class="dropdown-menu">
<li><button class="dropdown-item" onclick="copy({{ item.question.id }})">Copy link</button></li>
{% if logged_in %}
<li><button class="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 class="card-header">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mt-1 mb-1">{% if item.question.from_who %}{{ item.question.from_who }}{% else %}Anonymous{% endif %}</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>
</div>
<p class="card-text">{{ item.question.content }}</p>
</div>
<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 %}
<p class="mb-0">{{ answer.content }}</p>
</div>
</a>
<div class="card-footer text-body-secondary d-flex justify-content-between align-items-center">
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
{% endfor %}
<div class="dropdown">
<a class="text-reset btn-sm icon-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i style="font-size: 1.2rem;" class="bi bi-three-dots"></i></a>
<ul class="dropdown-menu">
<li><button class="dropdown-item" onclick="copy({{ item.question.id }})">Copy link</button></li>
{% if logged_in %}
<li><button class="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>
{% endfor %}
{% endblock %}
{% block scripts %}
<script>
console.log(navigator.clipboard);
function copy(questionId) {
navigator.clipboard.writeText("{{ cfg.fullBaseUrl }}/q/" + questionId + "/")
}
@ -60,28 +59,28 @@
const tooltipList = [...tooltipTriggerList].map(tooltipTriggerEl => new bootstrap.Tooltip(tooltipTriggerEl));
const appendAlert = (elementId, message, type, onclick) => {
const alertPlaceholder = document.querySelector(`#${elementId}`);
const alertHtml = `
<div class="alert alert-${type} alert-dismissible" role="alert">
<div>${message}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick=${onclick}></button>
</div>
`;
alertPlaceholder.outerHTML = alertHtml;
const alertPlaceholder = document.querySelector(`#${elementId}`);
const alertHtml = `
<div class="alert alert-${type} alert-dismissible" role="alert">
<div>${message}</div>
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close" onclick=${onclick}></button>
</div>
`;
alertPlaceholder.outerHTML = alertHtml;
}
document.addEventListener('htmx:afterRequest', function(event) {
const jsonResponse = event.detail.xhr.response;
if (jsonResponse) {
const parsed = JSON.parse(jsonResponse);
const alertType = event.detail.successful ? 'success' : 'danger';
msgType = event.detail.successful ? parsed.message : parsed.error;
const targetElementId = event.detail.target.id;
onclick = event.detail.successful ? null : "window.location.reload()";
appendAlert(targetElementId, msgType, alertType, onclick);
document.getElementById('question-form').reset();
}
})
document.addEventListener('htmx:afterRequest', function(event) {
const jsonResponse = event.detail.xhr.response;
if (jsonResponse) {
const parsed = JSON.parse(jsonResponse);
const alertType = event.detail.successful ? 'success' : 'danger';
msgType = event.detail.successful ? parsed.message : parsed.error;
const targetElementId = event.detail.target.id;
onclick = event.detail.successful ? null : "window.location.reload()";
appendAlert(targetElementId, msgType, alertType, onclick);
document.getElementById('question-form').reset();
}
})
</script>
{% endblock %}

View file

@ -2,25 +2,25 @@
{% block content %}
<div class="card mt-2 mb-2">
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mt-0">{% if question.from_who %}{{ question.from_who }}{% else %}Anonymous{% endif %}</h5>
<h6 class="card-subtitle fw-light text-body-secondary">{{ formatRelativeTime(str(question.creation_date)) }}</h6>
</div>
<p class="card-text">{{ question.content }}</p>
<p class="mb-0">{{ answer.content }}</p>
</div>
<div class="card-footer text-body-secondary d-flex justify-content-between align--center">
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
<div class="dropdown">
<a class="text-reset btn-sm icon-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i style="font-size: 1.2rem;" class="bi bi-three-dots"></i></a>
<ul class="dropdown-menu">
<li><button class="dropdown-item" onclick="copy({{ question.id }})">Copy link</button></li>
{% if logged_in %}
<li><button class="dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Return to inbox</button></li>
{% endif %}
</ul>
</div>
</div>
<div class="card-body">
<div class="d-flex justify-content-between align-items-center">
<h5 class="card-title mt-0">{% if question.from_who %}{{ question.from_who }}{% else %}Anonymous{% endif %}</h5>
<h6 class="card-subtitle fw-light text-body-secondary">{{ formatRelativeTime(str(question.creation_date)) }}</h6>
</div>
<p class="card-text">{{ question.content }}</p>
<p class="mb-0">{{ answer.content }}</p>
</div>
<div class="card-footer text-body-secondary d-flex justify-content-between align--center">
<span class="fs-6">answered {{ formatRelativeTime(str(answer.creation_date)) }}</span>
<div class="dropdown">
<a class="text-reset btn-sm icon-link dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"><i style="font-size: 1.2rem;" class="bi bi-three-dots"></i></a>
<ul class="dropdown-menu">
<li><button class="dropdown-item" onclick="copy({{ question.id }})">Copy link</button></li>
{% if logged_in %}
<li><button class="dropdown-item" hx-post="{{ url_for('api.returnToInbox', question_id=question.id) }}" hx-target="#question-{{ question.id }}" hx-swap="none">Return to inbox</button></li>
{% endif %}
</ul>
</div>
</div>
</div>
{% endblock %}