feat(email): add multilingual support and image handling to email templates

Introduce `EmailImage`, `EmailTemplate`, and `EmailCampaign` models for enhanced email management, including campaign tracking and one-click unsubscribe tokens. Added multilingual fields for email templates and improved accessibility with image alt text. Also adapted `Post` content fields to support translations.
This commit is contained in:
Egor Pavlovich Gorbunov 2026-01-26 15:35:30 +03:00
parent 9cab9fdd3a
commit b5e303e7a5
9 changed files with 874 additions and 1 deletions

View file

@ -1,6 +1,6 @@
.PHONY: help install run restart test test-xml test-html uninstall backup \
generate-env export-env make-messages compile-messages \
format check typecheck precommit clear
format check typecheck precommit clear make-migrations migrate
# Detect OS and set script paths
ifeq ($(OS),Windows_NT)
@ -36,6 +36,8 @@ help: clear
@echo " export-env Export environment variables"
@echo " make-messages Extract translation strings"
@echo " compile-messages Compile translation files"
@echo " make-migrations Generate migration files"
@echo " migrate Apply migration files"
@echo " format Format code with ruff"
@echo " check Lint code with ruff"
@echo " typecheck Typecheck code with ty"
@ -93,6 +95,14 @@ make-messages: clear
compile-messages: clear
@$(call RUN_SCRIPT,compile-messages)
make-migrations: clear
@$(call RUN_SCRIPT,make-migrations)
migrate: clear
@$(call RUN_SCRIPT,migrate)
migration: clear make-migrations migrate
format: clear
@ruff format

View file

@ -0,0 +1,158 @@
# Generated by Django 5.2.9 on 2026-01-26 12:33
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0007_post_is_static_page'),
]
operations = [
migrations.AlterField(
model_name='post',
name='content',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_ar_ar',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_cs_cz',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_da_dk',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_de_de',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_en_gb',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_en_us',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_es_es',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_fa_ir',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_fr_fr',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_he_il',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_hi_in',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_hr_hr',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_id_id',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_it_it',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_ja_jp',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_kk_kz',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_ko_kr',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_nl_nl',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_no_no',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_pl_pl',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_pt_br',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_ro_ro',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_ru_ru',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_sv_se',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_th_th',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_tr_tr',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_vi_vn',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
migrations.AlterField(
model_name='post',
name='content_zh_hans',
field=models.TextField(blank=True, help_text='post content', null=True, verbose_name='content'),
),
]

View file

@ -0,0 +1,111 @@
# Generated by Django 5.2.9 on 2026-01-26 12:33
import django.db.models.deletion
import django_extensions.db.fields
import engine.vibes_auth.emailing.models
import uuid
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vibes_auth', '0006_chatthread_chatmessage_and_more'),
]
operations = [
migrations.CreateModel(
name='EmailImage',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='unique id is used to surely identify any database object', primary_key=True, serialize=False, verbose_name='unique id')),
('is_active', models.BooleanField(default=True, help_text="if set to false, this object can't be seen by users without needed permission", verbose_name='is active')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, help_text='when the object first appeared on the database', verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, help_text='when the object was last modified', verbose_name='modified')),
('name', models.CharField(help_text='descriptive name for the image', max_length=100, verbose_name='name')),
('image', models.ImageField(help_text='image file to use in email templates', upload_to=engine.vibes_auth.emailing.models.get_email_image_path, verbose_name='image')),
('alt_text', models.CharField(blank=True, default='', help_text='alternative text for accessibility', max_length=255, verbose_name='alt text')),
],
options={
'verbose_name': 'email image',
'verbose_name_plural': 'email images',
'ordering': ('-created',),
},
),
migrations.CreateModel(
name='EmailTemplate',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='unique id is used to surely identify any database object', primary_key=True, serialize=False, verbose_name='unique id')),
('is_active', models.BooleanField(default=True, help_text="if set to false, this object can't be seen by users without needed permission", verbose_name='is active')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, help_text='when the object first appeared on the database', verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, help_text='when the object was last modified', verbose_name='modified')),
('name', models.CharField(help_text='internal name for the template', max_length=100, verbose_name='name')),
('slug', models.SlugField(help_text='unique identifier for the template', unique=True, verbose_name='slug')),
('subject', models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, verbose_name='subject')),
('html_content', models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', verbose_name='HTML content')),
('plain_content', models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', verbose_name='plain text content')),
('available_variables', models.TextField(blank=True, default='user.first_name, user.last_name, user.email, project_name, unsubscribe_url', help_text='documentation of available template variables', verbose_name='available variables')),
],
options={
'verbose_name': 'email template',
'verbose_name_plural': 'email templates',
'ordering': ('name',),
},
),
migrations.AddField(
model_name='user',
name='unsubscribe_token',
field=models.UUIDField(default=uuid.uuid4, help_text='token for secure one-click unsubscribe from campaigns', verbose_name='unsubscribe token'),
),
migrations.CreateModel(
name='EmailCampaign',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='unique id is used to surely identify any database object', primary_key=True, serialize=False, verbose_name='unique id')),
('is_active', models.BooleanField(default=True, help_text="if set to false, this object can't be seen by users without needed permission", verbose_name='is active')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, help_text='when the object first appeared on the database', verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, help_text='when the object was last modified', verbose_name='modified')),
('name', models.CharField(help_text='internal name for the campaign', max_length=200, verbose_name='name')),
('status', models.CharField(choices=[('draft', 'Draft'), ('scheduled', 'Scheduled'), ('sending', 'Sending'), ('sent', 'Sent'), ('cancelled', 'Cancelled')], default='draft', max_length=16, verbose_name='status')),
('scheduled_at', models.DateTimeField(blank=True, help_text='when to send the campaign (leave empty for manual send)', null=True, verbose_name='scheduled at')),
('sent_at', models.DateTimeField(blank=True, help_text='when the campaign was actually sent', null=True, verbose_name='sent at')),
('total_recipients', models.PositiveIntegerField(default=0, verbose_name='total recipients')),
('sent_count', models.PositiveIntegerField(default=0, verbose_name='sent count')),
('failed_count', models.PositiveIntegerField(default=0, verbose_name='failed count')),
('opened_count', models.PositiveIntegerField(default=0, verbose_name='opened count')),
('clicked_count', models.PositiveIntegerField(default=0, verbose_name='clicked count')),
('template', models.ForeignKey(help_text='email template to use for this campaign', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='campaigns', to='vibes_auth.emailtemplate', verbose_name='template')),
],
options={
'verbose_name': 'email campaign',
'verbose_name_plural': 'email campaigns',
'ordering': ('-created',),
},
),
migrations.CreateModel(
name='CampaignRecipient',
fields=[
('uuid', models.UUIDField(default=uuid.uuid4, editable=False, help_text='unique id is used to surely identify any database object', primary_key=True, serialize=False, verbose_name='unique id')),
('is_active', models.BooleanField(default=True, help_text="if set to false, this object can't be seen by users without needed permission", verbose_name='is active')),
('created', django_extensions.db.fields.CreationDateTimeField(auto_now_add=True, help_text='when the object first appeared on the database', verbose_name='created')),
('modified', django_extensions.db.fields.ModificationDateTimeField(auto_now=True, help_text='when the object was last modified', verbose_name='modified')),
('status', models.CharField(choices=[('pending', 'Pending'), ('sent', 'Sent'), ('failed', 'Failed'), ('opened', 'Opened'), ('clicked', 'Clicked')], default='pending', max_length=16, verbose_name='status')),
('sent_at', models.DateTimeField(blank=True, null=True, verbose_name='sent at')),
('opened_at', models.DateTimeField(blank=True, null=True, verbose_name='opened at')),
('clicked_at', models.DateTimeField(blank=True, null=True, verbose_name='clicked at')),
('tracking_id', models.UUIDField(default=uuid.uuid4, help_text='unique ID for tracking opens and clicks', unique=True, verbose_name='tracking ID')),
('error_message', models.TextField(blank=True, default='', help_text='error details if sending failed', verbose_name='error message')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='campaign_emails', to=settings.AUTH_USER_MODEL, verbose_name='user')),
('campaign', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='recipients', to='vibes_auth.emailcampaign', verbose_name='campaign')),
],
options={
'verbose_name': 'campaign recipient',
'verbose_name_plural': 'campaign recipients',
'ordering': ('-created',),
'indexes': [models.Index(fields=['campaign', 'status'], name='recipient_camp_status_idx'), models.Index(fields=['tracking_id'], name='recipient_tracking_idx')],
},
),
migrations.AddIndex(
model_name='emailcampaign',
index=models.Index(fields=['status', 'scheduled_at'], name='campaign_status_sched_idx'),
),
]

View file

@ -0,0 +1,433 @@
# Generated by Django 5.2.9 on 2026-01-26 12:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('vibes_auth', '0007_emailimage_emailtemplate_user_unsubscribe_token_and_more'),
]
operations = [
migrations.AddField(
model_name='emailtemplate',
name='html_content_ar_ar',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_cs_cz',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_da_dk',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_de_de',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_en_gb',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_en_us',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_es_es',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_fa_ir',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_fr_fr',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_he_il',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_hi_in',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_hr_hr',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_id_id',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_it_it',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_ja_jp',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_kk_kz',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_ko_kr',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_nl_nl',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_no_no',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_pl_pl',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_pt_br',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_ro_ro',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_ru_ru',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_sv_se',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_th_th',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_tr_tr',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_vi_vn',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='html_content_zh_hans',
field=models.TextField(help_text='email body content - supports {{ user.first_name }}, {{ user.email }}, {{ project_name }}, {{ unsubscribe_url }}', null=True, verbose_name='HTML content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_ar_ar',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_cs_cz',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_da_dk',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_de_de',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_en_gb',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_en_us',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_es_es',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_fa_ir',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_fr_fr',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_he_il',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_hi_in',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_hr_hr',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_id_id',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_it_it',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_ja_jp',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_kk_kz',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_ko_kr',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_nl_nl',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_no_no',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_pl_pl',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_pt_br',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_ro_ro',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_ru_ru',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_sv_se',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_th_th',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_tr_tr',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_vi_vn',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='plain_content_zh_hans',
field=models.TextField(blank=True, default='', help_text='plain text fallback (auto-generated if empty)', null=True, verbose_name='plain text content'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_ar_ar',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_cs_cz',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_da_dk',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_de_de',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_en_gb',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_en_us',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_es_es',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_fa_ir',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_fr_fr',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_he_il',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_hi_in',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_hr_hr',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_id_id',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_it_it',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_ja_jp',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_kk_kz',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_ko_kr',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_nl_nl',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_no_no',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_pl_pl',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_pt_br',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_ro_ro',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_ru_ru',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_sv_se',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_th_th',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_tr_tr',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_vi_vn',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
migrations.AddField(
model_name='emailtemplate',
name='subject_zh_hans',
field=models.CharField(help_text='email subject line - supports {{ variables }}', max_length=255, null=True, verbose_name='subject'),
),
]

View file

@ -0,0 +1,9 @@
from modeltranslation.decorators import register
from modeltranslation.translator import TranslationOptions
from engine.vibes_auth.emailing import EmailTemplate
@register(EmailTemplate)
class EmailTemplateOptions(TranslationOptions):
fields = ("subject", "html_content", "plain_content")

View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
source ./scripts/Unix/starter.sh
# Detect environment: Docker or native
use_docker=false
use_native=false
if [ -f .env ]; then
use_docker=true
fi
if [ -d .venv ]; then
use_native=true
fi
if [ "$use_docker" = false ] && [ "$use_native" = false ]; then
log_error "Neither .env (Docker) nor .venv (native) found. Please set up your environment first."
exit 1
fi
log_step "Generating migration files..."
if [ "$use_docker" = true ]; then
if ! docker compose exec app uv run manage.py makemigrations; then
log_error "Failed to generate migration files"
exit 1
fi
elif [ "$use_native" = true ]; then
if ! .venv/bin/python manage.py makemigrations; then
log_error "Failed to generate migration files"
exit 1
fi
fi
log_success "Migration files created successfully!"
echo
log_result "You can now use migrate.sh script or run: make migrate"

40
scripts/Unix/migrate.sh Normal file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
source ./scripts/Unix/starter.sh
# Detect environment: Docker or native
use_docker=false
use_native=false
if [ -f .env ]; then
use_docker=true
fi
if [ -d .venv ]; then
use_native=true
fi
if [ "$use_docker" = false ] && [ "$use_native" = false ]; then
log_error "Neither .env (Docker) nor .venv (native) found. Please set up your environment first."
exit 1
fi
log_step "Applying migration files..."
if [ "$use_docker" = true ]; then
if ! docker compose exec app uv run manage.py migrate; then
log_error "Failed to apply migration files"
exit 1
fi
elif [ "$use_native" = true ]; then
if ! .venv/bin/python manage.py migrate; then
log_error "Failed to apply migration files"
exit 1
fi
fi
log_success "Migration files applied successfully!"
echo
log_result "Database is now up to date"

View file

@ -0,0 +1,36 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Load shared utilities
$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1'
. $utilsPath
$starterPath = Join-Path $PSScriptRoot 'starter.ps1'
. $starterPath
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$useDocker = Test-Path '.env'
$useNative = Test-Path '.venv'
if (-not $useDocker -and -not $useNative) {
Write-Warning-Custom "Neither .env (Docker) nor .venv (native) found. Please set up your environment first."
exit 1
}
Write-Step "Generating migration files..."
if ($useDocker) {
docker compose exec app uv run manage.py makemigrations
} elseif ($useNative) {
& .\.venv\Scripts\python.exe manage.py makemigrations
}
if ($LASTEXITCODE -ne 0) {
Write-Error-Custom "Failed to generate migration files"
exit $LASTEXITCODE
}
Write-Success "Migration files created successfully!"
Write-Result "You can now use migrate.ps1 script or run: make migrate"

View file

@ -0,0 +1,36 @@
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Load shared utilities
$utilsPath = Join-Path $PSScriptRoot '..\lib\utils.ps1'
. $utilsPath
$starterPath = Join-Path $PSScriptRoot 'starter.ps1'
. $starterPath
if ($LASTEXITCODE -ne 0) {
exit $LASTEXITCODE
}
$useDocker = Test-Path '.env'
$useNative = Test-Path '.venv'
if (-not $useDocker -and -not $useNative) {
Write-Warning-Custom "Neither .env (Docker) nor .venv (native) found. Please set up your environment first."
exit 1
}
Write-Step "Applying migration files..."
if ($useDocker) {
docker compose exec app uv run manage.py migrate
} elseif ($useNative) {
& .\.venv\Scripts\python.exe manage.py migrate
}
if ($LASTEXITCODE -ne 0) {
Write-Error-Custom "Failed to apply migration files"
exit $LASTEXITCODE
}
Write-Success "Migration files applied successfully!"
Write-Result "Database is now up to date"