From b5e303e7a5d1dd5c8353a3a6083c38b33f994d34 Mon Sep 17 00:00:00 2001 From: Egor fureunoir Gorbunov Date: Mon, 26 Jan 2026 15:35:30 +0300 Subject: [PATCH] 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. --- Makefile | 12 +- ...ntent_alter_post_content_ar_ar_and_more.py | 158 +++++++ ...emplate_user_unsubscribe_token_and_more.py | 111 +++++ ...ailtemplate_html_content_ar_ar_and_more.py | 433 ++++++++++++++++++ engine/vibes_auth/translation.py | 9 + scripts/Unix/make-migrations.sh | 40 ++ scripts/Unix/migrate.sh | 40 ++ scripts/Windows/make-migrations.ps1 | 36 ++ scripts/Windows/migrate.ps1 | 36 ++ 9 files changed, 874 insertions(+), 1 deletion(-) create mode 100644 engine/blog/migrations/0008_alter_post_content_alter_post_content_ar_ar_and_more.py create mode 100644 engine/vibes_auth/migrations/0007_emailimage_emailtemplate_user_unsubscribe_token_and_more.py create mode 100644 engine/vibes_auth/migrations/0008_emailtemplate_html_content_ar_ar_and_more.py create mode 100644 engine/vibes_auth/translation.py create mode 100644 scripts/Unix/make-migrations.sh create mode 100644 scripts/Unix/migrate.sh create mode 100644 scripts/Windows/make-migrations.ps1 create mode 100644 scripts/Windows/migrate.ps1 diff --git a/Makefile b/Makefile index cd5607d1..8287a553 100644 --- a/Makefile +++ b/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 diff --git a/engine/blog/migrations/0008_alter_post_content_alter_post_content_ar_ar_and_more.py b/engine/blog/migrations/0008_alter_post_content_alter_post_content_ar_ar_and_more.py new file mode 100644 index 00000000..7c51490d --- /dev/null +++ b/engine/blog/migrations/0008_alter_post_content_alter_post_content_ar_ar_and_more.py @@ -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'), + ), + ] diff --git a/engine/vibes_auth/migrations/0007_emailimage_emailtemplate_user_unsubscribe_token_and_more.py b/engine/vibes_auth/migrations/0007_emailimage_emailtemplate_user_unsubscribe_token_and_more.py new file mode 100644 index 00000000..32666092 --- /dev/null +++ b/engine/vibes_auth/migrations/0007_emailimage_emailtemplate_user_unsubscribe_token_and_more.py @@ -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'), + ), + ] diff --git a/engine/vibes_auth/migrations/0008_emailtemplate_html_content_ar_ar_and_more.py b/engine/vibes_auth/migrations/0008_emailtemplate_html_content_ar_ar_and_more.py new file mode 100644 index 00000000..f4cce6fd --- /dev/null +++ b/engine/vibes_auth/migrations/0008_emailtemplate_html_content_ar_ar_and_more.py @@ -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'), + ), + ] diff --git a/engine/vibes_auth/translation.py b/engine/vibes_auth/translation.py new file mode 100644 index 00000000..04626ad0 --- /dev/null +++ b/engine/vibes_auth/translation.py @@ -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") diff --git a/scripts/Unix/make-migrations.sh b/scripts/Unix/make-migrations.sh new file mode 100644 index 00000000..b015f9f7 --- /dev/null +++ b/scripts/Unix/make-migrations.sh @@ -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" diff --git a/scripts/Unix/migrate.sh b/scripts/Unix/migrate.sh new file mode 100644 index 00000000..f69a8689 --- /dev/null +++ b/scripts/Unix/migrate.sh @@ -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" diff --git a/scripts/Windows/make-migrations.ps1 b/scripts/Windows/make-migrations.ps1 new file mode 100644 index 00000000..ff6aa2ab --- /dev/null +++ b/scripts/Windows/make-migrations.ps1 @@ -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" diff --git a/scripts/Windows/migrate.ps1 b/scripts/Windows/migrate.ps1 new file mode 100644 index 00000000..eb84e3a5 --- /dev/null +++ b/scripts/Windows/migrate.ps1 @@ -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"