168 lines
6.7 KiB
Python
168 lines
6.7 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)
|
|
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")
|
|
|
|
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)
|
|
|