From 5807e5b7dccf0fb100e634d32335c5c0c9d181c2 Mon Sep 17 00:00:00 2001 From: ngovinh2k2 Date: Fri, 3 Jul 2026 09:44:28 +0700 Subject: [PATCH] feat: update the email ui in the template folder --- apps/authentication/services.py | 13 ------- apps/authentication/views.py | 22 +++++++----- apps/custom_email/serializers.py | 27 +++++++++++++++ apps/custom_email/service.py | 43 ++++++++++++++++++++++++ apps/custom_email/views.py | 1 + apps/organization_setting/serializers.py | 3 -- apps/organization_setting/services.py | 2 +- apps/organization_setting/views.py | 23 ++++++++++++- 8 files changed, 107 insertions(+), 27 deletions(-) diff --git a/apps/authentication/services.py b/apps/authentication/services.py index 7d405eb..31f29a5 100644 --- a/apps/authentication/services.py +++ b/apps/authentication/services.py @@ -4,9 +4,7 @@ from common.apps.refresh_tokens.services import create_jwt_tokens from django.conf import settings from django.core.cache import cache -from django.template.loader import render_to_string from rest_framework import status -from rest_framework.exceptions import ValidationError from rest_framework.response import Response from apps.authentication.models import RootUser @@ -92,14 +90,3 @@ def handle_access_token(access_token, provider: Literal["GOOGLE"]): "default_organization": default_organization_slug, }, ) - - -def render_email_format(template, data): - try: - html_message = render_to_string( - template, - data, - ) - return html_message - except Exception as e: - raise ValidationError({"error": f"Error: {e}"}) diff --git a/apps/authentication/views.py b/apps/authentication/views.py index c419c6c..e4dab92 100644 --- a/apps/authentication/views.py +++ b/apps/authentication/views.py @@ -1,6 +1,7 @@ from datetime import datetime, timezone from common.apps.refresh_tokens.serializers import TokenPairSerializer +from common.utils.email_context import get_email_context, render_email_format from common.utils.send_email import send_email from common.utils.token_jwt import generate_token from django.conf import settings @@ -22,10 +23,9 @@ SendEmailSerializer, UserSerializer, ) -from apps.authentication.services import ( - create_organization_access_token, - render_email_format, -) +from apps.authentication.services import create_organization_access_token +from apps.custom_email.constants import EmailTypes +from apps.custom_email.service import get_custom_email_item class LoginAPIView(TokenObtainPairView): @@ -69,11 +69,15 @@ def post(self, request): subject = "🔒 Forgot your password? Reset now" token = generate_token({"email": email}) - data = { - "redirect_url": f"{settings.HOST_FRONTEND_ADMIN}/auth/reset-password?token={token}", - "host": settings.HOST, - } - message = render_email_format("email_forget_password.html", data) + custom_email_item = get_custom_email_item(EmailTypes.RESET_PASSWORD) + email_context = get_email_context( + { + "host": settings.HOST, + "redirect_url": f"{settings.HOST_FRONTEND_ADMIN}/auth/reset-password?token={token}", + }, + custom_email=custom_email_item, + ) + message = render_email_format("email_forget_password.html", email_context) send_email(settings.DEFAULT_FROM_EMAIL, [email], subject, message) return Response( { diff --git a/apps/custom_email/serializers.py b/apps/custom_email/serializers.py index f7fdad8..c0f0e56 100644 --- a/apps/custom_email/serializers.py +++ b/apps/custom_email/serializers.py @@ -4,13 +4,22 @@ from apps.custom_email.constants import EmailTypes from apps.custom_email.models import OrganizationEmail +from apps.custom_email.service import get_theme_logo_url class OrganizationEmailSerializer(serializers.ModelSerializer): + brand_logo_dark = serializers.SerializerMethodField() + brand_logo_light = serializers.SerializerMethodField() + brand_name = serializers.SerializerMethodField() + class Meta: model = OrganizationEmail fields = [ "id", + "brand_logo_dark", + "brand_logo_light", + "brand_name", + "email_type", "email_type", "sender_name", "sender_email", @@ -25,6 +34,9 @@ class Meta: "id": {"read_only": True}, "email_type": {"required": True}, "header_image": {"write_only": True}, + "brand_logo_dark": {"read_only": True}, + "brand_logo_light": {"read_only": True}, + "brand_name": {"read_only": True}, } def to_representation(self, instance): @@ -47,3 +59,18 @@ def to_representation(self, instance): f"{host}/static/images/{image_path}" if image_path else "" ) return data + + def get_brand_logo_dark(self, instance): + return get_theme_logo_url(instance, "dark") + + def get_brand_logo_light(self, instance): + return get_theme_logo_url(instance, "light") + + def get_brand_name(self, instance): + organization = getattr(instance, "organization", None) + setting = ( + getattr(organization, "organization_settings", None) + if organization + else None + ) + return getattr(setting, "brand_name", "") or "" diff --git a/apps/custom_email/service.py b/apps/custom_email/service.py index cf303db..6409ee9 100644 --- a/apps/custom_email/service.py +++ b/apps/custom_email/service.py @@ -1,5 +1,31 @@ +from common.apps.upload_file.service import get_presigned_url +from django.conf import settings + from apps.custom_email.constants import EmailTypes from apps.custom_email.models import OrganizationEmail +from apps.organization_setting.constants import ThemeType + + +def get_theme_logo_url(instance, theme_key): + host = settings.HOST.rstrip("/") + default_logo = "logo_white.png" if theme_key == ThemeType.DARK else "logo_black.png" + organization = getattr(instance, "organization", None) + if not organization: + return f"{host}/static/images/branding/{default_logo}" + + setting = getattr(organization, "organization_settings", None) + if not setting: + return f"{host}/static/images/branding/{default_logo}" + + theme = setting.themes.filter(theme_key=theme_key).first() + + if not theme or not theme.logo: + return f"{host}/static/images/branding/{default_logo}" + + return get_presigned_url( + settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"), + f"uploads/{theme.logo}", + ) def get_default_organization_emails(): @@ -57,3 +83,20 @@ def create_default_organization_email(organization): for email_data in get_default_organization_emails() ] ) + + +def get_custom_email_item(email_type): + from apps.custom_email.serializers import OrganizationEmailSerializer + + custom_email = next( + ( + item + for item in get_default_organization_emails() + if item.get("email_type") == email_type + ), + None, + ) + if not custom_email: + return {} + + return OrganizationEmailSerializer(OrganizationEmail(**custom_email)).data diff --git a/apps/custom_email/views.py b/apps/custom_email/views.py index ec0c5b1..1298b79 100644 --- a/apps/custom_email/views.py +++ b/apps/custom_email/views.py @@ -12,5 +12,6 @@ class ListCustomEmailView(OrganizationListAPIView): queryset = OrganizationEmail.objects.all() organization_field = "organization" pagination_class = BasePagination + filterset_fields = ["email_type"] filter_backends = [DjangoFilterBackend, OrderingFilter] ordering = ["-created_at"] diff --git a/apps/organization_setting/serializers.py b/apps/organization_setting/serializers.py index c350e10..797a777 100644 --- a/apps/organization_setting/serializers.py +++ b/apps/organization_setting/serializers.py @@ -149,9 +149,6 @@ def _update_custom_emails(self, instance, custom_emails_data): ) self._save_instance(custom_email, custom_email_data) - def to_representation(self, instance): - return OrganizationConfigSerializer(instance, context=self.context).data - def update(self, instance, validated_data): with transaction.atomic(): custom_pages_data = validated_data.pop("custom_pages", []) diff --git a/apps/organization_setting/services.py b/apps/organization_setting/services.py index 5062613..d016f4d 100644 --- a/apps/organization_setting/services.py +++ b/apps/organization_setting/services.py @@ -38,7 +38,7 @@ "logo": "", "theme_colors": { "primary": "#171A28", - "outline": "#FFFFFF", + "outline": "#171A28", "background": "#FFFFFF", "text": "#1F2937", "input": "#F0F1F3", diff --git a/apps/organization_setting/views.py b/apps/organization_setting/views.py index 8f12006..16c13c7 100644 --- a/apps/organization_setting/views.py +++ b/apps/organization_setting/views.py @@ -1,9 +1,13 @@ from django.shortcuts import get_object_or_404 from rest_framework import generics +from rest_framework.response import Response from apps.organization.models import Organization from apps.organization_setting.models import OrganizationSetting -from apps.organization_setting.serializers import UpdateOrganizationSettingSerializer +from apps.organization_setting.serializers import ( + OrganizationConfigSerializer, + UpdateOrganizationSettingSerializer, +) class UpdateOrganizationSettingView(generics.UpdateAPIView): @@ -23,3 +27,20 @@ def get_object(self): is_active=True, ) return get_object_or_404(self.get_queryset(), organization=organization) + + def update(self, request, *args, **kwargs): + partial = kwargs.pop("partial", False) + instance = self.get_object() + serializer = self.get_serializer( + instance, + data=request.data, + partial=partial, + ) + serializer.is_valid(raise_exception=True) + updated_instance = serializer.save() + fresh_instance = self.get_queryset().get(pk=updated_instance.pk) + response_serializer = OrganizationConfigSerializer( + fresh_instance, + context=self.get_serializer_context(), + ) + return Response(response_serializer.data)