Skip to content

feat: implement custom emails models#26

Merged
ngovinh2k2 merged 1 commit into
devfrom
feat/implement-custom-emails-models
Jul 2, 2026
Merged

feat: implement custom emails models#26
ngovinh2k2 merged 1 commit into
devfrom
feat/implement-custom-emails-models

Conversation

@ngovinh2k2

Copy link
Copy Markdown
Member

What?

Implement the custom emails models

Why?

How?

Testing?

  • Functional Testing
  • Security
  • Performance
  • Error Handling
  • Code Quality
  • Documentation
  • Database
  • Deployment
  • Final Review

Anything Else?

@ngovinh2k2
ngovinh2k2 merged commit b8c18a1 into dev Jul 2, 2026
1 check passed

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a new custom_email application to allow organizations to customize their email configurations, including sender details, theme colors, and header images. It integrates these custom emails into the organization settings and provides a migration to backfill existing organizations with default email configurations. The review feedback highlights several important improvements: avoiding the import of application services within migrations to prevent future schema mismatch failures, using models.EmailField instead of models.CharField for email validation, and safely accessing Django settings with getattr to avoid potential runtime errors in environments where specific settings might be missing.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +3 to +30
from apps.custom_email.service import get_default_organization_emails


def backfill_organization_emails(apps, schema_editor):
Organization = apps.get_model("organization", "Organization")
OrganizationEmail = apps.get_model("custom_email", "OrganizationEmail")

existing_pairs = set(
OrganizationEmail.objects.values_list("organization_id", "email_type")
)
emails_to_create = []
default_emails = get_default_organization_emails()

for organization in Organization.objects.all().iterator():
for email_data in default_emails:
email_type = email_data["email_type"]
if (organization.id, email_type) in existing_pairs:
continue

emails_to_create.append(
OrganizationEmail(
organization_id=organization.id,
**email_data,
)
)

if emails_to_create:
OrganizationEmail.objects.bulk_create(emails_to_create, batch_size=100)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Importing helper functions or models from application code (like apps.custom_email.service) inside Django migrations is a known anti-pattern. If the models or services change in the future, running this migration from scratch will fail because the historical database schema won't match the current codebase. Instead, define the default email data directly within the migration file or in a static constants file that does not import any Django models.

def get_default_emails():
    common_data = {
        "sender_name": "The @SpaceDF team",
        "sender_email": "support@spacedf.com",
        "footer_text": "©2025 Digital Fortress. All rights reserved.",
        "header_image": "",
        "show_logo": True,
        "social_links": {
            "linkedin_url": "https://vn.linkedin.com/company/digital-fortress-vn",
            "facebook_url": "https://www.facebook.com/digitalfortress.dev/",
            "tiktok_url": "",
            "instagram_url": "https://www.instagram.com/df.iot/",
        },
        "metadata": {
            "show_facebook": True,
            "show_instagram": True,
            "show_linkedin": True,
            "show_tiktok": False,
        },
    }
    return [
        {
            **common_data,
            "email_type": "invitation_to_space",
            "theme_colors": {
                "background_color": "#FFFFFF",
                "primary_color": "#171A28",
            },
        },
        {
            **common_data,
            "email_type": "verification_code",
            "theme_colors": {
                "background_color": "#FFFFFF",
                "primary_color": "#171A28",
            },
        },
        {
            **common_data,
            "email_type": "reset_password",
            "theme_colors": {
                "background_color": "#FFFFFF",
                "primary_color": "#171A28",
            },
        },
    ]


def backfill_organization_emails(apps, schema_editor):
    Organization = apps.get_model("organization", "Organization")
    OrganizationEmail = apps.get_model("custom_email", "OrganizationEmail")

    existing_pairs = set(
        OrganizationEmail.objects.values_list("organization_id", "email_type")
    )
    emails_to_create = []
    default_emails = get_default_emails()

    for organization in Organization.objects.all().iterator():
        for email_data in default_emails:
            email_type = email_data["email_type"]
            if (organization.id, email_type) in existing_pairs:
                continue

            emails_to_create.append(
                OrganizationEmail(
                    organization_id=organization.id,
                    **email_data,
                )
            )

    if emails_to_create:
        OrganizationEmail.objects.bulk_create(emails_to_create, batch_size=100)

)
email_type = models.CharField(max_length=255, choices=EmailTypes.choices)
sender_name = models.CharField(max_length=255, blank=True)
sender_email = models.CharField(max_length=255, blank=True)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using models.EmailField instead of models.CharField for email addresses is a standard Django best practice. It automatically validates that the input is a valid email address at both the model and form/serializer levels.

Suggested change
sender_email = models.CharField(max_length=255, blank=True)
sender_email = models.EmailField(max_length=255, blank=True)

),
),
("sender_name", models.CharField(blank=True, max_length=255)),
("sender_email", models.CharField(blank=True, max_length=255)),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To match the model definition change of sender_email to EmailField, update the field type in the initial migration as well.

Suggested change
("sender_email", models.CharField(blank=True, max_length=255)),
("sender_email", models.EmailField(blank=True, max_length=255)),

Comment on lines +32 to +49
if instance.header_image:
data["url_header_image"] = get_presigned_url(
settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.header_image}",
)
return data

image_mapping = {
EmailTypes.INVITATION_TO_SPACE: "auth/invitation.png",
EmailTypes.VERIFICATION_CODE: "auth/verification_code.png",
EmailTypes.RESET_PASSWORD: "auth/forget_password.png",
}
image_path = image_mapping.get(instance.email_type, "")
host = settings.HOST.rstrip("/")
data["url_header_image"] = (
f"{host}/static/images/{image_path}" if image_path else ""
)
return data

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Accessing settings.AWS_S3 and settings.HOST directly can lead to AttributeError or other runtime crashes if these settings are not defined or are set to None in certain environments (e.g., local development or testing). Use getattr with a safe fallback to make this code more robust.

Suggested change
if instance.header_image:
data["url_header_image"] = get_presigned_url(
settings.AWS_S3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.header_image}",
)
return data
image_mapping = {
EmailTypes.INVITATION_TO_SPACE: "auth/invitation.png",
EmailTypes.VERIFICATION_CODE: "auth/verification_code.png",
EmailTypes.RESET_PASSWORD: "auth/forget_password.png",
}
image_path = image_mapping.get(instance.email_type, "")
host = settings.HOST.rstrip("/")
data["url_header_image"] = (
f"{host}/static/images/{image_path}" if image_path else ""
)
return data
if instance.header_image:
aws_s3 = getattr(settings, "AWS_S3", {}) or {}
data["url_header_image"] = get_presigned_url(
aws_s3.get("AWS_STORAGE_BUCKET_NAME"),
f"uploads/{instance.header_image}",
)
return data
image_mapping = {
EmailTypes.INVITATION_TO_SPACE: "auth/invitation.png",
EmailTypes.VERIFICATION_CODE: "auth/verification_code.png",
EmailTypes.RESET_PASSWORD: "auth/forget_password.png",
}
image_path = image_mapping.get(instance.email_type, "")
host = getattr(settings, "HOST", "") or ""
host = host.rstrip("/")
data["url_header_image"] = (
f"{host}/static/images/{image_path}" if image_path and host else ""
)
return data

@ngovinh2k2
ngovinh2k2 deleted the feat/implement-custom-emails-models branch July 3, 2026 08:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant