Initial commit: Notes Manager App (notes.braetter-int.de)

- Python/Flask Backend
- SQLAlchemy Models (notes, tasks, templates, users)
- Gunicorn + Nginx Deploy-Konfiguration
- Static Assets (CSS/JS)
- Jinja2 Templates
This commit is contained in:
2026-04-15 09:28:33 +02:00
commit 5c7ce5d0ca
24 changed files with 1666 additions and 0 deletions

285
app/routes.py Executable file
View File

@@ -0,0 +1,285 @@
from datetime import datetime, date
from flask import Blueprint, render_template, request, redirect, url_for, flash, Response
from flask_login import login_user, logout_user, login_required, current_user
from werkzeug.security import check_password_hash, generate_password_hash
from .models import db, User, DocumentationEntry, DocumentationTemplate, TaskItem
main_bp = Blueprint('main', __name__)
def parse_date(value):
if not value:
return date.today()
return datetime.strptime(value, '%Y-%m-%d').date()
@main_bp.route('/')
def index():
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
return redirect(url_for('main.login'))
@main_bp.route('/login', methods=['GET', 'POST'])
def login():
if current_user.is_authenticated:
return redirect(url_for('main.dashboard'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
password = request.form.get('password', '')
user = User.query.filter_by(username=username).first()
if user and check_password_hash(user.password_hash, password):
login_user(user)
flash('Anmeldung erfolgreich.', 'success')
return redirect(url_for('main.dashboard'))
flash('Ungültige Zugangsdaten.', 'danger')
return render_template('login.html')
@main_bp.route('/logout')
@login_required
def logout():
logout_user()
flash('Du wurdest abgemeldet.', 'info')
return redirect(url_for('main.login'))
@main_bp.route('/dashboard')
@login_required
def dashboard():
today = date.today()
entries_today = DocumentationEntry.query.filter_by(work_date=today).order_by(DocumentationEntry.created_at.desc()).all()
open_tasks = TaskItem.query.filter(TaskItem.status != 'erledigt').order_by(TaskItem.priority.desc(), TaskItem.due_date.asc()).all()
latest_entries = DocumentationEntry.query.order_by(DocumentationEntry.updated_at.desc()).limit(8).all()
stats = {
'entries_total': DocumentationEntry.query.count(),
'entries_today': len(entries_today),
'templates_total': DocumentationTemplate.query.count(),
'open_tasks': len(open_tasks),
}
return render_template('dashboard.html', today=today, entries_today=entries_today, open_tasks=open_tasks, latest_entries=latest_entries, stats=stats)
@main_bp.route('/entries')
@login_required
def entries():
q = request.args.get('q', '').strip()
status = request.args.get('status', '').strip()
category = request.args.get('category', '').strip()
query = DocumentationEntry.query
if q:
like = f'%{q}%'
query = query.filter(
db.or_(
DocumentationEntry.title.ilike(like),
DocumentationEntry.system_name.ilike(like),
DocumentationEntry.content.ilike(like),
DocumentationEntry.tags.ilike(like),
)
)
if status:
query = query.filter_by(status=status)
if category:
query = query.filter_by(category=category)
items = query.order_by(DocumentationEntry.work_date.desc(), DocumentationEntry.updated_at.desc()).all()
categories = [c[0] for c in db.session.query(DocumentationEntry.category).distinct().order_by(DocumentationEntry.category).all()]
return render_template('entries.html', items=items, q=q, status=status, category=category, categories=categories)
@main_bp.route('/entries/new', methods=['GET', 'POST'])
@login_required
def entry_new():
templates = DocumentationTemplate.query.order_by(DocumentationTemplate.name).all()
if request.method == 'POST':
entry = DocumentationEntry(
title=request.form.get('title', '').strip(),
work_date=parse_date(request.form.get('work_date')),
category=request.form.get('category', '').strip() or 'Allgemein',
system_name=request.form.get('system_name', '').strip(),
priority=request.form.get('priority', 'mittel').strip(),
status=request.form.get('status', 'offen').strip(),
content=request.form.get('content', '').strip(),
tags=request.form.get('tags', '').strip(),
created_by=current_user.display_name,
)
if not entry.title or not entry.content:
flash('Titel und Inhalt sind Pflichtfelder.', 'danger')
return render_template('entry_form.html', templates=templates, entry=None)
db.session.add(entry)
db.session.commit()
flash('Dokumentationseintrag wurde erstellt.', 'success')
return redirect(url_for('main.entries'))
return render_template('entry_form.html', templates=templates, entry=None)
@main_bp.route('/entries/<int:entry_id>/edit', methods=['GET', 'POST'])
@login_required
def entry_edit(entry_id):
entry = DocumentationEntry.query.get_or_404(entry_id)
templates = DocumentationTemplate.query.order_by(DocumentationTemplate.name).all()
if request.method == 'POST':
entry.title = request.form.get('title', '').strip()
entry.work_date = parse_date(request.form.get('work_date'))
entry.category = request.form.get('category', '').strip() or 'Allgemein'
entry.system_name = request.form.get('system_name', '').strip()
entry.priority = request.form.get('priority', 'mittel').strip()
entry.status = request.form.get('status', 'offen').strip()
entry.content = request.form.get('content', '').strip()
entry.tags = request.form.get('tags', '').strip()
if not entry.title or not entry.content:
flash('Titel und Inhalt sind Pflichtfelder.', 'danger')
return render_template('entry_form.html', templates=templates, entry=entry)
db.session.commit()
flash('Eintrag aktualisiert.', 'success')
return redirect(url_for('main.entries'))
return render_template('entry_form.html', templates=templates, entry=entry)
@main_bp.route('/entries/<int:entry_id>/delete', methods=['POST'])
@login_required
def entry_delete(entry_id):
entry = DocumentationEntry.query.get_or_404(entry_id)
db.session.delete(entry)
db.session.commit()
flash('Eintrag gelöscht.', 'info')
return redirect(url_for('main.entries'))
@main_bp.route('/entries/<int:entry_id>')
@login_required
def entry_view(entry_id):
entry = DocumentationEntry.query.get_or_404(entry_id)
return render_template('entry_view.html', entry=entry)
@main_bp.route('/templates', methods=['GET', 'POST'])
@login_required
def templates():
if request.method == 'POST':
name = request.form.get('name', '').strip()
content = request.form.get('content', '').strip()
description = request.form.get('description', '').strip()
if name and content:
db.session.add(DocumentationTemplate(name=name, description=description, content=content))
db.session.commit()
flash('Vorlage gespeichert.', 'success')
return redirect(url_for('main.templates'))
flash('Name und Inhalt sind Pflichtfelder.', 'danger')
items = DocumentationTemplate.query.order_by(DocumentationTemplate.name).all()
return render_template('templates.html', items=items)
@main_bp.route('/templates/<int:template_id>/delete', methods=['POST'])
@login_required
def template_delete(template_id):
item = DocumentationTemplate.query.get_or_404(template_id)
db.session.delete(item)
db.session.commit()
flash('Vorlage gelöscht.', 'info')
return redirect(url_for('main.templates'))
@main_bp.route('/tasks', methods=['GET', 'POST'])
@login_required
def tasks():
if request.method == 'POST':
task = TaskItem(
title=request.form.get('title', '').strip(),
description=request.form.get('description', '').strip(),
due_date=parse_date(request.form.get('due_date')) if request.form.get('due_date') else None,
status=request.form.get('status', 'offen').strip(),
priority=request.form.get('priority', 'mittel').strip(),
)
if not task.title:
flash('Titel ist ein Pflichtfeld.', 'danger')
else:
db.session.add(task)
db.session.commit()
flash('Aufgabe gespeichert.', 'success')
return redirect(url_for('main.tasks'))
items = TaskItem.query.order_by(TaskItem.status.asc(), TaskItem.priority.desc(), TaskItem.due_date.asc()).all()
return render_template('tasks.html', items=items)
@main_bp.route('/tasks/<int:task_id>/toggle', methods=['POST'])
@login_required
def task_toggle(task_id):
task = TaskItem.query.get_or_404(task_id)
task.status = 'erledigt' if task.status != 'erledigt' else 'offen'
db.session.commit()
flash('Aufgabenstatus geändert.', 'success')
return redirect(url_for('main.tasks'))
@main_bp.route('/tasks/<int:task_id>/delete', methods=['POST'])
@login_required
def task_delete(task_id):
task = TaskItem.query.get_or_404(task_id)
db.session.delete(task)
db.session.commit()
flash('Aufgabe gelöscht.', 'info')
return redirect(url_for('main.tasks'))
@main_bp.route('/users', methods=['GET', 'POST'])
@login_required
def users():
if current_user.role != 'admin':
flash('Nur Administratoren dürfen Benutzer verwalten.', 'danger')
return redirect(url_for('main.dashboard'))
if request.method == 'POST':
username = request.form.get('username', '').strip()
display_name = request.form.get('display_name', '').strip()
password = request.form.get('password', '').strip()
role = request.form.get('role', 'user').strip()
if not username or not display_name or not password:
flash('Bitte alle Pflichtfelder füllen.', 'danger')
elif User.query.filter_by(username=username).first():
flash('Benutzername existiert bereits.', 'warning')
else:
db.session.add(User(
username=username,
display_name=display_name,
role=role,
password_hash=generate_password_hash(password)
))
db.session.commit()
flash('Benutzer angelegt.', 'success')
return redirect(url_for('main.users'))
items = User.query.order_by(User.username).all()
return render_template('users.html', items=items)
@main_bp.route('/export/markdown')
@login_required
def export_markdown():
items = DocumentationEntry.query.order_by(DocumentationEntry.work_date.desc(), DocumentationEntry.id.desc()).all()
lines = ['# Export Dokumentation', '']
for item in items:
lines.extend([
f'## {item.title}',
f'- Datum: {item.work_date.isoformat()}',
f'- Kategorie: {item.category}',
f'- System: {item.system_name or "-"}',
f'- Priorität: {item.priority}',
f'- Status: {item.status}',
f'- Tags: {item.tags or "-"}',
f'- Erstellt von: {item.created_by}',
'',
item.content,
'',
'---',
''
])
content = '\n'.join(lines)
return Response(
content,
mimetype='text/markdown',
headers={'Content-Disposition': 'attachment; filename=dokumentation_export.md'}
)