-
Notifications
You must be signed in to change notification settings - Fork 0
feat: implement custom emails models #26
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| from django.apps import AppConfig | ||
|
|
||
|
|
||
| class CustomEmailConfig(AppConfig): | ||
| default_auto_field = "django.db.models.BigAutoField" | ||
| name = "apps.custom_email" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| from django.db import models | ||
|
|
||
|
|
||
| class EmailTypes(models.TextChoices): | ||
| INVITATION_TO_SPACE = "invitation_to_space" | ||
| VERIFICATION_CODE = "verification_code" | ||
| RESET_PASSWORD = "reset_password" # nosec B105 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,72 @@ | ||
| # Generated by Django 5.0.6 on 2026-06-01 00:00 | ||
|
|
||
| import django.db.models.deletion | ||
| import uuid | ||
| from django.db import migrations, models | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| initial = True | ||
|
|
||
| dependencies = [ | ||
| ("organization", "0004_alter_organization_template"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.CreateModel( | ||
| name="OrganizationEmail", | ||
| fields=[ | ||
| ( | ||
| "id", | ||
| models.UUIDField( | ||
| default=uuid.uuid4, | ||
| editable=False, | ||
| primary_key=True, | ||
| serialize=False, | ||
| unique=True, | ||
| ), | ||
| ), | ||
| ("created_at", models.DateTimeField(auto_now_add=True)), | ||
| ("updated_at", models.DateTimeField(auto_now=True)), | ||
| ( | ||
| "email_type", | ||
| models.CharField( | ||
| choices=[ | ||
| ("invitation_to_space", "Invitation To Space"), | ||
| ("verification_code", "Verification Code"), | ||
| ("reset_password", "Reset Password"), | ||
| ], | ||
| max_length=255, | ||
| ), | ||
| ), | ||
| ("sender_name", models.CharField(blank=True, max_length=255)), | ||
| ("sender_email", models.CharField(blank=True, max_length=255)), | ||
| ("theme_colors", models.JSONField(blank=True, default=dict)), | ||
| ("footer_text", models.TextField(blank=True)), | ||
| ( | ||
| "header_image", | ||
| models.CharField(blank=True, default="", max_length=500), | ||
| ), | ||
| ("show_logo", models.BooleanField(default=True)), | ||
| ("social_links", models.JSONField(blank=True, default=dict)), | ||
| ("metadata", models.JSONField(blank=True, default=dict)), | ||
| ( | ||
| "organization", | ||
| models.ForeignKey( | ||
| on_delete=django.db.models.deletion.CASCADE, | ||
| related_name="organization_custom_emails", | ||
| to="organization.organization", | ||
| ), | ||
| ), | ||
| ], | ||
| options={ | ||
| "db_table": "custom_emails", | ||
| "constraints": [ | ||
| models.UniqueConstraint( | ||
| fields=("organization", "email_type"), | ||
| name="unique_organization_email_type", | ||
| ) | ||
| ], | ||
| }, | ||
| ), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| from django.db import migrations | ||
|
|
||
| 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) | ||
|
Comment on lines
+3
to
+30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Importing helper functions or models from application code (like 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) |
||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("custom_email", "0001_initial"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.RunPython( | ||
| backfill_organization_emails, | ||
| migrations.RunPython.noop, | ||
| ), | ||
| ] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,31 @@ | ||||||
| from common.models.base_model import BaseModel | ||||||
| from django.db import models | ||||||
|
|
||||||
| from apps.custom_email.constants import EmailTypes | ||||||
| from apps.organization.models import Organization | ||||||
|
|
||||||
|
|
||||||
| class OrganizationEmail(BaseModel): | ||||||
| organization = models.ForeignKey( | ||||||
| Organization, | ||||||
| on_delete=models.CASCADE, | ||||||
| related_name="organization_custom_emails", | ||||||
| ) | ||||||
| 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. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using
Suggested change
|
||||||
| theme_colors = models.JSONField(default=dict, blank=True) | ||||||
| footer_text = models.TextField(blank=True) | ||||||
| header_image = models.CharField(max_length=500, blank=True, default="") | ||||||
| show_logo = models.BooleanField(default=True) | ||||||
| social_links = models.JSONField(default=dict, blank=True) | ||||||
| metadata = models.JSONField(default=dict, blank=True) | ||||||
|
|
||||||
| class Meta: | ||||||
| db_table = "custom_emails" | ||||||
| constraints = [ | ||||||
| models.UniqueConstraint( | ||||||
| fields=["organization", "email_type"], | ||||||
| name="unique_organization_email_type", | ||||||
| ) | ||||||
| ] | ||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,49 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from common.apps.upload_file.service import get_presigned_url | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from django.conf import settings | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from rest_framework import serializers | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from apps.custom_email.constants import EmailTypes | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from apps.custom_email.models import OrganizationEmail | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class OrganizationEmailSerializer(serializers.ModelSerializer): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class Meta: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| model = OrganizationEmail | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| fields = [ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "email_type", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "sender_name", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "sender_email", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "theme_colors", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "footer_text", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "header_image", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "show_logo", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "social_links", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "metadata", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ] | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| extra_kwargs = { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "id": {"read_only": True}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "email_type": {"required": True}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "header_image": {"write_only": True}, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| def to_representation(self, instance): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| data = super().to_representation(instance) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| 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 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+32
to
+49
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Accessing
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| from apps.custom_email.constants import EmailTypes | ||
| from apps.custom_email.models import OrganizationEmail | ||
|
|
||
|
|
||
| def get_default_organization_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": EmailTypes.INVITATION_TO_SPACE, | ||
| "theme_colors": { | ||
| "background_color": "#FFFFFF", | ||
| "primary_color": "#171A28", | ||
| }, | ||
| }, | ||
| { | ||
| **common_data, | ||
| "email_type": EmailTypes.VERIFICATION_CODE, | ||
| "theme_colors": { | ||
| "background_color": "#FFFFFF", | ||
| "primary_color": "#171A28", | ||
| }, | ||
| }, | ||
| { | ||
| **common_data, | ||
| "email_type": EmailTypes.RESET_PASSWORD, | ||
| "theme_colors": { | ||
| "background_color": "#FFFFFF", | ||
| "primary_color": "#171A28", | ||
| }, | ||
| }, | ||
| ] | ||
|
|
||
|
|
||
| def create_default_organization_email(organization): | ||
| return OrganizationEmail.objects.bulk_create( | ||
| [ | ||
| OrganizationEmail(organization=organization, **email_data) | ||
| for email_data in get_default_organization_emails() | ||
| ] | ||
| ) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| from django.urls import path | ||
|
|
||
| from apps.custom_email.views import ListCustomEmailView | ||
|
|
||
| app_name = "custom_email" | ||
|
|
||
| urlpatterns = [ | ||
| path("custom-emails", ListCustomEmailView.as_view(), name="custom-emails-list"), | ||
| ] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| from common.pagination.base_pagination import BasePagination | ||
| from django_filters.rest_framework import DjangoFilterBackend | ||
| from rest_framework.filters import OrderingFilter | ||
|
|
||
| from apps.custom_email.models import OrganizationEmail | ||
| from apps.custom_email.serializers import OrganizationEmailSerializer | ||
| from utils.views import OrganizationListAPIView | ||
|
|
||
|
|
||
| class ListCustomEmailView(OrganizationListAPIView): | ||
| serializer_class = OrganizationEmailSerializer | ||
| queryset = OrganizationEmail.objects.all() | ||
| organization_field = "organization" | ||
| pagination_class = BasePagination | ||
| filter_backends = [DjangoFilterBackend, OrderingFilter] | ||
| ordering = ["-created_at"] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To match the model definition change of
sender_emailtoEmailField, update the field type in the initial migration as well.