From f4c75526c37bbdad7033c977649921dd0b8d44a9 Mon Sep 17 00:00:00 2001 From: Markus Zimmerberger Date: Wed, 9 Sep 2026 17:39:25 +0200 Subject: [PATCH] Add workflow --- .gitea/workflows/deploy.yml | 40 ++++++++++ add_entry.html | 39 ++++++++++ app.py | 141 ++++++++++++++++++++++++++++++++++++ base.html | 69 ++++++++++++++++++ config.py | 10 +++ dashboard.html | 41 +++++++++++ gitignore | 5 ++ login.html | 15 ++++ models.py | 53 ++++++++++++++ pbt.service | 14 ++++ register.html | 18 +++++ requirements.txt | 7 ++ 12 files changed, 452 insertions(+) create mode 100644 .gitea/workflows/deploy.yml create mode 100644 add_entry.html create mode 100644 app.py create mode 100644 base.html create mode 100644 config.py create mode 100644 dashboard.html create mode 100644 gitignore create mode 100644 login.html create mode 100644 models.py create mode 100644 pbt.service create mode 100644 register.html create mode 100644 requirements.txt diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..d48ec98 --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,40 @@ +name: Deploy PBT + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: SSH-Key laden + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.PBT_DEPLOY_KEY }} + + - name: Host-Key vertrauen + run: ssh-keyscan -H pbt >> ~/.ssh/known_hosts + + - name: Dateien auf die pbt-LXC syncen + run: | + rsync -avz --delete \ + --exclude '.git' \ + --exclude '.gitea' \ + --exclude 'venv' \ + --exclude '__pycache__' \ + ./ deploy@pbt:/opt/pbt/ + + - name: Abhängigkeiten installieren & Service neu starten + run: | + ssh deploy@pbt ' + set -e + cd /opt/pbt + python3 -m venv venv + source venv/bin/activate + pip install -q -r requirements.txt + sudo /usr/bin/systemctl restart pbt + ' diff --git a/add_entry.html b/add_entry.html new file mode 100644 index 0000000..8066a0d --- /dev/null +++ b/add_entry.html @@ -0,0 +1,39 @@ +{% extends "base.html" %} +{% block title %}Neue Fahrt – PBT{% endblock %} +{% block content %} +

Neue Fahrt erfassen

+
+ + + + + + + + + + + + + + + + + + + + + + +
+{% endblock %} diff --git a/app.py b/app.py new file mode 100644 index 0000000..e5d155f --- /dev/null +++ b/app.py @@ -0,0 +1,141 @@ +from datetime import date + +from flask import Flask, render_template, redirect, url_for, request, flash +from flask_login import ( + LoginManager, + login_user, + logout_user, + login_required, + current_user, +) + +from config import Config +from models import db, User, Fahrt, VERKEHRSMITTEL_OPTIONEN + +login_manager = LoginManager() +login_manager.login_view = "login" + + +def create_app(): + app = Flask(__name__) + app.config.from_object(Config) + + db.init_app(app) + login_manager.init_app(app) + + with app.app_context(): + db.create_all() + + register_routes(app) + return app + + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) + + +def register_routes(app): + @app.route("/") + def index(): + if current_user.is_authenticated: + return redirect(url_for("dashboard")) + return redirect(url_for("login")) + + @app.route("/register", methods=["GET", "POST"]) + def register(): + if request.method == "POST": + username = request.form["username"].strip() + email = request.form["email"].strip().lower() + password = request.form["password"] + + if User.query.filter( + (User.username == username) | (User.email == email) + ).first(): + flash("Benutzername oder E-Mail bereits vergeben.", "error") + return redirect(url_for("register")) + + user = User(username=username, email=email) + user.set_password(password) + db.session.add(user) + db.session.commit() + + login_user(user) + return redirect(url_for("dashboard")) + + return render_template("register.html") + + @app.route("/login", methods=["GET", "POST"]) + def login(): + if request.method == "POST": + username = request.form["username"].strip() + password = request.form["password"] + + user = User.query.filter_by(username=username).first() + if user is None or not user.check_password(password): + flash("Login fehlgeschlagen. Bitte prüfe deine Daten.", "error") + return redirect(url_for("login")) + + login_user(user) + return redirect(url_for("dashboard")) + + return render_template("login.html") + + @app.route("/logout") + @login_required + def logout(): + logout_user() + return redirect(url_for("login")) + + @app.route("/dashboard") + @login_required + def dashboard(): + fahrten = ( + Fahrt.query.filter_by(user_id=current_user.id) + .order_by(Fahrt.datum.desc()) + .all() + ) + return render_template("dashboard.html", fahrten=fahrten) + + @app.route("/fahrt/neu", methods=["GET", "POST"]) + @login_required + def neue_fahrt(): + if request.method == "POST": + fahrt = Fahrt( + user_id=current_user.id, + verkehrsmittel=request.form["verkehrsmittel"], + linie=request.form.get("linie", "").strip(), + von=request.form["von"].strip(), + nach=request.form["nach"].strip(), + datum=request.form.get("datum") or date.today(), + bewertung=int(request.form["bewertung"]), + kommentar=request.form.get("kommentar", "").strip(), + ) + db.session.add(fahrt) + db.session.commit() + flash("Fahrt gespeichert.", "success") + return redirect(url_for("dashboard")) + + return render_template( + "add_entry.html", + verkehrsmittel_optionen=VERKEHRSMITTEL_OPTIONEN, + heute=date.today().isoformat(), + ) + + @app.route("/fahrt//loeschen", methods=["POST"]) + @login_required + def fahrt_loeschen(fahrt_id): + fahrt = Fahrt.query.get_or_404(fahrt_id) + if fahrt.user_id != current_user.id: + flash("Nicht erlaubt.", "error") + return redirect(url_for("dashboard")) + db.session.delete(fahrt) + db.session.commit() + flash("Fahrt gelöscht.", "success") + return redirect(url_for("dashboard")) + + +app = create_app() + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5000, debug=True) diff --git a/base.html b/base.html new file mode 100644 index 0000000..d14825b --- /dev/null +++ b/base.html @@ -0,0 +1,69 @@ + + + + + + {% block title %}PBT – Öffi-Erfahrungen{% endblock %} + + + +
+

🚋 PBT – Öffi-Erfahrungen

+ +
+ + {% with messages = get_flashed_messages(with_categories=true) %} + {% for category, message in messages %} +
{{ message }}
+ {% endfor %} + {% endwith %} + + {% block content %}{% endblock %} + + diff --git a/config.py b/config.py new file mode 100644 index 0000000..98979a8 --- /dev/null +++ b/config.py @@ -0,0 +1,10 @@ +import os + +class Config: + # Beispiel: postgresql://pbt_app:PASSWORT@10.10.10.x/pbt + SQLALCHEMY_DATABASE_URI = os.environ.get( + "PBT_DATABASE_URL", + "postgresql://pbt_app:changeme@postgres/pbt", + ) + SQLALCHEMY_TRACK_MODIFICATIONS = False + SECRET_KEY = os.environ.get("PBT_SECRET_KEY", "bitte-in-produktion-aendern") diff --git a/dashboard.html b/dashboard.html new file mode 100644 index 0000000..a8abf0b --- /dev/null +++ b/dashboard.html @@ -0,0 +1,41 @@ +{% extends "base.html" %} +{% block title %}Meine Fahrten – PBT{% endblock %} +{% block content %} +

Meine Fahrten

+ + {% if not fahrten %} +

Noch keine Fahrten erfasst. Jetzt die erste hinzufügen.

+ {% else %} + + + + + + + + + + + + + {% for f in fahrten %} + + + + + + + + + {% if f.kommentar %} + + {% endif %} + {% endfor %} + +
DatumVerkehrsmittelLinieStreckeBewertung
{{ f.datum.strftime("%d.%m.%Y") }}{{ f.verkehrsmittel }}{{ f.linie or "–" }}{{ f.von }} → {{ f.nach }}{{ "★" * f.bewertung }}{{ "☆" * (5 - f.bewertung) }} +
+ +
+
{{ f.kommentar }}
+ {% endif %} +{% endblock %} diff --git a/gitignore b/gitignore new file mode 100644 index 0000000..e813d97 --- /dev/null +++ b/gitignore @@ -0,0 +1,5 @@ +venv/ +__pycache__/ +*.pyc +.env +instance/ diff --git a/login.html b/login.html new file mode 100644 index 0000000..57e1907 --- /dev/null +++ b/login.html @@ -0,0 +1,15 @@ +{% extends "base.html" %} +{% block title %}Login – PBT{% endblock %} +{% block content %} +

Login

+
+ + + + + + + +
+

Noch kein Konto? Jetzt registrieren

+{% endblock %} diff --git a/models.py b/models.py new file mode 100644 index 0000000..4b72ae5 --- /dev/null +++ b/models.py @@ -0,0 +1,53 @@ +from datetime import date +from flask_sqlalchemy import SQLAlchemy +from flask_login import UserMixin +from werkzeug.security import generate_password_hash, check_password_hash + +db = SQLAlchemy() + + +class User(UserMixin, db.Model): + __tablename__ = "users" + + id = db.Column(db.Integer, primary_key=True) + username = db.Column(db.String(64), unique=True, nullable=False, index=True) + email = db.Column(db.String(120), unique=True, nullable=False, index=True) + password_hash = db.Column(db.String(255), nullable=False) + + fahrten = db.relationship( + "Fahrt", backref="user", lazy=True, cascade="all, delete-orphan" + ) + + def set_password(self, password: str) -> None: + self.password_hash = generate_password_hash(password) + + def check_password(self, password: str) -> bool: + return check_password_hash(self.password_hash, password) + + +VERKEHRSMITTEL_OPTIONEN = [ + "Bus", + "Straßenbahn", + "U-Bahn", + "S-Bahn", + "Regionalzug", + "Fernzug (ÖBB/Railjet/WESTbahn)", + "Sonstiges", +] + + +class Fahrt(db.Model): + __tablename__ = "fahrten" + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, db.ForeignKey("users.id"), nullable=False, index=True) + + verkehrsmittel = db.Column(db.String(50), nullable=False) + linie = db.Column(db.String(50)) + von = db.Column(db.String(120), nullable=False) + nach = db.Column(db.String(120), nullable=False) + datum = db.Column(db.Date, nullable=False, default=date.today) + bewertung = db.Column(db.Integer, nullable=False) # 1-5 + kommentar = db.Column(db.Text) + + erstellt_am = db.Column(db.DateTime, server_default=db.func.now()) diff --git a/pbt.service b/pbt.service new file mode 100644 index 0000000..f27aeb0 --- /dev/null +++ b/pbt.service @@ -0,0 +1,14 @@ +[Unit] +Description=PBT Flask App (Gunicorn) +After=network.target + +[Service] +User=www-data +WorkingDirectory=/opt/pbt +Environment="PBT_DATABASE_URL=postgresql://pbt_app:changeme@postgres/pbt" +Environment="PBT_SECRET_KEY=bitte-aendern" +ExecStart=/opt/pbt/venv/bin/gunicorn --workers 2 --bind 127.0.0.1:8000 app:app +Restart=always + +[Install] +WantedBy=multi-user.target diff --git a/register.html b/register.html new file mode 100644 index 0000000..e387195 --- /dev/null +++ b/register.html @@ -0,0 +1,18 @@ +{% extends "base.html" %} +{% block title %}Registrieren – PBT{% endblock %} +{% block content %} +

Registrieren

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

Schon registriert? Zum Login

+{% endblock %} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..19a5802 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Flask==3.0.3 +Flask-SQLAlchemy==3.1.1 +Flask-Login==0.6.3 +psycopg2-binary==2.9.9 +python-dotenv==1.0.1 +gunicorn==22.0.0 +email-validator==2.2.0