Project_Manager/app/projects/routes.py
2026-06-26 18:21:22 +02:00

206 lines
6.5 KiB
Python

"""Routen des Projekte-Moduls — voller Funktionsumfang aus dem HTML-Prototyp.
Alles ist login_required und strikt auf current_user gescoped. Schreibende
Aktionen laufen als POST + Redirect (PRG-Muster), damit kein Reload doppelt
ausführt.
"""
from datetime import date, datetime, timedelta
from urllib.parse import urlparse
from flask import render_template, request, redirect, url_for, flash, Response
from flask_login import login_required, current_user
from ..extensions import db
from .models import Project, Task
from .helpers import next_color, due_badge, sort_tasks, today_label
from .pdf import build_todo_pdf
from . import bp
VALID_FILTERS = {"all", "open", "overdue", "soon", "done"}
def _redirect_back():
"""Zur vorigen Ansicht zurück (erhält z.B. den Filter), sonst zur Übersicht."""
ref = request.referrer
if ref:
parsed = urlparse(ref)
if not parsed.netloc or parsed.netloc == urlparse(request.host_url).netloc:
return redirect(ref)
return redirect(url_for("projects.index"))
def _owned_project_or_none(project_id):
"""Projekt des aktuellen Users per (string) ID holen — oder None."""
try:
pid = int(project_id)
except (TypeError, ValueError):
return None
return Project.query.filter_by(id=pid, user_id=current_user.id).first()
@bp.route("/")
@login_required
def index():
flt = request.args.get("filter", "all")
if flt not in VALID_FILTERS:
flt = "all"
today = date.today()
week = today + timedelta(days=7)
projects = (
Project.query.filter_by(user_id=current_user.id)
.order_by(Project.created_at)
.all()
)
tasks = Task.query.filter_by(user_id=current_user.id).all()
projects_by_id = {p.id: p for p in projects}
# Statistik-Leiste
stats = {
"open": sum(1 for t in tasks if not t.done),
"done": sum(1 for t in tasks if t.done),
"overdue": sum(1 for t in tasks if not t.done and t.due_date and t.due_date < today),
"projects": len(projects),
}
# Ansicht "Nach Projekt"
project_views = []
for p in projects:
ptasks = sort_tasks([t for t in tasks if t.project_id == p.id])
project_views.append(
{
"project": p,
"tasks": ptasks,
"open": sum(1 for t in ptasks if not t.done),
"total": len(ptasks),
}
)
proj_open_total = sum(v["open"] for v in project_views)
# Ansicht "Alle Aufgaben" (gefiltert)
def keep(t):
if flt == "open":
return not t.done
if flt == "done":
return t.done
if flt == "overdue":
return not t.done and t.due_date and t.due_date < today
if flt == "soon":
return not t.done and t.due_date and today <= t.due_date <= week
return True
all_tasks = sort_tasks([t for t in tasks if keep(t)])
return render_template(
"projects/index.html",
projects=projects,
projects_by_id=projects_by_id,
project_views=project_views,
proj_open_total=proj_open_total,
all_tasks=all_tasks,
stats=stats,
active_filter=flt,
today_label=today_label(),
)
# ── Projekte ─────────────────────────────────────────────────────────────
@bp.route("/projects", methods=["POST"])
@login_required
def create_project():
name = (request.form.get("name") or "").strip()
if not name:
flash("Projektname fehlt.", "error")
return _redirect_back()
count = Project.query.filter_by(user_id=current_user.id).count()
project = Project(user_id=current_user.id, name=name[:120], color=next_color(count))
db.session.add(project)
db.session.commit()
return _redirect_back()
@bp.route("/projects/<int:project_id>/delete", methods=["POST"])
@login_required
def delete_project(project_id):
project = Project.query.filter_by(id=project_id, user_id=current_user.id).first_or_404()
# Aufgaben bleiben erhalten, verlieren nur die Projektzuordnung.
for t in project.tasks:
t.project_id = None
db.session.delete(project)
db.session.commit()
flash("Projekt gelöscht. Aufgaben bleiben erhalten.", "info")
return _redirect_back()
# ── Aufgaben ─────────────────────────────────────────────────────────────
@bp.route("/tasks", methods=["POST"])
@login_required
def create_task():
text = (request.form.get("text") or "").strip()
if not text:
flash("Aufgabentext fehlt.", "error")
return _redirect_back()
project = _owned_project_or_none(request.form.get("project_id"))
due_raw = (request.form.get("due_date") or "").strip()
due = None
if due_raw:
try:
due = datetime.strptime(due_raw, "%Y-%m-%d").date()
except ValueError:
due = None
task = Task(
user_id=current_user.id,
text=text[:500],
project_id=project.id if project else None,
due_date=due,
)
db.session.add(task)
db.session.commit()
return _redirect_back()
@bp.route("/tasks/<int:task_id>/toggle", methods=["POST"])
@login_required
def toggle_task(task_id):
task = Task.query.filter_by(id=task_id, user_id=current_user.id).first_or_404()
task.done = not task.done
db.session.commit()
return _redirect_back()
@bp.route("/tasks/<int:task_id>/delete", methods=["POST"])
@login_required
def delete_task(task_id):
task = Task.query.filter_by(id=task_id, user_id=current_user.id).first_or_404()
db.session.delete(task)
db.session.commit()
return _redirect_back()
# ── PDF-Export (druckbare To-Do-Liste) ──────────────────────────────────
@bp.route("/export.pdf")
@login_required
def export_pdf():
projects = (
Project.query.filter_by(user_id=current_user.id)
.order_by(Project.created_at)
.all()
)
tasks = Task.query.filter_by(user_id=current_user.id).all()
pdf_bytes = build_todo_pdf(projects, tasks)
filename = f"aufgaben_{date.today().isoformat()}.pdf"
return Response(
pdf_bytes,
mimetype="application/pdf",
headers={"Content-Disposition": f"inline; filename={filename}"},
)
# Fälligkeits-Badge als Template-Global verfügbar machen.
bp.add_app_template_global(due_badge, "due_badge")