This commit is contained in:
beabigegg
2025-08-29 16:25:46 +08:00
commit b0c86302ff
65 changed files with 19786 additions and 0 deletions

240
backend/models.py Normal file
View File

@@ -0,0 +1,240 @@
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)
# 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,
'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"""
if self.creator_ad == user_ad:
return True
return any(r.ad_account == user_ad for r in self.responsible_users)
def can_view(self, user_ad):
"""Check if user can view this todo"""
if self.can_edit(user_ad):
return True
return any(f.ad_account == user_ad for f in self.followers)
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'), 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')