Coverage for src / lexigram / admin / services / notifications / templates.py: 44%
27 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
« prev ^ index » next coverage.py v7.13.5, created at 2026-08-13 22:14 +0800
1"""Email templates and rendering for admin notifications."""
3from __future__ import annotations
5import re
6from typing import Any
8from lexigram.admin.services.notifications.models import NotificationType
9from lexigram.di.decorators import inject
11# Base template with admin branding
12ADMIN_EMAIL_BASE = """
13<!DOCTYPE html>
14<html>
15<head>
16 <meta charset="utf-8">
17 <meta name="viewport" content="width=device-width, initial-scale=1.0">
18 <style>
19 body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; }}
20 .container {{ max-width: 600px; margin: 0 auto; padding: 20px; }}
21 .header {{ background: #1a56db; color: white; padding: 20px; text-align: center; border-radius: 8px 8px 0 0; }}
22 .content {{ background: #fff; padding: 30px; border: 1px solid #e5e7eb; }}
23 .footer {{ background: #f9fafb; padding: 20px; text-align: center; font-size: 12px; color: #6b7280; border-radius: 0 0 8px 8px; }}
24 .btn {{ display: inline-block; padding: 12px 24px; background: #1a56db; color: white; text-decoration: none; border-radius: 6px; margin: 16px 0; }}
25 .alert {{ padding: 16px; border-radius: 6px; margin: 16px 0; }}
26 .alert-success {{ background: #d1fae5; border: 1px solid #34d399; }}
27 .alert-warning {{ background: #fef3c7; border: 1px solid #f59e0b; }}
28 .alert-error {{ background: #fee2e2; border: 1px solid #ef4444; }}
29 .progress {{ background: #e5e7eb; border-radius: 9999px; height: 8px; overflow: hidden; }}
30 .progress-bar {{ background: #1a56db; height: 100%; transition: width 0.3s; }}
31 table {{ width: 100%; border-collapse: collapse; margin: 16px 0; }}
32 th, td {{ padding: 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }}
33 th {{ background: #f9fafb; font-weight: 600; }}
34 </style>
35</head>
36<body>
37 <div class="container">
38 <div class="header">
39 <h1>{app_name} Admin</h1>
40 </div>
41 <div class="content">
42 {content}
43 </div>
44 <div class="footer">
45 <p>This is an automated notification from {app_name} Admin Panel.</p>
46 <p>You received this email because you are an administrator.</p>
47 </div>
48 </div>
49</body>
50</html>
51"""
53# Specific templates
54EMAIL_TEMPLATES: dict[NotificationType, tuple[str, str]] = {
55 # (subject_template, body_template)
56 NotificationType.USER_CREATED: (
57 "New user created: {user_name}",
58 """
59 <h2>New User Created</h2>
60 <p>A new user account has been created:</p>
61 <table>
62 <tr><th>Name</th><td>{user_name}</td></tr>
63 <tr><th>Email</th><td>{user_email}</td></tr>
64 <tr><th>Role</th><td>{user_role}</td></tr>
65 <tr><th>Created By</th><td>{created_by}</td></tr>
66 <tr><th>Created At</th><td>{created_at}</td></tr>
67 </table>
68 <a href="{user_url}" class="btn">View User</a>
69 """,
70 ),
71 NotificationType.USER_INVITED: (
72 "You've been invited to {app_name} Admin",
73 """
74 <h2>Welcome to {app_name} Admin</h2>
75 <p>Hello {user_name},</p>
76 <p>You've been invited to join {app_name} as an administrator.</p>
77 <p>Click the button below to set up your account:</p>
78 <a href="{invite_url}" class="btn">Accept Invitation</a>
79 <p><small>This invitation expires in {expires_in}.</small></p>
80 """,
81 ),
82 NotificationType.PASSWORD_RESET: (
83 "Password Reset Request",
84 """
85 <h2>Password Reset</h2>
86 <p>Hello {user_name},</p>
87 <p>We received a request to reset your password. Click the button below to create a new password:</p>
88 <a href="{reset_url}" class="btn">Reset Password</a>
89 <p><small>This link expires in {expires_in}. If you didn't request this, you can safely ignore this email.</small></p>
90 """,
91 ),
92 NotificationType.BULK_STARTED: (
93 "Bulk operation started: {operation_name}",
94 """
95 <h2>Bulk Operation Started</h2>
96 <p>A bulk operation has been started:</p>
97 <table>
98 <tr><th>Operation</th><td>{operation_name}</td></tr>
99 <tr><th>Resource</th><td>{resource}</td></tr>
100 <tr><th>Total Items</th><td>{total_items}</td></tr>
101 <tr><th>Started By</th><td>{started_by}</td></tr>
102 <tr><th>Started At</th><td>{started_at}</td></tr>
103 </table>
104 <p>You will receive another notification when the operation completes.</p>
105 """,
106 ),
107 NotificationType.BULK_PROGRESS: (
108 "Bulk operation progress: {progress}%",
109 """
110 <h2>Bulk Operation Progress</h2>
111 <p>Operation: {operation_name}</p>
112 <div class="progress">
113 <div class="progress-bar" style="width: {progress}%"></div>
114 </div>
115 <p>{processed} of {total_items} items processed ({progress}%)</p>
116 <table>
117 <tr><th>Successful</th><td>{successful}</td></tr>
118 <tr><th>Failed</th><td>{failed}</td></tr>
119 <tr><th>Remaining</th><td>{remaining}</td></tr>
120 </table>
121 """,
122 ),
123 NotificationType.BULK_COMPLETED: (
124 "Bulk operation completed: {operation_name}",
125 """
126 <h2>Bulk Operation Completed</h2>
127 <div class="alert alert-success">
128 <strong>Success!</strong> The bulk operation has completed.
129 </div>
130 <table>
131 <tr><th>Operation</th><td>{operation_name}</td></tr>
132 <tr><th>Resource</th><td>{resource}</td></tr>
133 <tr><th>Total Items</th><td>{total_items}</td></tr>
134 <tr><th>Successful</th><td>{successful}</td></tr>
135 <tr><th>Failed</th><td>{failed}</td></tr>
136 <tr><th>Duration</th><td>{duration}</td></tr>
137 </table>
138 <a href="{results_url}" class="btn">View Results</a>
139 """,
140 ),
141 NotificationType.BULK_FAILED: (
142 "Bulk operation failed: {operation_name}",
143 """
144 <h2>Bulk Operation Failed</h2>
145 <div class="alert alert-error">
146 <strong>Error!</strong> The bulk operation failed.
147 </div>
148 <table>
149 <tr><th>Operation</th><td>{operation_name}</td></tr>
150 <tr><th>Error</th><td>{error_message}</td></tr>
151 <tr><th>Processed Before Failure</th><td>{processed}</td></tr>
152 </table>
153 <p>Please check the logs for more details.</p>
154 """,
155 ),
156 NotificationType.EXPORT_READY: (
157 "Your export is ready: {export_name}",
158 """
159 <h2>Export Ready</h2>
160 <div class="alert alert-success">
161 <strong>Success!</strong> Your data export is ready for download.
162 </div>
163 <table>
164 <tr><th>Export</th><td>{export_name}</td></tr>
165 <tr><th>Format</th><td>{format}</td></tr>
166 <tr><th>Records</th><td>{record_count}</td></tr>
167 <tr><th>File Size</th><td>{file_size}</td></tr>
168 </table>
169 <a href="{download_url}" class="btn">Download Export</a>
170 <p><small>This link expires in {expires_in}.</small></p>
171 """,
172 ),
173 NotificationType.SYSTEM_ALERT: (
174 "[{severity}] System Alert: {alert_title}",
175 """
176 <h2>System Alert</h2>
177 <div class="alert alert-{severity_class}">
178 <strong>{severity}:</strong> {alert_title}
179 </div>
180 <p>{alert_message}</p>
181 <table>
182 <tr><th>Time</th><td>{occurred_at}</td></tr>
183 <tr><th>Component</th><td>{component}</td></tr>
184 </table>
185 """,
186 ),
187}
190@inject
191class TemplateRenderer:
192 """Handles rendering of email templates."""
194 def __init__(self, app_name: str = "Admin"):
195 """Initialize with app name for templates."""
196 self.app_name = app_name
198 def render_template(
199 self,
200 notification_type: NotificationType,
201 data: dict[str, Any],
202 ) -> tuple[str, str, str]:
203 """Render email template.
205 Args:
206 notification_type: Type of notification
207 data: Template data
209 Returns:
210 Tuple of (subject, text_body, html_body)
211 """
212 templates = EMAIL_TEMPLATES.get(notification_type)
213 if not templates:
214 return (
215 data.get("subject", "Admin Notification"),
216 data.get("body", ""),
217 "",
218 )
220 subject_tpl, body_tpl = templates
222 # Add app name to data
223 full_data = {
224 "app_name": self.app_name,
225 **data,
226 }
228 # Render
229 try:
230 subject = subject_tpl.format(**full_data)
231 body_content = body_tpl.format(**full_data)
232 html_body = ADMIN_EMAIL_BASE.format(
233 app_name=self.app_name,
234 content=body_content,
235 )
237 # Simple text conversion
238 text_body = re.sub(r"<[^>]+>", "", body_content)
239 text_body = re.sub(r"\s+", " ", text_body).strip()
241 return subject, text_body, html_body
242 except KeyError as e:
243 return (
244 f"Admin Notification: {notification_type.value}",
245 f"Missing template data: {e}",
246 "",
247 )
250__all__ = [
251 "ADMIN_EMAIL_BASE",
252 "EMAIL_TEMPLATES",
253 "TemplateRenderer",
254]