feat: implement custom emails models#26
Conversation
There was a problem hiding this comment.
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.
| 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) |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
| 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)), |
There was a problem hiding this comment.
| 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 |
There was a problem hiding this comment.
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.
| 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 |
What?
Implement the custom emails models
Why?
How?
Testing?
Anything Else?