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"" @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)