This commit is contained in:
26
.gitea/workflows/build.yml
Normal file
26
.gitea/workflows/build.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
name: Build,
|
||||
run-name: ${{ gitea.actor }} is building, testing and deploying the static page
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
build-image:
|
||||
runs-on: ubuntu-latest
|
||||
if: gitea.ref == 'refs/heads/main'
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
github-server-url: 'https://git.secretmine.de/'
|
||||
- name: Login to Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: git.secretmine.de
|
||||
username: ${{ secrets.REGISTRY_USER }}
|
||||
password: ${{ secrets.REGISTRY_TOKEN }}
|
||||
- name: Build Image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
file: Containerfile
|
||||
context: .
|
||||
push: true
|
||||
tags: git.secretmine.de/secretminede/couplequestions:latest
|
||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
venv/
|
||||
data/
|
||||
__pycache__/
|
||||
questions/
|
||||
18
Containerfile
Normal file
18
Containerfile
Normal file
@@ -0,0 +1,18 @@
|
||||
FROM python:3-slim-trixie
|
||||
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
locales && \
|
||||
rm -r /var/lib/apt/lists/*
|
||||
RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && \
|
||||
sed -i -e 's/# de_DE.UTF-8 UTF-8/de_DE.UTF-8 UTF-8/' /etc/locale.gen && \
|
||||
dpkg-reconfigure --frontend=noninteractive locales
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt requirements.txt
|
||||
RUN pip3 install -r requirements.txt
|
||||
COPY . /app/
|
||||
|
||||
|
||||
CMD ["python3", "-m", "flask", "run", "--host=0.0.0.0"]
|
||||
57
app.py
Normal file
57
app.py
Normal file
@@ -0,0 +1,57 @@
|
||||
from flask_bootstrap import Bootstrap5
|
||||
from flask import Flask, render_template, request
|
||||
import utils
|
||||
import random
|
||||
import config
|
||||
|
||||
app = Flask(__name__)
|
||||
bootstrap = Bootstrap5(app)
|
||||
|
||||
utils.check_conditions()
|
||||
|
||||
def get_questions(categories, num):
|
||||
question_pool = []
|
||||
weights = []
|
||||
for c in categories:
|
||||
for q in utils.get_questions_by_category(c):
|
||||
weight = utils.get_question_weights(c, q)
|
||||
question_pool.append((c, q))
|
||||
weights.append(weight)
|
||||
|
||||
|
||||
if num > len(question_pool):
|
||||
num = len(question_pool)
|
||||
if num == 0:
|
||||
return {}
|
||||
selected_question_tuples = []
|
||||
while len(selected_question_tuples) < num:
|
||||
selected = random.choices(question_pool, weights)[0]
|
||||
if selected not in selected_question_tuples:
|
||||
selected_question_tuples.append(selected)
|
||||
|
||||
questions = {}
|
||||
for category, question in selected_question_tuples:
|
||||
utils.set_question_selected(category, question)
|
||||
if category not in questions:
|
||||
questions[category] = []
|
||||
questions[category].append(question)
|
||||
utils.save_weights()
|
||||
return questions
|
||||
|
||||
|
||||
@app.route("/", methods=["POST", "GET"])
|
||||
def index():
|
||||
categories = utils.get_categories()
|
||||
selected = categories
|
||||
n = config.get_num_questions()
|
||||
questions = {}
|
||||
if request.method == "POST":
|
||||
selected = request.form.getlist("categories[]")
|
||||
n = int(request.form.get("num"))
|
||||
questions = get_questions(selected, n)
|
||||
|
||||
return render_template("index.html", categories=categories, selected=selected, n=n, questions=questions)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
app.run()
|
||||
11
config.py
Normal file
11
config.py
Normal file
@@ -0,0 +1,11 @@
|
||||
import os
|
||||
|
||||
|
||||
def get_num_questions():
|
||||
default_num = 5
|
||||
return int(os.getenv("NUM_QUESTIONS", default_num))
|
||||
|
||||
|
||||
def get_max_last_accessed_days():
|
||||
default_days = 30
|
||||
return int(os.getenv("QUESTION_LAST_ACCESSED_DAYS_MAX", default_days))
|
||||
3
requirements.txt
Normal file
3
requirements.txt
Normal file
@@ -0,0 +1,3 @@
|
||||
flask
|
||||
pyyaml
|
||||
bootstrap-flask
|
||||
87
templates/index.html
Normal file
87
templates/index.html
Normal file
@@ -0,0 +1,87 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
{% block head %}
|
||||
<!-- Required meta tags -->
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport"
|
||||
content="width=device-width, initial-scale=1, shrink-to-fit=no">
|
||||
|
||||
{% block styles %}
|
||||
<!-- Bootstrap CSS -->
|
||||
{{ bootstrap.load_css() }}
|
||||
{% endblock %}
|
||||
|
||||
<title>CoupleQuestions</title>
|
||||
{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
{% block content %}
|
||||
<div class="container">
|
||||
<h1>CoupleQuestions</h1>
|
||||
<br>
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title mb-4">Fragenkategorien auswählen</h4>
|
||||
<form method="post">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Kategorien</label>
|
||||
{% for c in categories %}
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox"
|
||||
value="{{ c }}" id="cat_{{ loop.index }}"
|
||||
name="categories[]" {% if c in selected
|
||||
%}checked{% endif %}>
|
||||
<label class="form-check-label"
|
||||
for="cat_{{ loop.index }}">
|
||||
{{ c }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Einstellungen</label>
|
||||
<div class="form-check">
|
||||
<input class="form-control" type="number"
|
||||
value="{{ n }}" id="num" name="num">
|
||||
<label class="form-label" for="num">
|
||||
Anzahl der Fragen
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">
|
||||
Fragen auswählen!
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% if questions | length > 0 %}
|
||||
<hr>
|
||||
<h2>Fragen</h2>
|
||||
<br>
|
||||
{% for category, c_questions in questions.items() %}
|
||||
<div class="card">
|
||||
<div class="card-body">
|
||||
<h4 class="card-title mb-4">{{ category }}</h4>
|
||||
<ul>
|
||||
{% for q in c_questions %}
|
||||
<li>{{ q }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<br>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block scripts %}
|
||||
{{ bootstrap.load_js() }}
|
||||
{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
102
utils.py
Normal file
102
utils.py
Normal file
@@ -0,0 +1,102 @@
|
||||
import os
|
||||
import yaml
|
||||
import json
|
||||
from datetime import datetime
|
||||
import config
|
||||
|
||||
|
||||
weights = None
|
||||
|
||||
|
||||
def check_conditions():
|
||||
folders = ["questions", "questions_custom"]
|
||||
found_one = False
|
||||
for folder in folders:
|
||||
if os.path.exists(folder):
|
||||
found_one = True
|
||||
break
|
||||
if not found_one:
|
||||
print("No questions folder found.")
|
||||
exit(1)
|
||||
|
||||
|
||||
def get_questions():
|
||||
folders = ["questions", "questions_custom"]
|
||||
questions = {}
|
||||
for folder in folders:
|
||||
if not os.path.exists(folder):
|
||||
continue
|
||||
for filename in os.listdir(folder):
|
||||
if filename.endswith(".yml") or filename.endswith(".yaml"):
|
||||
file_path = os.path.join(folder, filename)
|
||||
with open(file_path, "r") as f:
|
||||
file_questions = yaml.safe_load(f)
|
||||
for category, category_questions in file_questions.items():
|
||||
if category not in questions:
|
||||
questions[category] = []
|
||||
questions[category].extend(category_questions)
|
||||
questions[category] = list(set(questions[category]))
|
||||
|
||||
return questions
|
||||
|
||||
|
||||
def get_categories():
|
||||
return get_questions().keys()
|
||||
|
||||
|
||||
def get_questions_by_category(category):
|
||||
questions = get_questions()
|
||||
if category not in questions:
|
||||
return []
|
||||
return questions[category]
|
||||
|
||||
|
||||
def load_weights():
|
||||
os.makedirs("data/", exist_ok=True)
|
||||
fname = "data/weights.json"
|
||||
if os.path.exists(fname):
|
||||
with open(fname, "r") as fp:
|
||||
return json.load(fp)
|
||||
return {}
|
||||
|
||||
|
||||
def get_all_weights():
|
||||
global weights
|
||||
if weights is None:
|
||||
weights = load_weights()
|
||||
return weights
|
||||
|
||||
|
||||
def save_weights():
|
||||
weights = get_all_weights()
|
||||
os.makedirs("data/", exist_ok=True)
|
||||
fname = "data/weights.json"
|
||||
with open(fname, "w") as fp:
|
||||
json.dump(weights, fp, indent=2)
|
||||
|
||||
|
||||
def get_weight_index(c, q):
|
||||
return f"{c}_{q}"
|
||||
|
||||
|
||||
def get_question_weights(c, q):
|
||||
max_weight = config.get_max_last_accessed_days()
|
||||
min_weight = 1
|
||||
weights = get_all_weights()
|
||||
c_index = get_weight_index(c, q)
|
||||
last_selected_ts = 0
|
||||
if c_index in weights:
|
||||
last_selected_ts = weights[c_index]
|
||||
last_selected = datetime.fromtimestamp(last_selected_ts)
|
||||
now = datetime.now()
|
||||
days_passed = (now - last_selected).days
|
||||
days_passed = min(max_weight, days_passed)
|
||||
days_passed = max(min_weight, days_passed)
|
||||
return days_passed
|
||||
|
||||
|
||||
def set_question_selected(c, q):
|
||||
weights = get_all_weights()
|
||||
c_index = get_weight_index(c, q)
|
||||
ts = datetime.timestamp(datetime.now())
|
||||
weights[c_index] = int(ts)
|
||||
Reference in New Issue
Block a user