- Python/Flask Backend - SQLAlchemy Models (notes, tasks, templates, users) - Gunicorn + Nginx Deploy-Konfiguration - Static Assets (CSS/JS) - Jinja2 Templates
286 lines
11 KiB
Python
Executable File
286 lines
11 KiB
Python
Executable File
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'}
|
|
)
|