diff --git a/docs/dialer.rst b/docs/dialer.rst index 0c9f865..e48776f 100644 --- a/docs/dialer.rst +++ b/docs/dialer.rst @@ -9,7 +9,7 @@ Dialer messages that are stored inside Django model class defined later are sent Function has two required parameters ``recipient`` which is a phone number of the receiver and ``content``. Attribute ``content`` is a text message that will be read via 'text to speech' mechanism to the recipient. Attribute ``related_objects`` should contain a list of objects that you want to connect with the sent message (with generic relation). ``tag`` is string mark which is stored with the sent message. The last non required parameter ``**kwargs`` is extra data that will be stored inside dialer message model in field ``extra_data``. -.. function:: pymess.backend.dialer.send_template(recipient, slug, context_data, related_objects=None, tag=None, send_immediately=False) +.. function:: pymess.backend.dialer.send_template(recipient, slug, context_data, locale=None, related_objects=None, tag=None, send_immediately=False) The second function is used for sending prepared templates that are stored inside template model (class that extends ``pymess.models.dialer.AbstractDialerTemplate``). The first parameter ``recipient`` is phone number of the receiver, ``slug`` is key of the template, ``context_data`` is a dictionary that contains context data for rendering dialer message content from the template, ``related_objects`` should contains list of objects that you want to connect with the sent message and ``tag`` is string mark which is stored with the sent message. diff --git a/docs/emails.rst b/docs/emails.rst index 711fdd5..2c6736c 100644 --- a/docs/emails.rst +++ b/docs/emails.rst @@ -9,7 +9,7 @@ Like SMS E-mail messages are stored inside Django model class and sent via backe Parameter ``sender`` define source e-mail address of the message, you can specify the name of the sender and pre header with optional parameter ``sender_name`` and ``pre_header``. ``recipient`` is destination e-mail address. Subject and HTML content of the e-mail message is defined with ``subject`` and ``content`` parameters. Attribute ``related_objects`` should contain a list of objects that you want to connect with the send message (with generic relation). Optional parameter ``attachments`` should contains list of files that will be sent with the e-mail in format ``({file name}, {output stream with file content}, {content type})``. ``tag`` is string mark which is stored with the sent SMS message . The last non required parameter ``**email_kwargs`` is extra data that will be stored inside e-mail message model in field ``extra_data``. -.. function:: pymess.backend.emails.send_template(recipient, slug, context_data, related_objects=None, attachments=None, tag=None, send_immediately=False) +.. function:: pymess.backend.emails.send_template(recipient, slug, context_data, locale=None, related_objects=None, attachments=None, tag=None, send_immediately=False) The second function is used for sending prepared templates that are stored inside template model (class that extends ``pymess.models.sms.AbstractEmailTemplate``). The first parameter ``recipient`` is e-mail address of the receiver, ``slug`` is key of the template, ``context_data`` is a dictionary that contains context data for rendering e-mail content from the template, ``related_objects`` should contains list of objects that you want to connect with the send message, ``attachments`` should contains list of files that will be send with the e-mail and ``tag`` is string mark which is stored with the sent SMS message. diff --git a/docs/push.rst b/docs/push.rst index 795e215..a5c47e6 100644 --- a/docs/push.rst +++ b/docs/push.rst @@ -9,7 +9,7 @@ PUSH notifications are stored inside Django model class defined later, are sent Function has two required parameters ``recipient`` which is an identifier of the receiver and ``content``. Attribute ``content`` is a text message that will be sent inside the push notification. Attribute ``related_objects`` should contain a list of objects that you want to connect with the sent message (with generic relation). ``tag`` is string mark which is stored with the sent message . The last non required parameter ``**push_nofification_kwargs`` is extra data that will be stored inside push notification model in field ``extra_data``. -.. function:: pymess.backend.push.send_template(recipient, slug, context_data, related_objects=None, tag=None, send_immediately=False) +.. function:: pymess.backend.push.send_template(recipient, slug, context_data, locale=None, related_objects=None, tag=None, send_immediately=False) The second function is used for sending prepared templates that are stored inside template model (class that extends ``pymess.models.push.AbstractPushNotificationTemplate``). The first parameter ``recipient`` is identifier of the receiver, ``slug`` is key of the template, ``context_data`` is a dictionary that contains context data for rendering push notification content from the template, ``related_objects`` should contains list of objects that you want to connect with the sent message and ``tag`` is string mark which is stored with the sent push notification message. @@ -51,7 +51,7 @@ Models .. attribute:: template_slug - If push was sent from the template, this attribute cointains key of the template. + If push was sent from the template, this attribute contains key of the template. .. attribute:: template @@ -69,11 +69,11 @@ Models .. attribute:: backend - Field contains path to the push backend that was used for sending of the push notifiaction. + Field contains path to the push backend that was used for sending of the push notification. .. attribute:: error - If error was raised during sending of the push notifiaction this field contains text description of the error. + If error was raised during sending of the push notification this field contains text description of the error. .. attribute:: extra_data diff --git a/docs/sms.rst b/docs/sms.rst index 6ab7588..b23e8a4 100644 --- a/docs/sms.rst +++ b/docs/sms.rst @@ -3,13 +3,13 @@ SMS === -SMS messages that are stored inside Django model class defined later, are sent via SMS backend. There are implemented several SMS backends, every backed uses differend SMS service like twillio or AWS SNS. For sending SMS message you can use function ``pymess.backend.sms.send`` or ``pymwess.backend.sms.send_template``. +SMS messages that are stored inside Django model class defined later, are sent via SMS backend. There are implemented several SMS backends, every backed uses different SMS service like twillio or AWS SNS. For sending SMS message you can use function ``pymess.backend.sms.send`` or ``pymwess.backend.sms.send_template``. .. function:: pymess.backend.sms.send(recipient, content, related_objects=None, tag=None, send_immediately=False, **kwargs) Function has two required parameters ``recipient`` which is a phone number of the receiver and ``content``. Attribute ``content`` is a text message that will be sent inside the SMS body. If setting ``PYMESS_SMS_USE_ACCENT`` is set to ``False``, accent in the content will be replaced by appropriate ascii characters. Attribute ``related_objects`` should contain a list of objects that you want to connect with the sent message (with generic relation). ``tag`` is string mark which is stored with the sent SMS message . The last non required parameter ``**sms_kwargs`` is extra data that will be stored inside SMS message model in field ``extra_data``. -.. function:: pymess.backend.sms.send_template(recipient, slug, context_data, related_objects=None, tag=None, send_immediately=False) +.. function:: pymess.backend.sms.send_template(recipient, slug, context_data, locale=None, related_objects=None, tag=None, send_immediately=False) The second function is used for sending prepared templates that are stored inside template model (class that extends ``pymess.models.sms.AbstractSMSTemplate``). The first parameter ``recipient`` is phone number of the receiver, ``slug`` is key of the template, ``context_data`` is a dictionary that contains context data for rendering SMS content from the template, ``related_objects`` should contains list of objects that you want to connect with the sent message and ``tag`` is string mark which is stored with the sent SMS message. @@ -47,7 +47,7 @@ Models .. attribute:: template_slug - If SMS was sent from the template, this attribute cointains key of the template. + If SMS was sent from the template, this attribute contains key of the template. .. attribute:: template diff --git a/pymess/backend/__init__.py b/pymess/backend/__init__.py index b89155c..cd1524d 100644 --- a/pymess/backend/__init__.py +++ b/pymess/backend/__init__.py @@ -268,7 +268,7 @@ def get_retry_sending(self): raise NotImplementedError -def send_template(recipient, slug, context_data, related_objects=None, tag=None, template_model=None, **kwargs): +def send_template(recipient, slug, context_data, locale=None, related_objects=None, tag=None, template_model=None, **kwargs): """ Helper for building and sending message from a template. :param recipient: email or phone number of the recipient @@ -284,7 +284,7 @@ def send_template(recipient, slug, context_data, related_objects=None, tag=None, assert template_model is not None, _l('template_model cannot be None') - return template_model.objects.get(slug=slug).send( + return template_model.objects.get(slug=slug, locale=locale).send( recipient, context_data, related_objects=related_objects, diff --git a/pymess/backend/dialer/__init__.py b/pymess/backend/dialer/__init__.py index ae4dc8a..2364997 100644 --- a/pymess/backend/dialer/__init__.py +++ b/pymess/backend/dialer/__init__.py @@ -107,7 +107,7 @@ def _update_dialer_states(self, messages): raise NotImplementedError('Check dialer state is not supported with the backend') -def send_template(recipient, slug, context_data, related_objects=None, tag=None, send_immediately=False): +def send_template(recipient, slug, context_data, locale=None, related_objects=None, tag=None, send_immediately=False): """ Helper for building and sending dialer message from a template. :param recipient: phone number of the recipient @@ -120,11 +120,12 @@ def send_template(recipient, slug, context_data, related_objects=None, tag=None, :return: dialer message object or None if template cannot be sent """ return _send_template( - recipient, - slug, - context_data, - related_objects, - tag, + recipient=recipient, + slug=slug, + context_data=context_data, + locale=locale, + related_objects=related_objects, + tag=tag, template_model=get_dialer_template_model(), send_immediately=send_immediately ) diff --git a/pymess/backend/emails/__init__.py b/pymess/backend/emails/__init__.py index 1286d21..45eff26 100644 --- a/pymess/backend/emails/__init__.py +++ b/pymess/backend/emails/__init__.py @@ -93,8 +93,8 @@ def pull_message_info(self, message): raise NotImplementedError -def send_template(recipient, slug, context_data, related_objects=None, attachments=None, tag=None, - send_immediately=False): +def send_template(recipient, slug, context_data, locale=None, related_objects=None, attachments=None, + tag=None, send_immediately=False): """ Helper for building and sending e-mail message from a template. :param recipient: e-mail address of the receiver @@ -111,6 +111,7 @@ def send_template(recipient, slug, context_data, related_objects=None, attachmen recipient=recipient, slug=slug, context_data=context_data, + locale=locale, related_objects=related_objects, tag=tag, template_model=get_email_template_model(), diff --git a/pymess/backend/push/__init__.py b/pymess/backend/push/__init__.py index 2049ef9..bea4c37 100644 --- a/pymess/backend/push/__init__.py +++ b/pymess/backend/push/__init__.py @@ -51,11 +51,11 @@ def get_retry_sending(self): return settings.PUSH_NOTIFICATION_RETRY_SENDING and is_turned_on_push_notification_batch_sending() -def send_template(recipient, slug, context_data, related_objects=None, tag=None, send_immediately=None): +def send_template(recipient, slug, context_data, locale=None, related_objects=None, tag=None, send_immediately=None): """ Helper for building and sending push notification message from a template. :param recipient: push notification recipient - :param slug: slug of a push notifiaction template + :param slug: slug of a push notification template :param context_data: dict of data that will be sent to the template renderer :param related_objects: list of related objects that will be linked with the push notification using generic relation @@ -67,6 +67,7 @@ def send_template(recipient, slug, context_data, related_objects=None, tag=None, recipient=recipient, slug=slug, context_data=context_data, + locale=locale, related_objects=related_objects, tag=tag, template_model=get_push_notification_template_model(), diff --git a/pymess/backend/sms/__init__.py b/pymess/backend/sms/__init__.py index 5766567..55b0bc4 100644 --- a/pymess/backend/sms/__init__.py +++ b/pymess/backend/sms/__init__.py @@ -115,7 +115,7 @@ def get_retry_sending(self): return settings.SMS_RETRY_SENDING and is_turned_on_sms_batch_sending() -def send_template(recipient, slug, context_data, related_objects=None, tag=None, send_immediately=False): +def send_template(recipient, slug, context_data, locale=None, related_objects=None, tag=None, send_immediately=False): """ Helper for building and sending SMS message from a template. :param recipient: phone number of the recipient @@ -130,6 +130,7 @@ def send_template(recipient, slug, context_data, related_objects=None, tag=None, return _send_template( recipient=recipient, slug=slug, + locale=locale, context_data=context_data, related_objects=related_objects, tag=tag, diff --git a/pymess/management/commands/sync_emails.py b/pymess/management/commands/sync_emails.py index a058d3c..910bba7 100644 --- a/pymess/management/commands/sync_emails.py +++ b/pymess/management/commands/sync_emails.py @@ -13,19 +13,16 @@ class Command(BaseCommand): Every e-mail body is named with e-mail slug and stored as a HTML file. """ - def handle(self, *args, **options): - verbosity = int(options.get('verbosity')) - if verbosity > 0: - self.stdout.write('Syncing e-mails') + def _sync_for_locale(self, base_directory, locale): + locale_directory = os.path.join(base_directory, locale) if locale else base_directory - directory = os.path.join(settings.EMAIL_HTML_DATA_DIRECTORY) files_with_email_html_body = [ - filename for filename in os.listdir(directory) if filename.endswith('.html') + filename for filename in os.listdir(locale_directory) if filename.endswith('.html') ] for filename in files_with_email_html_body: try: template_obj = get_email_template_model().objects.get(slug=filename[:-5]) - with codecs.open(os.path.join(directory, filename), 'r', encoding='utf-8-sig') as file: + with codecs.open(os.path.join(locale_directory, filename), 'r', encoding='utf-8-sig') as file: template_obj.change_and_save(body=file.read()) except ObjectDoesNotExist: if verbosity > 0: @@ -34,3 +31,19 @@ def handle(self, *args, **options): self.stdout.write( self.style.SUCCESS('Synced "{}" e-mail templates'.format(len(files_with_email_html_body))) ) + + def handle(self, *args, **options): + verbosity = int(options.get('verbosity')) + if verbosity > 0: + self.stdout.write('Syncing e-mails') + + directory = os.path.join(settings.EMAIL_HTML_DATA_DIRECTORY) + # All dirs in the root directory represent a locale. + # e.g., `$EMAIL_HTML_DATA_DIRECTORY/cs/`, `$EMAIL_HTML_DATA_DIRECTORY/sk/`. + for filename in os.listdir(directory): + file_path = os.path.join(directory, filename) + if os.path.isdir(file_path): + self._sync_for_locale(directory, filename) + + # The `$EMAIL_HTML_DATA_DIRECTORY/` directory itself represents the locale of None value. + self._sync_for_locale(directory, None) diff --git a/pymess/migrations/0030_migration.py b/pymess/migrations/0030_migration.py new file mode 100644 index 0000000..f5d5c96 --- /dev/null +++ b/pymess/migrations/0030_migration.py @@ -0,0 +1,316 @@ +# Generated by Django 4.2.13 on 2024-07-03 18:01 + +import django.db.models.deletion +from django.db import migrations, models +from django.utils.timezone import localtime + +from dateutil.relativedelta import relativedelta + +from utils.migrations import migration_print + + +def create_template_ids_from_slugs(apps, schema_editor): + DialerMessage = apps.get_model("pymess", "DialerMessage") + DialerTemplateDisallowedObject = apps.get_model("pymess", "DialerTemplateDisallowedObject") + DialerTemplate = apps.get_model("pymess", "DialerTemplate") + + EmailTemplate = apps.get_model("pymess", "EmailTemplate") + EmailMessage = apps.get_model("pymess", "EmailMessage") + EmailTemplateAttachment = apps.get_model("pymess", "EmailTemplateAttachment") + EmailTemplateDisallowedObject = apps.get_model("pymess", "EmailTemplateDisallowedObject") + + SMSTemplate = apps.get_model("pymess", "SMSTemplate") + OutputSMSMessage = apps.get_model("pymess", "OutputSMSMessage") + + PushNotificationTemplate = apps.get_model("pymess", "PushNotificationTemplate") + PushNotificationMessage = apps.get_model("pymess", "PushNotificationMessage") + SMSTemplateDisallowedObject = apps.get_model("pymess", "SMSTemplateDisallowedObject") + + def migrate_back_template(template, relation, cutoff=False): + for obj in template.objects.all(): + query = relation.objects.filter(template_old=obj.slug) + if cutoff: + query = query.exclude(created_at__lt=localtime()-relativedelta(years=1)) + migration_print(f"Migrating {relation}.template={obj.id} from slug={obj.slug} ({query.count()} objects) | cutoff={cutoff}") + query.update(template=obj.id) + + migrate_back_template(DialerTemplate, DialerMessage, cutoff=True) + migrate_back_template(DialerTemplate, DialerTemplateDisallowedObject) + + migrate_back_template(EmailTemplate, EmailMessage, cutoff=True) + migrate_back_template(EmailTemplate, EmailTemplateAttachment) + migrate_back_template(EmailTemplate, EmailTemplateDisallowedObject) + + migrate_back_template(SMSTemplate, OutputSMSMessage, cutoff=True) + + migrate_back_template(PushNotificationTemplate, PushNotificationMessage, cutoff=True) + migrate_back_template(PushNotificationTemplate, SMSTemplateDisallowedObject) + + +class Migration(migrations.Migration): + + dependencies = [ + ('pymess', '0029_migration'), + ] + + operations = [ + # 1. Drop the foreign key constraint. + + migrations.AlterField( + model_name='dialermessage', + name='template', + field=models.SlugField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='dialertemplatedisallowedobject', + name='template', + field=models.SlugField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='emailmessage', + name='template', + field=models.SlugField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='emailtemplateattachment', + name='template', + field=models.SlugField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='emailtemplatedisallowedobject', + name='template', + field=models.SlugField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='outputsmsmessage', + name='template', + field=models.SlugField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='pushnotificationmessage', + name='template', + field=models.SlugField(blank=True, max_length=100, null=True), + ), + migrations.AlterField( + model_name='smstemplatedisallowedobject', + name='template', + field=models.SlugField(blank=True, max_length=100, null=True), + ), + + # 2. Rename 'template' -> 'template_old' + + migrations.RenameField( + model_name='dialermessage', + old_name='template', + new_name='template_old', + ), + migrations.RenameField( + model_name='dialertemplatedisallowedobject', + old_name='template', + new_name='template_old', + ), + migrations.RenameField( + model_name='emailmessage', + old_name='template', + new_name='template_old', + ), + migrations.RenameField( + model_name='emailtemplateattachment', + old_name='template', + new_name='template_old', + ), + migrations.RenameField( + model_name='emailtemplatedisallowedobject', + old_name='template', + new_name='template_old', + ), + migrations.RenameField( + model_name='outputsmsmessage', + old_name='template', + new_name='template_old', + ), + migrations.RenameField( + model_name='pushnotificationmessage', + old_name='template', + new_name='template_old', + ), + migrations.RenameField( + model_name='smstemplatedisallowedobject', + old_name='template', + new_name='template_old', + ), + + # 3. Slug is no longer a primary key. + + migrations.AlterField( + model_name='dialertemplate', + name='slug', + field=models.SlugField(editable=False, max_length=100), + ), + migrations.AlterField( + model_name='emailtemplate', + name='slug', + field=models.SlugField(editable=False, max_length=100), + ), + migrations.AlterField( + model_name='pushnotificationtemplate', + name='slug', + field=models.SlugField(editable=False, max_length=100), + ), + migrations.AlterField( + model_name='smstemplate', + name='slug', + field=models.SlugField(editable=False, max_length=100), + ), + + # 4. We create a new primary key -> AutoField + + migrations.AddField( + model_name='dialertemplate', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False), + preserve_default=False, + ), + migrations.AddField( + model_name='emailtemplate', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False), + preserve_default=False, + ), + migrations.AddField( + model_name='pushnotificationtemplate', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False), + preserve_default=False, + ), + migrations.AddField( + model_name='smstemplate', + name='id', + field=models.AutoField(auto_created=True, primary_key=True, serialize=False), + preserve_default=False, + ), + + # 5. We create a new locale field + + migrations.AddField( + model_name='dialertemplate', + name='locale', + field=models.CharField(blank=True, editable=False, max_length=10, null=True), + ), + migrations.AddField( + model_name='emailtemplate', + name='locale', + field=models.CharField(blank=True, editable=False, max_length=10, null=True), + ), + migrations.AddField( + model_name='pushnotificationtemplate', + name='locale', + field=models.CharField(blank=True, editable=False, max_length=10, null=True), + ), + migrations.AddField( + model_name='smstemplate', + name='locale', + field=models.CharField(blank=True, editable=False, max_length=10, null=True), + ), + + # 5. slug + locale are unique. + + migrations.AlterUniqueTogether( + name='dialertemplate', + unique_together={('slug', 'locale')}, + ), + migrations.AlterUniqueTogether( + name='emailtemplate', + unique_together={('slug', 'locale')}, + ), + migrations.AlterUniqueTogether( + name='pushnotificationtemplate', + unique_together={('slug', 'locale')}, + ), + migrations.AlterUniqueTogether( + name='smstemplate', + unique_together={('slug', 'locale')}, + ), + + # 6. We need to create removed `template` fields and return the foreign key constraint. + + migrations.AddField( + model_name='dialermessage', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='dialer_messages', to='pymess.dialertemplate'), + ), + migrations.AddField( + model_name='dialertemplatedisallowedobject', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='disallowed_objects', to='pymess.dialertemplate'), + ), + migrations.AddField( + model_name='emailmessage', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='email_messages', to='pymess.emailtemplate'), + ), + migrations.AddField( + model_name='emailtemplateattachment', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='template_attachments', to='pymess.emailtemplate'), + ), + migrations.AddField( + model_name='emailtemplatedisallowedobject', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='disallowed_objects', to='pymess.emailtemplate'), + ), + migrations.AddField( + model_name='outputsmsmessage', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='output_sms_messages', to='pymess.smstemplate'), + ), + migrations.AddField( + model_name='pushnotificationmessage', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='push_notifications', to='pymess.pushnotificationtemplate'), + ), + migrations.AddField( + model_name='smstemplatedisallowedobject', + name='template', + field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='disallowed_objects', to='pymess.smstemplate'), + ), + + # 7. We need to update `template` field with newly created primary keys. + + migrations.RunPython(create_template_ids_from_slugs), + + # 8. We can safely remove old `template_old` field. + + migrations.RemoveField( + model_name='dialertemplatedisallowedobject', + name='template_old', + ), + migrations.RemoveField( + model_name='emailtemplateattachment', + name='template_old', + ), + migrations.RemoveField( + model_name='emailtemplatedisallowedobject', + name='template_old', + ), + migrations.RemoveField( + model_name='smstemplatedisallowedobject', + name='template_old', + ), + migrations.RemoveField( + model_name='dialermessage', + name='template_old', + ), + migrations.RemoveField( + model_name='emailmessage', + name='template_old', + ), + migrations.RemoveField( + model_name='outputsmsmessage', + name='template_old', + ), + migrations.RemoveField( + model_name='pushnotificationmessage', + name='template_old', + ), + ] diff --git a/pymess/models/common.py b/pymess/models/common.py index 6ba5580..b66f237 100644 --- a/pymess/models/common.py +++ b/pymess/models/common.py @@ -160,8 +160,8 @@ def __str__(self): class BaseAbstractTemplate(SmartModel): - slug = models.SlugField(verbose_name=_('slug'), max_length=100, null=False, blank=False, editable=False, - primary_key=True) + slug = models.SlugField(verbose_name=_('slug'), max_length=100, null=False, blank=False, editable=False) + locale = models.CharField(verbose_name=_('locale'), max_length=10, null=True, blank=True, editable=False) body = models.TextField(verbose_name=_('message body'), null=True, blank=False) is_active = models.BooleanField(null=False, blank=False, default=True, verbose_name=_('is active')) is_locked = models.BooleanField(null=False, blank=False, default=False, verbose_name=_('is locked')) @@ -232,8 +232,9 @@ def send(self, recipient, context_data, related_objects=None, tag=None, **kwargs return None def __str__(self): - return self.slug + return f"{self.slug} ({self.locale})" if self.locale else self.slug class Meta: abstract = True + unique_together = ("slug", "locale") ordering = ('-created_at',) diff --git a/pymess/models/emails.py b/pymess/models/emails.py index e933e72..92f82dc 100644 --- a/pymess/models/emails.py +++ b/pymess/models/emails.py @@ -277,6 +277,7 @@ class Meta(BaseRelatedObject.Meta): class EmailTemplateAttachment(SmartModel): template = models.ForeignKey(to=settings.EMAIL_TEMPLATE_MODEL, verbose_name=_('template'), + null=True, blank=True, on_delete=models.CASCADE, related_name='template_attachments') content_type = models.CharField(verbose_name=_('content type'), blank=False, null=False, max_length=100) file = models.FileField(verbose_name=_('file'), null=False, blank=False, diff --git a/pymess/utils/migrations.py b/pymess/utils/migrations.py index a6fceef..117cfb1 100644 --- a/pymess/utils/migrations.py +++ b/pymess/utils/migrations.py @@ -1,32 +1,43 @@ import codecs import os +from typing import Any from chamber.shortcuts import change_and_save from pymess.config import settings +__all__ = [ + "change_and_save", + "get_email_template_body_from_file", + "SyncEmailTemplates", +] -def get_email_template_body_from_file(email_template_slug): + +def get_email_template_body_from_file(email_template_slug, locale): + base_dir = settings.EMAIL_HTML_DATA_DIRECTORY + if locale: + base_dir = os.path.join(settings.EMAIL_HTML_DATA_DIRECTORY, locale) with codecs.open(os.path.join( - os.path.join(settings.EMAIL_HTML_DATA_DIRECTORY), + base_dir, '{}.html'.format(email_template_slug)), 'r', encoding='utf-8-sig') as file: return file.read() class SyncEmailTemplates: - def __init__(self, template_slugs=None): + def __init__(self, template_slugs=None, locale=None): self.template_slugs = template_slugs + self.locale = locale def __call__(self, apps, schema_editor): email_template_class = apps.get_model(*settings.EMAIL_TEMPLATE_MODEL.split('.')) email_template_qs = email_template_class.objects.all() if self.template_slugs: - email_template_qs = email_template_qs.filter(slug__in=self.template_slugs) + email_template_qs = email_template_qs.filter(slug__in=self.template_slugs, locale=self.locale) email_templates_found = list(email_template_qs) if self.template_slugs: email_templates_not_found = set(self.template_slugs) - {t.slug for t in email_templates_found} assert not email_templates_not_found, f"Email templates {email_templates_not_found} were not found." for email_template in email_templates_found: - email_template.body = get_email_template_body_from_file(email_template.slug) + email_template.body = get_email_template_body_from_file(email_template.slug, self.locale) email_template_class.objects.bulk_update(email_templates_found, ['body'])