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

1"""Email templates and rendering for admin notifications.""" 

2 

3from __future__ import annotations 

4 

5import re 

6from typing import Any 

7 

8from lexigram.admin.services.notifications.models import NotificationType 

9from lexigram.di.decorators import inject 

10 

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""" 

52 

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} 

188 

189 

190@inject 

191class TemplateRenderer: 

192 """Handles rendering of email templates.""" 

193 

194 def __init__(self, app_name: str = "Admin"): 

195 """Initialize with app name for templates.""" 

196 self.app_name = app_name 

197 

198 def render_template( 

199 self, 

200 notification_type: NotificationType, 

201 data: dict[str, Any], 

202 ) -> tuple[str, str, str]: 

203 """Render email template. 

204 

205 Args: 

206 notification_type: Type of notification 

207 data: Template data 

208 

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 ) 

219 

220 subject_tpl, body_tpl = templates 

221 

222 # Add app name to data 

223 full_data = { 

224 "app_name": self.app_name, 

225 **data, 

226 } 

227 

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 ) 

236 

237 # Simple text conversion 

238 text_body = re.sub(r"<[^>]+>", "", body_content) 

239 text_body = re.sub(r"\s+", " ", text_body).strip() 

240 

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 ) 

248 

249 

250__all__ = [ 

251 "ADMIN_EMAIL_BASE", 

252 "EMAIL_TEMPLATES", 

253 "TemplateRenderer", 

254]