release: publish saldo 0.1.0
This commit is contained in:
+174
@@ -0,0 +1,174 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import secrets
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Flask, g, redirect, request, session, url_for
|
||||
from flask_login import current_user
|
||||
|
||||
from .config import Config
|
||||
from .extensions import db, login_manager, migrate
|
||||
from .models import NotificationPreference, User
|
||||
from .seed import seed_data
|
||||
from .utils.users import sync_user_participants
|
||||
from .services.allocation_service import AllocationSuggestionService
|
||||
from .services.comparison_service import ComparisonService
|
||||
from .services.month_service import MonthService
|
||||
from .services.notification_service import NotificationService
|
||||
from .services.push_service import PushService
|
||||
from .services.share_service import ShareCalculationService
|
||||
from .utils.formatting import currency
|
||||
|
||||
|
||||
def create_app(config_class: type[Config] = Config) -> Flask:
|
||||
app = Flask(__name__, instance_relative_config=True)
|
||||
app.config.from_object(config_class)
|
||||
app.config.setdefault("AVATAR_UPLOAD_DIR", Path(app.config["DATA_DIR"]) / "avatars")
|
||||
app.config.setdefault("MAX_CONTENT_LENGTH", 5 * 1024 * 1024)
|
||||
|
||||
Path(app.config["DATA_DIR"]).mkdir(parents=True, exist_ok=True)
|
||||
Path(app.config["AVATAR_UPLOAD_DIR"]).mkdir(parents=True, exist_ok=True)
|
||||
|
||||
db.init_app(app)
|
||||
migrate.init_app(app, db)
|
||||
login_manager.init_app(app)
|
||||
|
||||
app.jinja_env.filters["currency"] = currency
|
||||
|
||||
allocation_service = AllocationSuggestionService(
|
||||
app.config["ALLOCATION_TARGET_RULES"],
|
||||
app.config["DEFAULT_PERSONAL_SPLIT_DESI_PCT"],
|
||||
)
|
||||
comparison_service = ComparisonService()
|
||||
share_service = ShareCalculationService()
|
||||
push_service = PushService(
|
||||
app.config["VAPID_PUBLIC_KEY"],
|
||||
app.config["VAPID_PRIVATE_KEY"],
|
||||
app.config["VAPID_CLAIMS"],
|
||||
)
|
||||
month_service = MonthService(allocation_service, comparison_service, share_service)
|
||||
notification_service = NotificationService(
|
||||
month_service, push_service, app.config["STRONG_INCOME_CHANGE_THRESHOLD"]
|
||||
)
|
||||
|
||||
app.extensions["saldo.month_service"] = month_service
|
||||
app.extensions["saldo.allocation_service"] = allocation_service
|
||||
app.extensions["saldo.notification_service"] = notification_service
|
||||
app.extensions["saldo.push_service"] = push_service
|
||||
app.extensions["saldo.share_service"] = share_service
|
||||
|
||||
from .admin.routes import admin_bp
|
||||
from .auth.routes import auth_bp
|
||||
from .main.routes import main_bp
|
||||
from .months.routes import months_bp
|
||||
from .planning.routes import planning_bp
|
||||
|
||||
app.register_blueprint(auth_bp)
|
||||
app.register_blueprint(main_bp)
|
||||
app.register_blueprint(planning_bp)
|
||||
app.register_blueprint(months_bp)
|
||||
app.register_blueprint(admin_bp)
|
||||
|
||||
register_cli(app)
|
||||
register_context(app)
|
||||
register_hooks(app)
|
||||
configure_logging(app)
|
||||
return app
|
||||
|
||||
|
||||
def register_context(app: Flask) -> None:
|
||||
@app.context_processor
|
||||
def inject_globals():
|
||||
if "csrf_token" not in session:
|
||||
session["csrf_token"] = secrets.token_urlsafe(32)
|
||||
return {
|
||||
"app_name": app.config["APP_NAME"],
|
||||
"vapid_public_key": app.config["VAPID_PUBLIC_KEY"],
|
||||
"csrf_token": session["csrf_token"],
|
||||
}
|
||||
|
||||
|
||||
def register_hooks(app: Flask) -> None:
|
||||
@app.before_request
|
||||
def ensure_current_month():
|
||||
allowed_endpoints = {"auth.setup", "static", "main.health"}
|
||||
if User.query.count() == 0 and request.endpoint not in allowed_endpoints:
|
||||
return redirect(url_for("auth.setup"))
|
||||
if request.method == "POST" and app.config.get("CSRF_ENABLED") and not app.config.get("TESTING"):
|
||||
sent_token = request.headers.get("X-CSRF-Token") or request.form.get("csrf_token")
|
||||
if request.endpoint != "planning.subscribe_push" and sent_token != session.get("csrf_token"):
|
||||
return ("CSRF validation failed", 400)
|
||||
if current_user.is_authenticated:
|
||||
if sync_user_participants():
|
||||
db.session.commit()
|
||||
g.current_month = app.extensions["saldo.month_service"].ensure_month()
|
||||
|
||||
|
||||
def register_cli(app: Flask) -> None:
|
||||
import click
|
||||
|
||||
from .extensions import db
|
||||
from .models import NotificationPreference, User
|
||||
@app.cli.command("create-admin")
|
||||
@click.option("--username", required=True)
|
||||
@click.option("--email", required=True)
|
||||
@click.option("--password", required=True)
|
||||
@click.option("--display-name", default=None)
|
||||
def create_admin(username: str, email: str, password: str, display_name: str | None):
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user is None:
|
||||
user = User(
|
||||
username=username,
|
||||
display_name=display_name or username,
|
||||
email=email,
|
||||
role="admin",
|
||||
)
|
||||
db.session.add(user)
|
||||
elif not user.display_name:
|
||||
user.display_name = display_name or username
|
||||
user.set_password(password)
|
||||
user.is_active = True
|
||||
pref = user.notification_preference or NotificationPreference(user=user)
|
||||
db.session.add(pref)
|
||||
db.session.commit()
|
||||
click.echo(f"Admin {username} bereit.")
|
||||
|
||||
@app.cli.command("bootstrap-admin")
|
||||
def bootstrap_admin():
|
||||
username = app.config["ADMIN_BOOTSTRAP_USERNAME"]
|
||||
password = app.config["ADMIN_BOOTSTRAP_PASSWORD"]
|
||||
user = User.query.filter_by(username=username).first()
|
||||
if user:
|
||||
click.echo("Bootstrap-Admin existiert bereits.")
|
||||
return
|
||||
if not password:
|
||||
click.echo("Kein Bootstrap-Passwort gesetzt. Bitte Setup-Seite oder create-admin nutzen.")
|
||||
return
|
||||
user = User(
|
||||
username=username,
|
||||
display_name=username,
|
||||
email=app.config["ADMIN_BOOTSTRAP_EMAIL"],
|
||||
role="admin",
|
||||
)
|
||||
user.set_password(password)
|
||||
db.session.add(user)
|
||||
db.session.flush()
|
||||
db.session.add(NotificationPreference(user_id=user.id))
|
||||
db.session.commit()
|
||||
click.echo(f"Bootstrap-Admin {username} angelegt.")
|
||||
|
||||
@app.cli.command("seed")
|
||||
def seed():
|
||||
seed_data()
|
||||
click.echo("Basisdaten angelegt.")
|
||||
|
||||
@app.cli.command("run-reminders")
|
||||
def run_reminders():
|
||||
count = app.extensions["saldo.notification_service"].run_monthly_checks()
|
||||
click.echo(f"{count} Erinnerungen geprüft.")
|
||||
|
||||
|
||||
def configure_logging(app: Flask) -> None:
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
app.logger.setLevel(logging.INFO)
|
||||
Reference in New Issue
Block a user