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:
parent
9cab9fdd3a
commit
b5e303e7a5
9 changed files with 874 additions and 1 deletions
12
Makefile
12
Makefile
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
|
|
@ -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'),
|
||||
),
|
||||
]
|
||||
9
engine/vibes_auth/translation.py
Normal file
9
engine/vibes_auth/translation.py
Normal 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")
|
||||
40
scripts/Unix/make-migrations.sh
Normal file
40
scripts/Unix/make-migrations.sh
Normal 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
40
scripts/Unix/migrate.sh
Normal 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"
|
||||
36
scripts/Windows/make-migrations.ps1
Normal file
36
scripts/Windows/make-migrations.ps1
Normal 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"
|
||||
36
scripts/Windows/migrate.ps1
Normal file
36
scripts/Windows/migrate.ps1
Normal 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"
|
||||
Loading…
Reference in a new issue