This commit is contained in:
anhduy-tech
2026-01-30 12:03:15 +07:00
parent f55a0bbfac
commit 69937da0b6
8 changed files with 669 additions and 66 deletions

View File

@@ -27,6 +27,8 @@ from concurrent.futures import ThreadPoolExecutor
import json
import io
import pandas as pd
import numpy as np
from num2words import num2words
from django.db import transaction
from django.apps import apps
from django.core.exceptions import FieldDoesNotExist
@@ -1347,4 +1349,266 @@ def generate_document(request):
time.sleep(5)
count += 1
return Response({"error": "Timeout chờ generate."}, status=status.HTTP_408_REQUEST_TIMEOUT)
return Response({"error": "Timeout chờ generate."}, status=status.HTTP_408_REQUEST_TIMEOUT)
#=============================================================================
# EMAIL TEMPLATE PREVIEW
#=============================================================================
class EmailTemplatePreview:
"""
Class để preview nội dung email template với dữ liệu đã được map
Không gửi email, chỉ trả về nội dung đã render
"""
def __init__(self, template, context_pks: dict):
self.template = template
self.context_pks = context_pks
self.config = self.template.content
self.data_context = {}
self.replacements = {}
def _get_model(self, model_string):
"""Lấy model class từ string 'app.Model'"""
app_label, model_name = model_string.split(".")
return apps.get_model(app_label, model_name)
def _get_value_from_object(self, obj, field_path):
"""Lấy giá trị từ object theo field path (hỗ trợ nested: 'user.profile.name')"""
if obj is None:
return None
value = obj
for part in field_path.replace("__", ".").split("."):
if value is None:
return None
value = getattr(value, part, None)
return value
def _resolve_lookup_value(self, lookup_from):
"""Resolve giá trị lookup từ context_pks hoặc data_context"""
if lookup_from in self.context_pks:
return self.context_pks[lookup_from]
try:
alias, field_path = lookup_from.split(".", 1)
if alias not in self.data_context:
raise ValueError(f"Alias '{alias}' not found in data context.")
source_object = self.data_context.get(alias)
return self._get_value_from_object(source_object, field_path)
except ValueError:
raise ValueError(f"Could not resolve '{lookup_from}'.")
def fetch_data(self):
"""Fetch data từ database theo mappings config"""
mappings = self.config.get("mappings", [])
if not isinstance(mappings, list):
raise TypeError("Email template 'mappings' must be a list.")
# Process trigger object first
trigger_model_mapping = next((m for m in mappings if m.get("is_trigger_object", False)), None)
if trigger_model_mapping:
model_cls = self._get_model(trigger_model_mapping["model"])
lookup_field = trigger_model_mapping["lookup_field"]
lookup_value = self._resolve_lookup_value(trigger_model_mapping["lookup_value_from"])
alias = trigger_model_mapping["alias"]
if lookup_value is not None:
self.data_context[alias] = model_cls.objects.filter(**{lookup_field: lookup_value}).first()
else:
self.data_context[alias] = None
# Process other mappings
for mapping in mappings:
if mapping.get("is_trigger_object", False):
continue
model_cls = self._get_model(mapping["model"])
lookup_field = mapping["lookup_field"]
lookup_value = self._resolve_lookup_value(mapping["lookup_value_from"])
alias = mapping["alias"]
if lookup_value is None:
self.data_context[alias] = None if mapping.get("type") == "object" else []
continue
queryset = model_cls.objects.filter(**{lookup_field: lookup_value})
if mapping.get("type") == "object":
self.data_context[alias] = queryset.first()
elif mapping.get("type") == "list":
self.data_context[alias] = list(queryset)
def _format_value(self, value, format_config):
"""Format value theo config (currency, date, number_to_words, conditional)"""
if value is None:
return ""
format_type = format_config.get("type")
if not format_type:
return str(value)
try:
if format_type == "currency":
return "{:,}".format(np.int64(value)).replace(",", ".")
if format_type == "date":
date_format = format_config.get("format", "dd/mm/YYYY").replace("dd", "%d").replace("mm", "%m").replace("YYYY", "%Y")
return value.strftime(date_format)
if format_type == "number_to_words":
return num2words(value, lang=format_config.get("lang", "vi"))
if format_type == "conditional":
return format_config["true_value"] if value else format_config["false_value"]
except Exception as e:
print(f"Error formatting value '{value}' with config '{format_config}': {e}")
return ""
return str(value)
def prepare_replacements(self):
"""Chuẩn bị dict replacements cho placeholders"""
# Add date placeholders
today = datetime.now()
self.replacements['[day]'] = str(today.day)
self.replacements['[month]'] = str(today.month)
self.replacements['[year]'] = str(today.year)
self.replacements['[date]'] = today.strftime("%d/%m/%Y")
# Process field mappings
mappings = self.config.get("mappings", [])
for mapping in mappings:
alias = mapping["alias"]
data = self.data_context.get(alias)
fields = mapping.get("fields", {})
if mapping.get("type") == "object":
if data is None:
for placeholder in fields:
self.replacements[placeholder] = ""
continue
for placeholder, config in fields.items():
if isinstance(config, dict):
value = self._get_value_from_object(data, config["source"])
self.replacements[placeholder] = self._format_value(value, config.get("format", {}))
else:
value = self._get_value_from_object(data, config)
self.replacements[placeholder] = str(value) if value is not None else ""
def get_preview(self):
"""
Main method để lấy preview email content - trả về nội dung y nguyên đã thay thế placeholders
Returns:
dict: {
'subject': subject đã thay thế placeholders,
'content': body content đã thay thế placeholders (giữ nguyên format, căn lề),
'recipient_email': email người nhận,
'replacements': dict của tất cả replacements được áp dụng
}
"""
try:
print(f"Generating preview for template: {self.template.name}")
# Fetch data and prepare replacements
self.fetch_data()
self.prepare_replacements()
# Get templates from config
subject_template = self.config.get("subject", "")
body_template = self.config.get("content", "")
recipient_placeholder = self.config.get("recipient_placeholder", "[customer.email]")
# Apply replacements - giữ nguyên format, căn lề của template gốc
final_subject = subject_template
final_content = body_template
for key, value in self.replacements.items():
final_subject = final_subject.replace(key, str(value))
final_content = final_content.replace(key, str(value))
recipient_email = self.replacements.get(recipient_placeholder, "")
result = {
'subject': final_subject,
'content': final_content, # Nội dung y nguyên đã thay thế
'recipient_email': recipient_email,
'replacements': self.replacements.copy()
}
print(f"Preview generated successfully for '{self.template.name}'")
return result
except Exception as e:
print(f"Error generating preview for template '{self.template.name}': {e}")
import traceback
traceback.print_exc()
return None
@api_view(['POST'])
def preview_email_template(request):
"""
API để preview email template - trả về nội dung đã thay thế placeholders
POST /api/email/preview/
Body: {
"template_id": 1,
"context_pks": {
"contract_id": 456,
"customer_id": 789
}
}
Response: {
"subject": "Thông báo hợp đồng #HD-001",
"content": "<div>Nội dung y nguyên đã thay thế...</div>",
"recipient_email": "customer@example.com",
"replacements": {
"[contract.code]": "HD-001",
"[customer.name]": "Nguyễn Văn A",
...
}
}
"""
try:
# Validate input
template_id = request.data.get('template_id')
if not template_id:
return Response(
{'error': 'template_id is required'},
status=status.HTTP_400_BAD_REQUEST
)
context_pks = request.data.get('context_pks', {})
if not isinstance(context_pks, dict):
return Response(
{'error': 'context_pks must be a dictionary'},
status=status.HTTP_400_BAD_REQUEST
)
# Get template
try:
template = Email_Template.objects.get(pk=template_id)
except Email_Template.DoesNotExist:
return Response(
{'error': f'Email_Template with id={template_id} does not exist'},
status=status.HTTP_404_NOT_FOUND
)
# Generate preview
previewer = EmailTemplatePreview(template, context_pks)
preview = previewer.get_preview()
if preview:
return Response(preview, status=status.HTTP_200_OK)
else:
return Response(
{'error': 'Failed to generate preview'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)
except Exception as e:
import traceback
traceback.print_exc()
return Response(
{'error': f'Unexpected error: {str(e)}'},
status=status.HTTP_500_INTERNAL_SERVER_ERROR
)