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

View File

@@ -0,0 +1,319 @@
"""
Email Service
處理所有郵件相關功能,包括通知、提醒和摘要郵件
"""
import os
import smtplib
from email.mime.text import MIMEText
from email.mime.multipart import MIMEMultipart
from email.mime.application import MIMEApplication
from datetime import datetime, date
from flask import current_app
from jinja2 import Environment, FileSystemLoader, select_autoescape
from utils.logger import get_logger
from utils.ldap_utils import get_user_info
logger = get_logger(__name__)
class EmailService:
"""郵件服務類別"""
def __init__(self):
self.smtp_server = os.getenv('SMTP_SERVER')
self.smtp_port = int(os.getenv('SMTP_PORT', 587))
self.use_tls = os.getenv('SMTP_USE_TLS', 'false').lower() == 'true'
self.use_ssl = os.getenv('SMTP_USE_SSL', 'false').lower() == 'true'
self.auth_required = os.getenv('SMTP_AUTH_REQUIRED', 'false').lower() == 'true'
self.sender_email = os.getenv('SMTP_SENDER_EMAIL')
self.sender_password = os.getenv('SMTP_SENDER_PASSWORD', '')
# 設定 Jinja2 模板環境
template_dir = os.path.join(os.path.dirname(__file__), '..', 'templates', 'emails')
self.jinja_env = Environment(
loader=FileSystemLoader(template_dir),
autoescape=select_autoescape(['html', 'xml'])
)
def _create_smtp_connection(self):
"""建立 SMTP 連線"""
try:
if self.use_ssl:
server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port)
else:
server = smtplib.SMTP(self.smtp_server, self.smtp_port)
if self.use_tls:
server.starttls()
if self.auth_required and self.sender_password:
server.login(self.sender_email, self.sender_password)
return server
except Exception as e:
logger.error(f"SMTP connection failed: {str(e)}")
return None
def _send_email(self, to_email, subject, html_content, text_content=None):
"""發送郵件的基礎方法"""
try:
if not self.smtp_server or not self.sender_email:
logger.error("SMTP configuration incomplete")
return False
# 建立郵件
msg = MIMEMultipart('alternative')
msg['From'] = self.sender_email
msg['To'] = to_email
msg['Subject'] = subject
# 添加文本內容
if text_content:
text_part = MIMEText(text_content, 'plain', 'utf-8')
msg.attach(text_part)
# 添加 HTML 內容
html_part = MIMEText(html_content, 'html', 'utf-8')
msg.attach(html_part)
# 發送郵件
server = self._create_smtp_connection()
if not server:
return False
server.send_message(msg)
server.quit()
logger.info(f"Email sent successfully to {to_email}")
return True
except Exception as e:
logger.error(f"Failed to send email to {to_email}: {str(e)}")
return False
def _get_user_email(self, ad_account):
"""取得使用者郵件地址"""
user_info = get_user_info(ad_account)
if user_info and user_info.get('email'):
return user_info['email']
# 如果無法從 LDAP 取得,嘗試組合郵件地址
domain = os.getenv('LDAP_DOMAIN', 'panjit.com.tw')
return f"{ad_account}@{domain}"
def send_fire_email(self, todo, recipient, sender, custom_message=''):
"""發送緊急通知郵件"""
try:
recipient_email = self._get_user_email(recipient)
sender_info = get_user_info(sender)
sender_name = sender_info.get('displayName', sender) if sender_info else sender
# 準備模板資料
template_data = {
'todo': todo,
'recipient': recipient,
'sender': sender,
'sender_name': sender_name,
'custom_message': custom_message,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'app_name': current_app.config.get('APP_NAME', 'PANJIT Todo List')
}
# 渲染模板
template = self.jinja_env.get_template('fire_email.html')
html_content = template.render(**template_data)
# 主題
subject = f"🚨 緊急通知 - {todo.title}"
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Fire email failed for {recipient}: {str(e)}")
return False
def send_reminder_email(self, todo, recipient, reminder_type):
"""發送提醒郵件"""
try:
recipient_email = self._get_user_email(recipient)
# 根據提醒類型設定主題和模板
if reminder_type == 'due_tomorrow':
subject = f"📅 明日到期提醒 - {todo.title}"
template_name = 'reminder_due_tomorrow.html'
elif reminder_type == 'overdue':
subject = f"⚠️ 逾期提醒 - {todo.title}"
template_name = 'reminder_overdue.html'
else:
subject = f"📋 待辦提醒 - {todo.title}"
template_name = 'reminder_general.html'
# 準備模板資料
template_data = {
'todo': todo,
'recipient': recipient,
'reminder_type': reminder_type,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'app_name': current_app.config.get('APP_NAME', 'PANJIT Todo List')
}
# 渲染模板
template = self.jinja_env.get_template(template_name)
html_content = template.render(**template_data)
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Reminder email failed for {recipient}: {str(e)}")
return False
def send_digest_email(self, recipient, digest_data):
"""發送摘要郵件"""
try:
recipient_email = self._get_user_email(recipient)
# 根據摘要類型設定主題
digest_type = digest_data.get('type', 'weekly')
type_names = {
'daily': '每日',
'weekly': '每週',
'monthly': '每月'
}
subject = f"📊 {type_names.get(digest_type, '定期')}摘要報告"
# 準備模板資料
template_data = {
'recipient': recipient,
'digest_data': digest_data,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'app_name': current_app.config.get('APP_NAME', 'PANJIT Todo List')
}
# 渲染模板
template = self.jinja_env.get_template('digest.html')
html_content = template.render(**template_data)
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Digest email failed for {recipient}: {str(e)}")
return False
def send_todo_notification(self, todo, recipients, action, actor):
"""發送待辦事項變更通知"""
try:
success_count = 0
for recipient in recipients:
try:
recipient_email = self._get_user_email(recipient)
actor_info = get_user_info(actor)
actor_name = actor_info.get('displayName', actor) if actor_info else actor
# 根據動作類型設定主題和模板
action_names = {
'CREATE': '建立',
'UPDATE': '更新',
'DELETE': '刪除',
'ASSIGN': '指派',
'COMPLETE': '完成'
}
action_name = action_names.get(action, action)
subject = f"📋 待辦事項{action_name} - {todo.title}"
# 準備模板資料
template_data = {
'todo': todo,
'recipient': recipient,
'action': action,
'action_name': action_name,
'actor': actor,
'actor_name': actor_name,
'timestamp': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'app_name': current_app.config.get('APP_NAME', 'PANJIT Todo List')
}
# 渲染模板
template = self.jinja_env.get_template('todo_notification.html')
html_content = template.render(**template_data)
if self._send_email(recipient_email, subject, html_content):
success_count += 1
except Exception as e:
logger.error(f"Todo notification failed for {recipient}: {str(e)}")
return success_count
except Exception as e:
logger.error(f"Todo notification batch failed: {str(e)}")
return 0
def send_test_email(self, recipient):
"""發送測試郵件"""
try:
recipient_email = self._get_user_email(recipient)
subject = "✅ 郵件服務測試"
html_content = f"""
<html>
<body>
<h2>郵件服務測試</h2>
<p>您好 {recipient}</p>
<p>這是一封測試郵件,用於驗證 PANJIT Todo List 系統的郵件功能是否正常運作。</p>
<p>如果您收到這封郵件,表示郵件服務配置正確。</p>
<br>
<p>測試時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}</p>
<p>此郵件由系統自動發送,請勿回覆。</p>
</body>
</html>
"""
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Test email failed for {recipient}: {str(e)}")
return False
def send_test_email_direct(self, recipient_email):
"""直接發送測試郵件到指定郵件地址"""
try:
subject = "✅ PANJIT Todo List 郵件服務測試"
html_content = f"""
<html>
<body style="font-family: Arial, sans-serif; line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;">
<h2 style="color: #2563eb; border-bottom: 2px solid #e5e7eb; padding-bottom: 10px;">📧 郵件服務測試</h2>
<p>您好!</p>
<p>這是一封來自 <strong>PANJIT Todo List 系統</strong> 的測試郵件,用於驗證郵件服務功能是否正常運作。</p>
<div style="background-color: #f0f9ff; border-left: 4px solid #2563eb; padding: 15px; margin: 20px 0;">
<p style="margin: 0;"><strong>✅ 如果您收到這封郵件,表示:</strong></p>
<ul style="margin: 10px 0; padding-left: 20px;">
<li>SMTP 服務器連線正常</li>
<li>郵件發送功能運作良好</li>
<li>您的郵件地址設定正確</li>
</ul>
</div>
<hr style="border: none; border-top: 1px solid #e5e7eb; margin: 30px 0;">
<p style="font-size: 14px; color: #6b7280;">
<strong>測試詳細資訊:</strong><br>
📅 測試時間: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}<br>
📧 收件人: {recipient_email}<br>
🏢 發件人: PANJIT Todo List 系統
</p>
<p style="font-size: 12px; color: #9ca3af; margin-top: 30px;">
此郵件由系統自動發送,請勿回覆。如有任何問題,請聯繫系統管理員。
</p>
</div>
</body>
</html>
"""
return self._send_email(recipient_email, subject, html_content)
except Exception as e:
logger.error(f"Direct test email failed for {recipient_email}: {str(e)}")
return False

230
backend/utils/ldap_utils.py Normal file
View File

@@ -0,0 +1,230 @@
import time
from ldap3 import Server, Connection, SUBTREE, ALL_ATTRIBUTES
from flask import current_app
from utils.logger import get_logger
logger = get_logger(__name__)
def create_ldap_connection(retries=3):
"""Create LDAP connection with retry mechanism"""
config = current_app.config
for attempt in range(retries):
try:
server = Server(
config['LDAP_SERVER'],
port=config['LDAP_PORT'],
use_ssl=config['LDAP_USE_SSL'],
get_info=ALL_ATTRIBUTES
)
conn = Connection(
server,
user=config['LDAP_BIND_USER_DN'],
password=config['LDAP_BIND_USER_PASSWORD'],
auto_bind=True,
raise_exceptions=True
)
logger.info("LDAP connection established successfully")
return conn
except Exception as e:
logger.error(f"LDAP connection attempt {attempt + 1} failed: {str(e)}")
if attempt == retries - 1:
raise
time.sleep(1)
return None
def authenticate_user(username, password):
"""Authenticate user against LDAP/AD"""
try:
conn = create_ldap_connection()
if not conn:
return None
config = current_app.config
search_filter = f"(&(objectClass=person)(objectCategory=person)({config['LDAP_USER_LOGIN_ATTR']}={username}))"
# Search for user
conn.search(
config['LDAP_SEARCH_BASE'],
search_filter,
SUBTREE,
attributes=['displayName', 'mail', 'sAMAccountName', 'userPrincipalName']
)
if not conn.entries:
logger.warning(f"User not found: {username}")
return None
user_entry = conn.entries[0]
user_dn = user_entry.entry_dn
# Try to bind with user credentials
try:
user_conn = Connection(
conn.server,
user=user_dn,
password=password,
auto_bind=True,
raise_exceptions=True
)
user_conn.unbind()
# Return user info
user_info = {
'ad_account': str(user_entry.sAMAccountName) if user_entry.sAMAccountName else username,
'display_name': str(user_entry.displayName) if user_entry.displayName else username,
'email': str(user_entry.mail) if user_entry.mail else '',
'user_principal_name': str(user_entry.userPrincipalName) if user_entry.userPrincipalName else username
}
logger.info(f"User authenticated successfully: {username}")
return user_info
except Exception as e:
logger.warning(f"Authentication failed for user {username}: {str(e)}")
return None
except Exception as e:
logger.error(f"LDAP authentication error: {str(e)}")
return None
finally:
if conn:
conn.unbind()
def search_ldap_principals(search_term, limit=20):
"""Search for LDAP users and groups"""
try:
conn = create_ldap_connection()
if not conn:
return []
config = current_app.config
# Build search filter for active users
search_filter = f"""(&
(objectClass=person)
(objectCategory=person)
(!(userAccountControl:1.2.840.113556.1.4.803:=2))
(|
(displayName=*{search_term}*)
(mail=*{search_term}*)
(sAMAccountName=*{search_term}*)
(userPrincipalName=*{search_term}*)
)
)"""
# Remove extra whitespace
search_filter = ' '.join(search_filter.split())
conn.search(
config['LDAP_SEARCH_BASE'],
search_filter,
SUBTREE,
attributes=['sAMAccountName', 'displayName', 'mail'],
size_limit=limit
)
results = []
for entry in conn.entries:
results.append({
'ad_account': str(entry.sAMAccountName) if entry.sAMAccountName else '',
'display_name': str(entry.displayName) if entry.displayName else '',
'email': str(entry.mail) if entry.mail else ''
})
logger.info(f"LDAP search found {len(results)} results for term: {search_term}")
return results
except Exception as e:
logger.error(f"LDAP search error: {str(e)}")
return []
finally:
if conn:
conn.unbind()
def get_user_info(ad_account):
"""Get user information from LDAP"""
try:
conn = create_ldap_connection()
if not conn:
return None
config = current_app.config
search_filter = f"(&(objectClass=person)(sAMAccountName={ad_account}))"
conn.search(
config['LDAP_SEARCH_BASE'],
search_filter,
SUBTREE,
attributes=['displayName', 'mail', 'sAMAccountName', 'userPrincipalName']
)
if not conn.entries:
return None
entry = conn.entries[0]
return {
'ad_account': str(entry.sAMAccountName) if entry.sAMAccountName else ad_account,
'display_name': str(entry.displayName) if entry.displayName else ad_account,
'email': str(entry.mail) if entry.mail else ''
}
except Exception as e:
logger.error(f"Error getting user info for {ad_account}: {str(e)}")
return None
finally:
if conn:
conn.unbind()
def validate_ad_accounts(ad_accounts):
"""Validate multiple AD accounts exist"""
try:
conn = create_ldap_connection()
if not conn:
return {}
config = current_app.config
valid_accounts = {}
for account in ad_accounts:
search_filter = f"(&(objectClass=person)(sAMAccountName={account}))"
conn.search(
config['LDAP_SEARCH_BASE'],
search_filter,
SUBTREE,
attributes=['sAMAccountName', 'displayName', 'mail']
)
if conn.entries:
entry = conn.entries[0]
valid_accounts[account] = {
'ad_account': str(entry.sAMAccountName) if entry.sAMAccountName else account,
'display_name': str(entry.displayName) if entry.displayName else account,
'email': str(entry.mail) if entry.mail else ''
}
return valid_accounts
except Exception as e:
logger.error(f"Error validating AD accounts: {str(e)}")
return {}
finally:
if conn:
conn.unbind()
def test_ldap_connection():
"""Test LDAP connection for health check"""
try:
conn = create_ldap_connection(retries=1)
if conn:
conn.unbind()
return True
return False
except Exception as e:
logger.error(f"LDAP connection test failed: {str(e)}")
return False

58
backend/utils/logger.py Normal file
View File

@@ -0,0 +1,58 @@
import os
import logging
from logging.handlers import RotatingFileHandler
from colorlog import ColoredFormatter
def setup_logger(app):
"""Setup application logging"""
# Create logs directory if it doesn't exist
log_dir = 'logs'
if not os.path.exists(log_dir):
os.makedirs(log_dir)
log_file = app.config.get('LOG_FILE', 'logs/app.log')
log_level = app.config.get('LOG_LEVEL', 'INFO')
# Set up file handler
file_handler = RotatingFileHandler(
log_file,
maxBytes=10485760, # 10MB
backupCount=10
)
file_handler.setLevel(getattr(logging, log_level))
# File formatter
file_formatter = logging.Formatter(
'%(asctime)s - %(name)s - %(levelname)s - %(message)s'
)
file_handler.setFormatter(file_formatter)
# Console handler with colors
console_handler = logging.StreamHandler()
console_handler.setLevel(getattr(logging, log_level))
# Console formatter with colors
console_formatter = ColoredFormatter(
'%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s%(reset)s',
log_colors={
'DEBUG': 'cyan',
'INFO': 'green',
'WARNING': 'yellow',
'ERROR': 'red',
'CRITICAL': 'red,bg_white',
}
)
console_handler.setFormatter(console_formatter)
# Add handlers to app logger
app.logger.addHandler(file_handler)
app.logger.addHandler(console_handler)
app.logger.setLevel(getattr(logging, log_level))
# Log startup
app.logger.info(f"Application started in {app.config.get('ENV', 'development')} mode")
def get_logger(name):
"""Get a logger instance"""
return logging.getLogger(name)

140
backend/utils/mock_ldap.py Normal file
View File

@@ -0,0 +1,140 @@
"""
Mock LDAP for development/testing purposes
當無法連接到實際LDAP時使用
"""
from utils.logger import get_logger
logger = get_logger(__name__)
def authenticate_user(username, password):
"""Mock authentication for development"""
logger.info(f"Mock LDAP: Authenticating user {username}")
# 簡單的開發用驗證
if not username or not password:
return None
# 模擬用戶資料
mock_users = {
'admin': {
'ad_account': 'admin',
'display_name': '系統管理員',
'email': 'admin@panjit.com.tw'
},
'test': {
'ad_account': 'test',
'display_name': '測試使用者',
'email': 'test@panjit.com.tw'
},
'user1': {
'ad_account': 'user1',
'display_name': '使用者一',
'email': 'user1@panjit.com.tw'
},
'ymirliu@panjit.com.tw': {
'ad_account': '92367',
'display_name': 'ymirliu 陸一銘',
'email': 'ymirliu@panjit.com.tw'
}
}
if username.lower() in mock_users:
logger.info(f"Mock LDAP: User {username} authenticated successfully")
return mock_users[username.lower()]
logger.warning(f"Mock LDAP: User {username} not found")
return None
def search_ldap_principals(search_term, limit=20):
"""Mock LDAP search"""
logger.info(f"Mock LDAP: Searching for '{search_term}'")
mock_results = [
{
'ad_account': 'admin',
'display_name': '系統管理員',
'email': 'admin@panjit.com.tw'
},
{
'ad_account': 'test',
'display_name': '測試使用者',
'email': 'test@panjit.com.tw'
},
{
'ad_account': 'user1',
'display_name': '使用者一',
'email': 'user1@panjit.com.tw'
},
{
'ad_account': 'user2',
'display_name': '使用者二',
'email': 'user2@panjit.com.tw'
}
]
# 簡單的搜尋過濾
if search_term:
results = []
for user in mock_results:
if (search_term.lower() in user['ad_account'].lower() or
search_term.lower() in user['display_name'].lower() or
search_term.lower() in user['email'].lower()):
results.append(user)
return results[:limit]
return mock_results[:limit]
def get_user_info(ad_account):
"""Mock get user info"""
mock_users = {
'admin': {
'ad_account': 'admin',
'display_name': '系統管理員',
'email': 'admin@panjit.com.tw'
},
'test': {
'ad_account': 'test',
'display_name': '測試使用者',
'email': 'test@panjit.com.tw'
},
'user1': {
'ad_account': 'user1',
'display_name': '使用者一',
'email': 'user1@panjit.com.tw'
}
}
return mock_users.get(ad_account.lower())
def validate_ad_accounts(ad_accounts):
"""Mock validate AD accounts"""
mock_users = {
'admin': {
'ad_account': 'admin',
'display_name': '系統管理員',
'email': 'admin@panjit.com.tw'
},
'test': {
'ad_account': 'test',
'display_name': '測試使用者',
'email': 'test@panjit.com.tw'
},
'user1': {
'ad_account': 'user1',
'display_name': '使用者一',
'email': 'user1@panjit.com.tw'
}
}
valid_accounts = {}
for account in ad_accounts:
if account.lower() in mock_users:
valid_accounts[account] = mock_users[account.lower()]
return valid_accounts
def test_ldap_connection():
"""Mock LDAP connection test"""
logger.info("Mock LDAP: Connection test - always returns True")
return True

View File

@@ -0,0 +1,225 @@
"""
Notification Service
處理通知邏輯和摘要資料準備
"""
from datetime import datetime, date, timedelta
from sqlalchemy import and_, or_, func
from models import (
db, TodoItem, TodoItemResponsible, TodoItemFollower,
TodoUserPref, TodoAuditLog
)
from utils.logger import get_logger
logger = get_logger(__name__)
class NotificationService:
"""通知服務類別"""
def get_notification_recipients(self, todo):
"""取得待辦事項的通知收件人清單"""
recipients = set()
# 加入建立者(如果啟用通知)
creator_pref = TodoUserPref.query.filter_by(ad_account=todo.creator_ad).first()
if creator_pref and creator_pref.notification_enabled:
recipients.add(todo.creator_ad)
# 加入負責人(如果啟用通知)
for responsible in todo.responsible_users:
user_pref = TodoUserPref.query.filter_by(ad_account=responsible.ad_account).first()
if user_pref and user_pref.notification_enabled:
recipients.add(responsible.ad_account)
# 加入追蹤人(如果啟用通知)
for follower in todo.followers:
user_pref = TodoUserPref.query.filter_by(ad_account=follower.ad_account).first()
if user_pref and user_pref.notification_enabled:
recipients.add(follower.ad_account)
return list(recipients)
def prepare_digest(self, user_ad, digest_type='weekly'):
"""準備摘要資料"""
try:
# 計算日期範圍
today = date.today()
if digest_type == 'daily':
start_date = today
end_date = today
period_name = '今日'
elif digest_type == 'weekly':
start_date = today - timedelta(days=today.weekday()) # 週一
end_date = start_date + timedelta(days=6) # 週日
period_name = '本週'
elif digest_type == 'monthly':
start_date = today.replace(day=1)
next_month = today.replace(day=28) + timedelta(days=4)
end_date = next_month - timedelta(days=next_month.day)
period_name = '本月'
else:
raise ValueError(f"Unsupported digest type: {digest_type}")
# 基礎查詢 - 使用者相關的待辦事項
base_query = TodoItem.query.filter(
or_(
TodoItem.creator_ad == user_ad,
TodoItem.responsible_users.any(TodoItemResponsible.ad_account == user_ad),
TodoItem.followers.any(TodoItemFollower.ad_account == user_ad)
)
)
# 統計資料
stats = {
'total_todos': base_query.count(),
'completed_todos': base_query.filter(TodoItem.status == 'DONE').count(),
'doing_todos': base_query.filter(TodoItem.status == 'DOING').count(),
'blocked_todos': base_query.filter(TodoItem.status == 'BLOCKED').count(),
'new_todos': base_query.filter(TodoItem.status == 'NEW').count()
}
# 期間內完成的待辦事項
completed_in_period = base_query.filter(
and_(
TodoItem.status == 'DONE',
func.date(TodoItem.completed_at).between(start_date, end_date)
)
).all()
# 期間內建立的待辦事項
created_in_period = base_query.filter(
func.date(TodoItem.created_at).between(start_date, end_date)
).all()
# 即將到期的待辦事項未來7天
upcoming_due = base_query.filter(
and_(
TodoItem.due_date.between(today, today + timedelta(days=7)),
TodoItem.status != 'DONE'
)
).order_by(TodoItem.due_date).all()
# 逾期的待辦事項
overdue = base_query.filter(
and_(
TodoItem.due_date < today,
TodoItem.status != 'DONE'
)
).order_by(TodoItem.due_date).all()
# 高優先級待辦事項
high_priority = base_query.filter(
and_(
TodoItem.priority == 'HIGH',
TodoItem.status != 'DONE'
)
).all()
# 活動記錄(期間內的操作)
activities = TodoAuditLog.query.filter(
and_(
TodoAuditLog.actor_ad == user_ad,
func.date(TodoAuditLog.created_at).between(start_date, end_date)
)
).order_by(TodoAuditLog.created_at.desc()).limit(10).all()
# 組織摘要資料
digest_data = {
'type': digest_type,
'period_name': period_name,
'start_date': start_date,
'end_date': end_date,
'user_ad': user_ad,
'stats': stats,
'completed_in_period': [todo.to_dict() for todo in completed_in_period],
'created_in_period': [todo.to_dict() for todo in created_in_period],
'upcoming_due': [todo.to_dict() for todo in upcoming_due],
'overdue': [todo.to_dict() for todo in overdue],
'high_priority': [todo.to_dict() for todo in high_priority],
'recent_activities': [
{
'action': activity.action,
'created_at': activity.created_at,
'detail': activity.detail,
'todo_id': activity.todo_id
}
for activity in activities
],
'generated_at': datetime.now()
}
return digest_data
except Exception as e:
logger.error(f"Failed to prepare digest for {user_ad}: {str(e)}")
raise
def should_send_notification(self, user_ad, notification_type):
"""檢查是否應該發送通知"""
try:
user_pref = TodoUserPref.query.filter_by(ad_account=user_ad).first()
if not user_pref:
return False
# 檢查通知開關
if notification_type == 'email_reminder':
return user_pref.email_reminder_enabled
elif notification_type == 'weekly_summary':
return user_pref.weekly_summary_enabled
elif notification_type == 'general':
return user_pref.notification_enabled
return False
except Exception as e:
logger.error(f"Error checking notification settings for {user_ad}: {str(e)}")
return False
def get_users_for_batch_notifications(self, notification_type):
"""取得需要接收批量通知的使用者清單"""
try:
if notification_type == 'weekly_summary':
users = db.session.query(TodoUserPref.ad_account).filter(
TodoUserPref.weekly_summary_enabled == True
).all()
elif notification_type == 'email_reminder':
users = db.session.query(TodoUserPref.ad_account).filter(
TodoUserPref.email_reminder_enabled == True
).all()
else:
users = db.session.query(TodoUserPref.ad_account).filter(
TodoUserPref.notification_enabled == True
).all()
return [user[0] for user in users]
except Exception as e:
logger.error(f"Error getting users for batch notifications: {str(e)}")
return []
def create_notification_summary(self, todos, notification_type):
"""建立通知摘要"""
try:
if notification_type == 'due_tomorrow':
return {
'title': '明日到期提醒',
'description': f'您有 {len(todos)} 項待辦事項將於明日到期',
'todos': [todo.to_dict() for todo in todos]
}
elif notification_type == 'overdue':
return {
'title': '逾期提醒',
'description': f'您有 {len(todos)} 項待辦事項已逾期',
'todos': [todo.to_dict() for todo in todos]
}
else:
return {
'title': '待辦事項提醒',
'description': f'您有 {len(todos)} 項待辦事項需要關注',
'todos': [todo.to_dict() for todo in todos]
}
except Exception as e:
logger.error(f"Error creating notification summary: {str(e)}")
return None