Files
TODO_list_system/backend/models.py
beabigegg 00061adeb7 6th
2025-09-01 16:42:41 +08:00

257 lines
11 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from datetime import datetime
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.dialects.mysql import CHAR, ENUM, JSON, BIGINT
from sqlalchemy import text
import uuid
db = SQLAlchemy()
def generate_uuid():
return str(uuid.uuid4())
class TodoItem(db.Model):
__tablename__ = 'todo_item'
id = db.Column(CHAR(36), primary_key=True, default=generate_uuid)
title = db.Column(db.String(200), nullable=False)
description = db.Column(db.Text)
status = db.Column(ENUM('NEW', 'DOING', 'BLOCKED', 'DONE'), default='NEW')
priority = db.Column(ENUM('LOW', 'MEDIUM', 'HIGH', 'URGENT'), default='MEDIUM')
due_date = db.Column(db.Date)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
completed_at = db.Column(db.DateTime)
creator_ad = db.Column(db.String(128), nullable=False)
creator_display_name = db.Column(db.String(128))
creator_email = db.Column(db.String(256))
starred = db.Column(db.Boolean, default=False)
is_public = db.Column(db.Boolean, default=False)
tags = db.Column(JSON, default=list)
# Relationships
responsible_users = db.relationship('TodoItemResponsible', back_populates='todo', cascade='all, delete-orphan')
followers = db.relationship('TodoItemFollower', back_populates='todo', cascade='all, delete-orphan')
mail_logs = db.relationship('TodoMailLog', back_populates='todo', cascade='all, delete-orphan')
audit_logs = db.relationship('TodoAuditLog', back_populates='todo')
fire_email_logs = db.relationship('TodoFireEmailLog', back_populates='todo', cascade='all, delete-orphan')
def to_dict(self, include_user_details=True):
result = {
'id': self.id,
'title': self.title,
'description': self.description,
'status': self.status,
'priority': self.priority,
'due_date': self.due_date.isoformat() if self.due_date else None,
'created_at': self.created_at.isoformat(),
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
'creator_ad': self.creator_ad,
'creator_display_name': self.creator_display_name,
'creator_email': self.creator_email,
'starred': self.starred,
'is_public': self.is_public,
'tags': self.tags if self.tags else [],
'responsible_users': [r.ad_account for r in self.responsible_users],
'followers': [f.ad_account for f in self.followers]
}
# 如果需要包含用戶詳細信息,則添加 display names
if include_user_details:
from utils.ldap_utils import validate_ad_accounts
# 獲取所有相關用戶的 display names
all_users = set([self.creator_ad] + [r.ad_account for r in self.responsible_users] + [f.ad_account for f in self.followers])
user_details = validate_ad_accounts(list(all_users))
# 添加用戶詳細信息
result['responsible_users_details'] = []
for r in self.responsible_users:
user_info = user_details.get(r.ad_account, {})
result['responsible_users_details'].append({
'ad_account': r.ad_account,
'display_name': user_info.get('display_name', r.ad_account),
'email': user_info.get('email', '')
})
result['followers_details'] = []
for f in self.followers:
user_info = user_details.get(f.ad_account, {})
result['followers_details'].append({
'ad_account': f.ad_account,
'display_name': user_info.get('display_name', f.ad_account),
'email': user_info.get('email', '')
})
return result
def can_edit(self, user_ad):
"""Check if user can edit this todo"""
# Only creator can edit
return self.creator_ad == user_ad
def can_view(self, user_ad):
"""Check if user can view this todo"""
# Public todos can be viewed by anyone
if self.is_public:
return True
# Private todos can be viewed by creator and responsible users only
if self.creator_ad == user_ad:
return True
# Check if user is a responsible user
return any(r.ad_account == user_ad for r in self.responsible_users)
def can_follow(self, user_ad):
"""Check if user can follow this todo"""
# Anyone can follow public todos
if self.is_public:
return True
# For private todos, only creator/responsible can add followers
return False
class TodoItemResponsible(db.Model):
__tablename__ = 'todo_item_responsible'
todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='CASCADE'), primary_key=True)
ad_account = db.Column(db.String(128), primary_key=True)
added_by = db.Column(db.String(128))
added_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
# Relationships
todo = db.relationship('TodoItem', back_populates='responsible_users')
class TodoItemFollower(db.Model):
__tablename__ = 'todo_item_follower'
todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='CASCADE'), primary_key=True)
ad_account = db.Column(db.String(128), primary_key=True)
added_by = db.Column(db.String(128))
added_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
# Relationships
todo = db.relationship('TodoItem', back_populates='followers')
class TodoMailLog(db.Model):
__tablename__ = 'todo_mail_log'
id = db.Column(BIGINT, primary_key=True, autoincrement=True)
todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='CASCADE'))
type = db.Column(ENUM('SCHEDULED', 'FIRE'), nullable=False)
triggered_by_ad = db.Column(db.String(128))
recipients = db.Column(db.Text)
subject = db.Column(db.String(255))
status = db.Column(ENUM('QUEUED', 'SENT', 'FAILED'), default='QUEUED')
provider_msg_id = db.Column(db.String(128))
error_text = db.Column(db.Text)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
sent_at = db.Column(db.DateTime)
# Relationships
todo = db.relationship('TodoItem', back_populates='mail_logs')
class TodoAuditLog(db.Model):
__tablename__ = 'todo_audit_log'
id = db.Column(BIGINT, primary_key=True, autoincrement=True)
actor_ad = db.Column(db.String(128), nullable=False)
todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='SET NULL'))
action = db.Column(ENUM('CREATE', 'UPDATE', 'DELETE', 'COMPLETE', 'IMPORT',
'MAIL_SENT', 'MAIL_FAIL', 'FIRE_EMAIL', 'DIGEST_EMAIL', 'BULK_REMINDER',
'FOLLOW', 'UNFOLLOW'), nullable=False)
detail = db.Column(JSON)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
# Relationships
todo = db.relationship('TodoItem', back_populates='audit_logs')
class TodoUserPref(db.Model):
__tablename__ = 'todo_user_pref'
ad_account = db.Column(db.String(128), primary_key=True)
email = db.Column(db.String(256))
display_name = db.Column(db.String(128))
theme = db.Column(ENUM('light', 'dark', 'auto'), default='auto')
language = db.Column(db.String(10), default='zh-TW')
timezone = db.Column(db.String(50), default='Asia/Taipei')
notification_enabled = db.Column(db.Boolean, default=True)
email_reminder_enabled = db.Column(db.Boolean, default=True)
weekly_summary_enabled = db.Column(db.Boolean, default=True)
monthly_summary_enabled = db.Column(db.Boolean, default=False)
# 彈性的到期提醒天數設定 (JSON陣列如 [1, 3, 5] 表示前1天、前3天、前5天提醒)
reminder_days_before = db.Column(JSON, default=lambda: [1, 3])
# 摘要郵件時間設定 (時:分格式,如 "09:00")
daily_summary_time = db.Column(db.String(5), default='09:00')
weekly_summary_time = db.Column(db.String(5), default='09:00')
monthly_summary_time = db.Column(db.String(5), default='09:00')
# 摘要郵件週幾發送 (0=週日, 1=週一, ..., 6=週六)
weekly_summary_day = db.Column(db.Integer, default=1) # 預設週一
monthly_summary_day = db.Column(db.Integer, default=1) # 預設每月1日
# Fire email 配額控制
fire_email_today_count = db.Column(db.Integer, default=0)
fire_email_last_reset = db.Column(db.Date)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
def to_dict(self):
return {
'ad_account': self.ad_account,
'email': self.email,
'display_name': self.display_name,
'theme': self.theme,
'language': self.language,
'timezone': self.timezone,
'notification_enabled': self.notification_enabled,
'email_reminder_enabled': self.email_reminder_enabled,
'weekly_summary_enabled': self.weekly_summary_enabled,
'monthly_summary_enabled': self.monthly_summary_enabled,
'reminder_days_before': self.reminder_days_before or [1, 3],
'daily_summary_time': self.daily_summary_time,
'weekly_summary_time': self.weekly_summary_time,
'monthly_summary_time': self.monthly_summary_time,
'weekly_summary_day': self.weekly_summary_day,
'monthly_summary_day': self.monthly_summary_day,
}
class TodoImportJob(db.Model):
__tablename__ = 'todo_import_job'
id = db.Column(CHAR(36), primary_key=True, default=generate_uuid)
actor_ad = db.Column(db.String(128), nullable=False)
filename = db.Column(db.String(255))
total_rows = db.Column(db.Integer, default=0)
success_rows = db.Column(db.Integer, default=0)
failed_rows = db.Column(db.Integer, default=0)
status = db.Column(ENUM('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED'), default='PENDING')
error_file_path = db.Column(db.String(500))
error_details = db.Column(JSON)
created_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
completed_at = db.Column(db.DateTime)
def to_dict(self):
return {
'id': self.id,
'actor_ad': self.actor_ad,
'filename': self.filename,
'total_rows': self.total_rows,
'success_rows': self.success_rows,
'failed_rows': self.failed_rows,
'status': self.status,
'error_file_path': self.error_file_path,
'error_details': self.error_details,
'created_at': self.created_at.isoformat(),
'completed_at': self.completed_at.isoformat() if self.completed_at else None
}
class TodoFireEmailLog(db.Model):
__tablename__ = 'todo_fire_email_log'
id = db.Column(BIGINT, primary_key=True, autoincrement=True)
todo_id = db.Column(CHAR(36), db.ForeignKey('todo_item.id', ondelete='CASCADE'), nullable=False)
sender_ad = db.Column(db.String(128), nullable=False)
sent_at = db.Column(db.DateTime, nullable=False, default=datetime.utcnow)
# Relationships
todo = db.relationship('TodoItem', back_populates='fire_email_logs')