Files
putzliga/app/models.py

193 lines
7.8 KiB
Python

from __future__ import annotations
from datetime import UTC, date, datetime, timedelta
from flask_login import UserMixin
from werkzeug.security import check_password_hash, generate_password_hash
from .extensions import db, login_manager
def utcnow() -> datetime:
return datetime.now(UTC).replace(tzinfo=None)
class TimestampMixin:
created_at = db.Column(db.DateTime, nullable=False, default=utcnow)
updated_at = db.Column(db.DateTime, nullable=False, default=utcnow, onupdate=utcnow)
class User(UserMixin, TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(120), nullable=False)
email = db.Column(db.String(255), nullable=False, unique=True, index=True)
password_hash = db.Column(db.String(255), nullable=False)
is_admin = db.Column(db.Boolean, nullable=False, default=False)
avatar_path = db.Column(db.String(255), nullable=True)
notification_task_due_enabled = db.Column(db.Boolean, nullable=False, default=True)
notification_monthly_winner_enabled = db.Column(db.Boolean, nullable=False, default=True)
assigned_task_templates = db.relationship(
"TaskTemplate",
foreign_keys="TaskTemplate.default_assigned_user_id",
backref="default_assigned_user",
lazy=True,
)
assigned_tasks = db.relationship(
"TaskInstance",
foreign_keys="TaskInstance.assigned_user_id",
backref="assigned_user",
lazy=True,
)
completed_tasks = db.relationship(
"TaskInstance",
foreign_keys="TaskInstance.completed_by_user_id",
backref="completed_by_user",
lazy=True,
)
subscriptions = db.relationship("PushSubscription", backref="user", lazy=True, cascade="all, delete-orphan")
awarded_badges = db.relationship(
"UserBadge",
backref="user",
lazy=True,
cascade="all, delete-orphan",
order_by="desc(UserBadge.awarded_at)",
)
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)
@property
def display_avatar(self) -> str:
return self.avatar_path or "images/avatars/default.svg"
def __repr__(self) -> str:
return f"<User {self.email}>"
@login_manager.user_loader
def load_user(user_id: str) -> User | None:
return db.session.get(User, int(user_id))
class TaskTemplate(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(160), nullable=False)
description = db.Column(db.Text, nullable=True)
default_points = db.Column(db.Integer, nullable=False, default=10)
default_assigned_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True)
recurrence_interval_value = db.Column(db.Integer, nullable=True)
recurrence_interval_unit = db.Column(db.String(20), nullable=False, default="none")
active = db.Column(db.Boolean, nullable=False, default=True)
instances = db.relationship("TaskInstance", backref="task_template", lazy=True, cascade="all, delete-orphan")
@property
def recurrence_label(self) -> str:
if self.recurrence_interval_unit == "none" or not self.recurrence_interval_value:
return "Einmalig"
return f"Alle {self.recurrence_interval_value} {self.recurrence_interval_unit}"
class TaskInstance(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
task_template_id = db.Column(db.Integer, db.ForeignKey("task_template.id"), nullable=False, index=True)
title = db.Column(db.String(160), nullable=False)
description = db.Column(db.Text, nullable=True)
assigned_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True, index=True)
due_date = db.Column(db.Date, nullable=False, index=True)
status = db.Column(db.String(20), nullable=False, default="open", index=True)
completed_at = db.Column(db.DateTime, nullable=True, index=True)
completed_by_user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=True, index=True)
points_awarded = db.Column(db.Integer, nullable=False, default=10)
@property
def is_completed(self) -> bool:
return self.completed_at is not None
def compute_status(self, reference_date: date | None = None) -> str:
reference_date = reference_date or date.today()
if self.completed_at:
return "completed"
if self.due_date < reference_date:
return "overdue"
if self.due_date <= reference_date + timedelta(days=2):
return "soon"
return "open"
@property
def status_label(self) -> str:
labels = {
"open": "Offen",
"soon": "Bald fällig",
"overdue": "Überfällig",
"completed": "Erledigt",
}
return labels.get(self.status, "Offen")
class MonthlyScoreSnapshot(db.Model):
id = db.Column(db.Integer, primary_key=True)
year = db.Column(db.Integer, nullable=False, index=True)
month = db.Column(db.Integer, nullable=False, index=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True)
total_points = db.Column(db.Integer, nullable=False, default=0)
completed_tasks_count = db.Column(db.Integer, nullable=False, default=0)
rank = db.Column(db.Integer, nullable=False, default=1)
created_at = db.Column(db.DateTime, nullable=False, default=utcnow)
user = db.relationship("User", backref="monthly_snapshots")
__table_args__ = (db.UniqueConstraint("year", "month", "user_id", name="uq_snapshot_month_user"),)
class PushSubscription(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True)
endpoint = db.Column(db.Text, nullable=False, unique=True)
p256dh = db.Column(db.Text, nullable=False)
auth = db.Column(db.Text, nullable=False)
class NotificationLog(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True)
type = db.Column(db.String(80), nullable=False, index=True)
payload = db.Column(db.Text, nullable=False)
sent_at = db.Column(db.DateTime, nullable=False, default=utcnow, index=True)
user = db.relationship("User", backref="notification_logs")
class BadgeDefinition(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(80), nullable=False, unique=True, index=True)
name = db.Column(db.String(120), nullable=False)
description = db.Column(db.String(255), nullable=False)
icon_name = db.Column(db.String(80), nullable=False, default="sparkles")
trigger_type = db.Column(db.String(80), nullable=False)
threshold = db.Column(db.Integer, nullable=False, default=1)
bonus_points = db.Column(db.Integer, nullable=False, default=0)
active = db.Column(db.Boolean, nullable=False, default=True)
user_badges = db.relationship("UserBadge", backref="badge_definition", lazy=True, cascade="all, delete-orphan")
class UserBadge(db.Model):
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey("user.id"), nullable=False, index=True)
badge_definition_id = db.Column(db.Integer, db.ForeignKey("badge_definition.id"), nullable=False, index=True)
awarded_at = db.Column(db.DateTime, nullable=False, default=utcnow, index=True)
context = db.Column(db.Text, nullable=True)
__table_args__ = (db.UniqueConstraint("user_id", "badge_definition_id", name="uq_user_badge_once"),)
class AppSetting(TimestampMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
key = db.Column(db.String(120), nullable=False, unique=True, index=True)
value = db.Column(db.String(255), nullable=False)