commit 23086b838e2f4ca425398fb000e39f1356eb8e79 Author: Timon Horlboge Date: Sun Feb 22 15:01:18 2026 +0100 initial commit diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..b8e471c --- /dev/null +++ b/.gitea/workflows/build.yml @@ -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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a2709f --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +venv/ +data/ +__pycache__/ +questions/ diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..cb1aec3 --- /dev/null +++ b/Containerfile @@ -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"] diff --git a/app.py b/app.py new file mode 100644 index 0000000..1ac72f4 --- /dev/null +++ b/app.py @@ -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() diff --git a/config.py b/config.py new file mode 100644 index 0000000..8184996 --- /dev/null +++ b/config.py @@ -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)) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..71933c7 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask +pyyaml +bootstrap-flask diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..bb91c6d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,87 @@ + + + + {% block head %} + + + + + {% block styles %} + + {{ bootstrap.load_css() }} + {% endblock %} + + CoupleQuestions + {% endblock %} + + + {% block content %} +
+

CoupleQuestions

+
+
+
+

Fragenkategorien auswählen

+
+
+ + {% for c in categories %} +
+ + +
+ {% endfor %} +
+ +
+ +
+ + +
+
+ +
+
+
+ + {% if questions | length > 0 %} +
+

Fragen

+
+ {% for category, c_questions in questions.items() %} +
+
+

{{ category }}

+
    + {% for q in c_questions %} +
  • {{ q }}
  • + {% endfor %} +
+
+
+
+ {% endfor %} + {% endif %} + +
+ + {% endblock %} + + {% block scripts %} + {{ bootstrap.load_js() }} + {% endblock %} + + diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..0c876cb --- /dev/null +++ b/utils.py @@ -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)