257 lines
11 KiB
Python
257 lines
11 KiB
Python
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') |