diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5390963f..38a27560 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,7 +7,16 @@ on: permissions: contents: read -jobs: +jobs: + ruff-format: + runs-on: ubuntu-latest + timeout-minutes: 1 + steps: + - uses: actions/checkout@v3 + - uses: chartboost/ruff-action@v1 + with: + version: 0.4.8 + args: 'format --check' build: runs-on: ubuntu-latest diff --git a/post_office/__init__.py b/post_office/__init__.py index c131d547..b7343d6d 100644 --- a/post_office/__init__.py +++ b/post_office/__init__.py @@ -1,7 +1,7 @@ from ast import literal_eval from os.path import dirname, join -with open(join(dirname(__file__), "version.txt")) as fh: +with open(join(dirname(__file__), 'version.txt')) as fh: VERSION = literal_eval(fh.read()) from .backends import EmailBackend diff --git a/post_office/admin.py b/post_office/admin.py index 29ed9fa4..41dea32f 100644 --- a/post_office/admin.py +++ b/post_office/admin.py @@ -8,8 +8,7 @@ from django.db import models from django.forms import BaseInlineFormSet from django.forms.widgets import TextInput -from django.http.response import (HttpResponse, HttpResponseNotFound, - HttpResponseRedirect) +from django.http.response import HttpResponse, HttpResponseNotFound, HttpResponseRedirect from django.template import Context, Template from django.urls import re_path, reverse from django.utils.html import format_html @@ -22,8 +21,7 @@ def get_message_preview(instance): - return (f'{instance.message[:25]}...' if len(instance.message) > 25 - else instance.message) + return f'{instance.message[:25]}...' if len(instance.message) > 25 else instance.message get_message_preview.short_description = 'Message' @@ -32,7 +30,7 @@ def get_message_preview(instance): class AttachmentInline(admin.StackedInline): model = Attachment.emails.through extra = 0 - autocomplete_fields = ["attachment"] + autocomplete_fields = ['attachment'] def get_formset(self, request, obj=None, **kwargs): self.parent_obj = obj @@ -51,7 +49,7 @@ def get_queryset(self, request): a.id for a in queryset if isinstance(a.attachment.headers, dict) - and a.attachment.headers.get("Content-Disposition", "").startswith("inline") + and a.attachment.headers.get('Content-Disposition', '').startswith('inline') ] return queryset.exclude(id__in=inlined_attachments) @@ -69,7 +67,6 @@ def has_change_permission(self, request, obj=None): class CommaSeparatedEmailWidget(TextInput): - def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.attrs.update({'class': 'vTextField'}) @@ -79,7 +76,7 @@ def format_value(self, value): if not value: return '' if isinstance(value, str): - value = [value, ] + value = [value] return ','.join([item for item in value]) @@ -92,19 +89,29 @@ def requeue(modeladmin, request, queryset): class EmailAdmin(admin.ModelAdmin): - list_display = ['truncated_message_id', 'to_display', 'shortened_subject', 'status', 'last_updated', 'scheduled_time', 'use_template'] + list_display = [ + 'truncated_message_id', + 'to_display', + 'shortened_subject', + 'status', + 'last_updated', + 'scheduled_time', + 'use_template', + ] search_fields = ['to', 'subject'] - readonly_fields = ['message_id', 'render_subject', 'render_plaintext_body', 'render_html_body'] + readonly_fields = ['message_id', 'render_subject', 'render_plaintext_body', 'render_html_body'] inlines = [AttachmentInline, LogInline] list_filter = ['status', 'template__language', 'template__name'] - formfield_overrides = { - CommaSeparatedEmailField: {'widget': CommaSeparatedEmailWidget} - } + formfield_overrides = {CommaSeparatedEmailField: {'widget': CommaSeparatedEmailWidget}} actions = [requeue] def get_urls(self): urls = [ - re_path(r'^(?P\d+)/image/(?P[0-9a-f]{32})$', self.fetch_email_image, name='post_office_email_image'), + re_path( + r'^(?P\d+)/image/(?P[0-9a-f]{32})$', + self.fetch_email_image, + name='post_office_email_image', + ), re_path(r'^(?P\d+)/resend/$', self.resend, name='resend'), ] urls.extend(super().get_urls()) @@ -121,9 +128,9 @@ def truncated_message_id(self, instance): return Truncator(instance.message_id[1:-1]).chars(10) return str(instance.id) - to_display.short_description = _("To") + to_display.short_description = _('To') to_display.admin_order_field = 'to' - truncated_message_id.short_description = "Message-ID" + truncated_message_id.short_description = 'Message-ID' def has_add_permission(self, request): return False @@ -141,13 +148,13 @@ def shortened_subject(self, instance): subject = instance.subject return Truncator(subject).chars(100) - shortened_subject.short_description = _("Subject") + shortened_subject.short_description = _('Subject') shortened_subject.admin_order_field = 'subject' def use_template(self, instance): return bool(instance.template_id) - use_template.short_description = _("Use Template") + use_template.short_description = _('Use Template') use_template.boolean = True def get_fieldsets(self, request, obj=None): @@ -166,17 +173,11 @@ def get_fieldsets(self, request, obj=None): has_html_content = True if has_html_content: - fieldsets.append( - (_("HTML Email"), {'fields': ['render_subject', 'render_html_body']}) - ) + fieldsets.append((_('HTML Email'), {'fields': ['render_subject', 'render_html_body']})) if has_plaintext_content: - fieldsets.append( - (_("Text Email"), {'classes': ['collapse'], 'fields': ['render_plaintext_body']}) - ) + fieldsets.append((_('Text Email'), {'classes': ['collapse'], 'fields': ['render_plaintext_body']})) elif has_plaintext_content: - fieldsets.append( - (_("Text Email"), {'fields': ['render_subject', 'render_plaintext_body']}) - ) + fieldsets.append((_('Text Email'), {'fields': ['render_subject', 'render_plaintext_body']})) return fieldsets @@ -184,14 +185,14 @@ def render_subject(self, instance): message = instance.email_message() return message.subject - render_subject.short_description = _("Subject") + render_subject.short_description = _('Subject') def render_plaintext_body(self, instance): for message in instance.email_message().message().walk(): if isinstance(message, SafeMIMEText) and message.get_content_type() == 'text/plain': return format_html('
{}
', message.get_payload()) - render_plaintext_body.short_description = _("Mail Body") + render_plaintext_body.short_description = _('Mail Body') def render_html_body(self, instance): pattern = re.compile('cid:([0-9a-f]{32})') @@ -202,7 +203,7 @@ def render_html_body(self, instance): payload = message.get_payload(decode=True).decode('utf-8') return clean_html(pattern.sub(url, payload)) - render_html_body.short_description = _("HTML Body") + render_html_body.short_description = _('HTML Body') def fetch_email_image(self, request, pk, content_id): instance = self.get_object(request, pk) @@ -214,7 +215,7 @@ def fetch_email_image(self, request, pk, content_id): def resend(self, request, pk): instance = self.get_object(request, pk) instance.dispatch() - messages.info(request, "Email has been sent again") + messages.info(request, 'Email has been sent again') return HttpResponseRedirect(reverse('admin:post_office_email_change', args=[instance.pk])) @@ -249,14 +250,13 @@ class EmailTemplateAdminForm(forms.ModelForm): language = forms.ChoiceField( choices=settings.LANGUAGES, required=False, - label=_("Language"), - help_text=_("Render template in alternative language"), + label=_('Language'), + help_text=_('Render template in alternative language'), ) class Meta: model = EmailTemplate - fields = ['name', 'description', 'subject', 'content', 'html_content', 'language', - 'default_template'] + fields = ['name', 'description', 'subject', 'content', 'html_content', 'language', 'default_template'] def __init__(self, *args, **kwargs): instance = kwargs.get('instance') @@ -270,10 +270,8 @@ class EmailTemplateInline(admin.StackedInline): formset = EmailTemplateAdminFormSet model = EmailTemplate extra = 0 - fields = ('language', 'subject', 'content', 'html_content',) - formfield_overrides = { - models.CharField: {'widget': SubjectField} - } + fields = ('language', 'subject', 'content', 'html_content') + formfield_overrides = {models.CharField: {'widget': SubjectField}} def get_max_num(self, request, obj=None, **kwargs): return len(settings.LANGUAGES) @@ -284,30 +282,26 @@ class EmailTemplateAdmin(admin.ModelAdmin): list_display = ('name', 'description_shortened', 'subject', 'languages_compact', 'created') search_fields = ('name', 'description', 'subject') fieldsets = [ - (None, { - 'fields': ('name', 'description'), - }), - (_("Default Content"), { - 'fields': ('subject', 'content', 'html_content'), - }), + (None, {'fields': ('name', 'description')}), + (_('Default Content'), {'fields': ('subject', 'content', 'html_content')}), ] inlines = (EmailTemplateInline,) if settings.USE_I18N else () - formfield_overrides = { - models.CharField: {'widget': SubjectField} - } + formfield_overrides = {models.CharField: {'widget': SubjectField}} def get_queryset(self, request): return self.model.objects.filter(default_template__isnull=True) def description_shortened(self, instance): return Truncator(instance.description.split('\n')[0]).chars(200) - description_shortened.short_description = _("Description") + + description_shortened.short_description = _('Description') description_shortened.admin_order_field = 'description' def languages_compact(self, instance): languages = [tt.language for tt in instance.translated_templates.order_by('language')] return ', '.join(languages) - languages_compact.short_description = _("Languages") + + languages_compact.short_description = _('Languages') def save_model(self, request, obj, form, change): obj.save() @@ -320,8 +314,8 @@ def save_model(self, request, obj, form, change): class AttachmentAdmin(admin.ModelAdmin): list_display = ['name', 'file'] filter_horizontal = ['emails'] - search_fields = ["name"] - autocomplete_fields = ["emails"] + search_fields = ['name'] + autocomplete_fields = ['emails'] admin.site.register(Email, EmailAdmin) diff --git a/post_office/apps.py b/post_office/apps.py index f4896a4c..a4daed5c 100644 --- a/post_office/apps.py +++ b/post_office/apps.py @@ -4,7 +4,7 @@ class PostOfficeConfig(AppConfig): name = 'post_office' - verbose_name = _("Post Office") + verbose_name = _('Post Office') default_auto_field = 'django.db.models.AutoField' def ready(self): diff --git a/post_office/backends.py b/post_office/backends.py index eef918b1..b93e49f9 100644 --- a/post_office/backends.py +++ b/post_office/backends.py @@ -4,8 +4,8 @@ from django.core.mail.backends.base import BaseEmailBackend from .settings import get_default_priority -class EmailBackend(BaseEmailBackend): +class EmailBackend(BaseEmailBackend): def open(self): pass @@ -33,14 +33,14 @@ def send_messages(self, email_messages): from_email = email_message.from_email headers = email_message.extra_headers if email_message.reply_to: - reply_to_header = ", ".join(str(v) for v in email_message.reply_to) - headers.setdefault("Reply-To", reply_to_header) - message = email_message.body # The plaintext message is called body + reply_to_header = ', '.join(str(v) for v in email_message.reply_to) + headers.setdefault('Reply-To', reply_to_header) + message = email_message.body # The plaintext message is called body html_body = '' # The default if no html body can be found if hasattr(email_message, 'alternatives') and len(email_message.alternatives) > 0: for alternative in email_message.alternatives: if alternative[1] == 'text/html': - html_body = alternative[0] + html_body = alternative[0] attachment_files = {} for attachment in email_message.attachments: @@ -53,11 +53,16 @@ def send_messages(self, email_messages): else: attachment_files[attachment[0]] = ContentFile(attachment[1]) - email = create(sender=from_email, - recipients=email_message.to, cc=email_message.cc, - bcc=email_message.bcc, subject=subject, - message=message, html_message=html_body, - headers=headers) + email = create( + sender=from_email, + recipients=email_message.to, + cc=email_message.cc, + bcc=email_message.bcc, + subject=subject, + message=message, + html_message=html_body, + headers=headers, + ) if attachment_files: attachments = create_attachments(attachment_files) diff --git a/post_office/connections.py b/post_office/connections.py index 43f1d7bd..435749ee 100644 --- a/post_office/connections.py +++ b/post_office/connections.py @@ -12,6 +12,7 @@ class ConnectionHandler: Ensures only one instance of each alias exists per thread. """ + def __init__(self): self._connections = local() diff --git a/post_office/fields.py b/post_office/fields.py index a8ad5ada..0bc7b97b 100644 --- a/post_office/fields.py +++ b/post_office/fields.py @@ -6,7 +6,7 @@ class CommaSeparatedEmailField(TextField): default_validators = [validate_comma_separated_emails] - description = _("Comma-separated emails") + description = _('Comma-separated emails') def __init__(self, *args, **kwargs): kwargs['blank'] = True @@ -52,6 +52,7 @@ def south_field_triple(self): Taken from smiley chris' easy_thumbnails """ from south.modelsinspector import introspector + field_class = 'django.db.models.fields.TextField' args, kwargs = introspector(self) return (field_class, args, kwargs) diff --git a/post_office/lockfile.py b/post_office/lockfile.py index d89352d1..ea4c4abc 100644 --- a/post_office/lockfile.py +++ b/post_office/lockfile.py @@ -31,17 +31,13 @@ class FileLocked(Exception): class FileLock: - def __init__(self, lock_filename, timeout=None, force=False): self.lock_filename = '%s.lock' % lock_filename self.timeout = timeout self.force = force self._pid = str(os.getpid()) # Store pid in a file in the same directory as desired lockname - self.pid_filename = os.path.join( - os.path.dirname(self.lock_filename), - self._pid, - ) + '.lock' + self.pid_filename = os.path.join(os.path.dirname(self.lock_filename), self._pid) + '.lock' def get_lock_pid(self): try: @@ -97,14 +93,14 @@ def is_locked(self, force=False): raise FileLocked() # Locked, but want to wait for an unlock - interval = .1 + interval = 0.1 intervals = int(self.timeout / interval) while intervals: if self.valid_lock(): intervals -= 1 time.sleep(interval) - #print('stopping %s' % intervals) + # print('stopping %s' % intervals) else: return True diff --git a/post_office/logutils.py b/post_office/logutils.py index ce445c96..1b49c8a3 100644 --- a/post_office/logutils.py +++ b/post_office/logutils.py @@ -7,30 +7,20 @@ def setup_loghandlers(level=None): # Setup logging for post_office if not already configured logger = logging.getLogger('post_office') if not logger.handlers: - dictConfig({ - "version": 1, - "disable_existing_loggers": False, - - "formatters": { - "post_office": { - "format": "[%(levelname)s]%(asctime)s PID %(process)d: %(message)s", - "datefmt": "%Y-%m-%d %H:%M:%S", + dictConfig( + { + 'version': 1, + 'disable_existing_loggers': False, + 'formatters': { + 'post_office': { + 'format': '[%(levelname)s]%(asctime)s PID %(process)d: %(message)s', + 'datefmt': '%Y-%m-%d %H:%M:%S', + }, }, - }, - - "handlers": { - "post_office": { - "level": "DEBUG", - "class": "logging.StreamHandler", - "formatter": "post_office" + 'handlers': { + 'post_office': {'level': 'DEBUG', 'class': 'logging.StreamHandler', 'formatter': 'post_office'}, }, - }, - - "loggers": { - "post_office": { - "handlers": ["post_office"], - "level": level or "DEBUG" - } + 'loggers': {'post_office': {'handlers': ['post_office'], 'level': level or 'DEBUG'}}, } - }) + ) return logger diff --git a/post_office/mail.py b/post_office/mail.py index 156260de..6a7db7d6 100644 --- a/post_office/mail.py +++ b/post_office/mail.py @@ -1,4 +1,3 @@ - from django.conf import settings from django.core.exceptions import ValidationError from django.db import connection as db_connection @@ -34,7 +33,7 @@ split_emails, ) -logger = setup_loghandlers("INFO") +logger = setup_loghandlers('INFO') def create( @@ -42,9 +41,9 @@ def create( recipients=None, cc=None, bcc=None, - subject="", - message="", - html_message="", + subject='', + message='', + html_message='', context=None, scheduled_time=None, expires_at=None, @@ -53,7 +52,7 @@ def create( priority=None, render_on_delivery=False, commit=True, - backend="", + backend='', ): """ Creates an email from supplied keyword arguments. If template is @@ -69,10 +68,8 @@ def create( if bcc is None: bcc = [] if context is None: - context = "" - message_id = ( - make_msgid(domain=get_message_id_fqdn()) if get_message_id_enabled() else None - ) + context = '' + message_id = make_msgid(domain=get_message_id_fqdn()) if get_message_id_enabled() else None # If email is to be rendered during delivery, save all necessary # information @@ -133,9 +130,9 @@ def send( sender=None, template=None, context=None, - subject="", - message="", - html_message="", + subject='', + message='', + html_message='', scheduled_time=None, expires_at=None, headers=None, @@ -146,23 +143,23 @@ def send( commit=True, cc=None, bcc=None, - language="", - backend="", + language='', + backend='', ): try: recipients = parse_emails(recipients) except ValidationError as e: - raise ValidationError("recipients: %s" % e.message) + raise ValidationError('recipients: %s' % e.message) try: cc = parse_emails(cc) except ValidationError as e: - raise ValidationError("c: %s" % e.message) + raise ValidationError('c: %s' % e.message) try: bcc = parse_emails(bcc) except ValidationError as e: - raise ValidationError("bcc: %s" % e.message) + raise ValidationError('bcc: %s' % e.message) if sender is None: sender = settings.DEFAULT_FROM_EMAIL @@ -180,17 +177,11 @@ def send( if template: if subject: - raise ValueError( - 'You can\'t specify both "template" and "subject" arguments' - ) + raise ValueError('You can\'t specify both "template" and "subject" arguments') if message: - raise ValueError( - 'You can\'t specify both "template" and "message" arguments' - ) + raise ValueError('You can\'t specify both "template" and "message" arguments') if html_message: - raise ValueError( - 'You can\'t specify both "template" and "html_message" arguments' - ) + raise ValueError('You can\'t specify both "template" and "html_message" arguments') # template can be an EmailTemplate instance or name if isinstance(template, EmailTemplate): @@ -202,7 +193,7 @@ def send( template = get_email_template(template, language) if backend and backend not in get_available_backends().keys(): - raise ValueError("%s is not a valid backend alias" % backend) + raise ValueError('%s is not a valid backend alias' % backend) email = create( sender, @@ -255,14 +246,12 @@ def get_queued(): - Has expires_at after the current time or is None """ now = timezone.now() - query = (Q(scheduled_time__lte=now) | Q(scheduled_time=None)) & ( - Q(expires_at__gt=now) | Q(expires_at=None) - ) + query = (Q(scheduled_time__lte=now) | Q(scheduled_time=None)) & (Q(expires_at__gt=now) | Q(expires_at=None)) return ( Email.objects.filter(query, status__in=[STATUS.queued, STATUS.requeued]) - .select_related("template") + .select_related('template') .order_by(*get_sending_order()) - .prefetch_related("attachments")[: get_batch_size()] + .prefetch_related('attachments')[: get_batch_size()] ) @@ -274,9 +263,7 @@ def send_queued(processes=1, log_level=None): total_sent, total_failed, total_requeued = 0, 0, 0 total_email = len(queued_emails) - logger.info( - "Started sending %s emails with %s processes." % (total_email, processes) - ) + logger.info('Started sending %s emails with %s processes.' % (total_email, processes)) if log_level is None: log_level = get_log_level() @@ -300,7 +287,7 @@ def send_queued(processes=1, log_level=None): tasks = [] for email_list in email_lists: tasks.append(pool.apply_async(_send_bulk, args=(email_list,))) - + timeout = get_batch_delivery_timeout() results = [] @@ -311,12 +298,12 @@ def send_queued(processes=1, log_level=None): # for task in tasks: # try: # # Wait for all tasks to complete with a timeout - # # The get method is used with a timeout to wait for each result + # # The get method is used with a timeout to wait for each result # results.append(task.get(timeout=timeout)) # except (TimeoutError, ContextTimeoutError): # logger.exception("Process timed out after %d seconds" % timeout) - # results = pool.map(_send_bulk, email_lists) + # results = pool.map(_send_bulk, email_lists) pool.terminate() pool.join() @@ -325,7 +312,7 @@ def send_queued(processes=1, log_level=None): total_requeued = [result[2] for result in results] logger.info( - "%s emails attempted, %s sent, %s failed, %s requeued", + '%s emails attempted, %s sent, %s failed, %s requeued', total_email, total_sent, total_failed, @@ -349,17 +336,15 @@ def _send_bulk(emails, uses_multiprocessing=True, log_level=None): failed_emails = [] # This is a list of two tuples (email, exception) email_count = len(emails) - logger.info("Process started, sending %s emails" % email_count) + logger.info('Process started, sending %s emails' % email_count) def send(email): try: - email.dispatch( - log_level=log_level, commit=False, disconnect_after_delivery=False - ) + email.dispatch(log_level=log_level, commit=False, disconnect_after_delivery=False) sent_emails.append(email) - logger.debug("Successfully sent email #%d" % email.id) + logger.debug('Successfully sent email #%d' % email.id) except Exception as e: - logger.exception("Failed to send email #%d" % email.id) + logger.exception('Failed to send email #%d' % email.id) failed_emails.append((email, e)) # Prepare emails before we send these to threads for sending @@ -370,7 +355,7 @@ def send(email): try: email.prepare_email_message() except Exception as e: - logger.exception("Failed to prepare email #%d" % email.id) + logger.exception('Failed to prepare email #%d' % email.id) failed_emails.append((email, e)) number_of_threads = min(get_threads_per_process(), email_count) @@ -383,13 +368,13 @@ def send(email): timeout = get_batch_delivery_timeout() # Wait for all tasks to complete with a timeout - # The get method is used with a timeout to wait for each result + # The get method is used with a timeout to wait for each result for result in results: result.get(timeout=timeout) # for result in results: # try: # # Wait for all tasks to complete with a timeout - # # The get method is used with a timeout to wait for each result + # # The get method is used with a timeout to wait for each result # result.get(timeout=timeout) # except TimeoutError: # logger.exception("Process timed out after %d seconds" % timeout) @@ -421,9 +406,7 @@ def send(email): email.status = STATUS.failed num_failed += 1 - Email.objects.bulk_update( - emails_failed, ["status", "scheduled_time", "number_of_retries"] - ) + Email.objects.bulk_update(emails_failed, ['status', 'scheduled_time', 'number_of_retries']) # If log level is 0, log nothing, 1 logs only sending failures # and 2 means log both successes and failures @@ -451,7 +434,7 @@ def send(email): Log.objects.bulk_create(logs) logger.info( - "Process finished, %s attempted, %s sent, %s failed, %s requeued", + 'Process finished, %s attempted, %s sent, %s failed, %s requeued', email_count, len(sent_emails), num_failed, @@ -467,12 +450,12 @@ def send_queued_mail_until_done(lockfile=default_lockfile, processes=1, log_leve """ try: with FileLock(lockfile): - logger.info("Acquired lock for sending queued emails at %s.lock", lockfile) + logger.info('Acquired lock for sending queued emails at %s.lock', lockfile) while True: try: send_queued(processes, log_level) except Exception as e: - logger.exception(e, extra={"status_code": 500}) + logger.exception(e, extra={'status_code': 500}) raise # Close DB connection to avoid multiprocessing errors @@ -481,4 +464,4 @@ def send_queued_mail_until_done(lockfile=default_lockfile, processes=1, log_leve if not get_queued().exists(): break except FileLocked: - logger.info("Failed to acquire lock, terminating now.") + logger.info('Failed to acquire lock, terminating now.') diff --git a/post_office/management/commands/cleanup_mail.py b/post_office/management/commands/cleanup_mail.py index bdbe354e..fadf2ab4 100644 --- a/post_office/management/commands/cleanup_mail.py +++ b/post_office/management/commands/cleanup_mail.py @@ -10,18 +10,17 @@ class Command(BaseCommand): help = 'Place deferred messages back in the queue.' def add_arguments(self, parser): - parser.add_argument('-d', '--days', - type=int, default=90, - help="Cleanup mails older than this many days, defaults to 90.") + parser.add_argument( + '-d', '--days', type=int, default=90, help='Cleanup mails older than this many days, defaults to 90.' + ) - parser.add_argument('-da', '--delete-attachments', action='store_true', - help="Delete orphaned attachments.") + parser.add_argument('-da', '--delete-attachments', action='store_true', help='Delete orphaned attachments.') - parser.add_argument('-b', '--batch-size', type=int, default=1000, help="Batch size for cleanup.") + parser.add_argument('-b', '--batch-size', type=int, default=1000, help='Batch size for cleanup.') def handle(self, verbosity, days, delete_attachments, batch_size, **options): # Delete mails and their related logs and queued created before X days cutoff_date = now() - datetime.timedelta(days) num_emails, num_attachments = cleanup_expired_mails(cutoff_date, delete_attachments, batch_size) - msg = "Deleted {0} mails created before {1} and {2} attachments." + msg = 'Deleted {0} mails created before {1} and {2} attachments.' self.stdout.write(msg.format(num_emails, cutoff_date, num_attachments)) diff --git a/post_office/management/commands/send_queued_mail.py b/post_office/management/commands/send_queued_mail.py index 84c2584f..5b205d69 100644 --- a/post_office/management/commands/send_queued_mail.py +++ b/post_office/management/commands/send_queued_mail.py @@ -7,18 +7,21 @@ class Command(BaseCommand): def add_arguments(self, parser): parser.add_argument( - '-p', '--processes', + '-p', + '--processes', type=int, default=1, help='Number of processes used to send emails', ) parser.add_argument( - '-L', '--lockfile', + '-L', + '--lockfile', default=default_lockfile, help='Absolute path of lockfile to acquire', ) parser.add_argument( - '-l', '--log-level', + '-l', + '--log-level', type=int, help='"0" to log nothing, "1" to only log errors', ) diff --git a/post_office/migrations/0001_initial.py b/post_office/migrations/0001_initial.py index 0f80ca5e..ff7fcd38 100644 --- a/post_office/migrations/0001_initial.py +++ b/post_office/migrations/0001_initial.py @@ -6,9 +6,7 @@ class Migration(migrations.Migration): - - dependencies = [ - ] + dependencies = [] operations = [ migrations.CreateModel( @@ -18,31 +16,42 @@ class Migration(migrations.Migration): ('file', models.FileField(upload_to=post_office.models.get_upload_path)), ('name', models.CharField(help_text='The original filename', max_length=255)), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( name='Email', fields=[ ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('from_email', models.CharField(max_length=254, validators=[post_office.validators.validate_email_with_name])), + ( + 'from_email', + models.CharField(max_length=254, validators=[post_office.validators.validate_email_with_name]), + ), ('to', post_office.fields.CommaSeparatedEmailField(blank=True)), ('cc', post_office.fields.CommaSeparatedEmailField(blank=True)), ('bcc', post_office.fields.CommaSeparatedEmailField(blank=True)), ('subject', models.CharField(max_length=255, blank=True)), ('message', models.TextField(blank=True)), ('html_message', models.TextField(blank=True)), - ('status', models.PositiveSmallIntegerField(blank=True, null=True, db_index=True, choices=[(0, 'sent'), (1, 'failed'), (2, 'queued')])), - ('priority', models.PositiveSmallIntegerField(blank=True, null=True, choices=[(0, 'low'), (1, 'medium'), (2, 'high'), (3, 'now')])), + ( + 'status', + models.PositiveSmallIntegerField( + blank=True, null=True, db_index=True, choices=[(0, 'sent'), (1, 'failed'), (2, 'queued')] + ), + ), + ( + 'priority', + models.PositiveSmallIntegerField( + blank=True, null=True, choices=[(0, 'low'), (1, 'medium'), (2, 'high'), (3, 'now')] + ), + ), ('created', models.DateTimeField(auto_now_add=True, db_index=True)), ('last_updated', models.DateTimeField(auto_now=True, db_index=True)), ('scheduled_time', models.DateTimeField(db_index=True, null=True, blank=True)), ('headers', models.JSONField(null=True, blank=True)), ('context', models.JSONField(null=True, blank=True)), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( @@ -51,14 +60,21 @@ class Migration(migrations.Migration): ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), ('name', models.CharField(help_text="e.g: 'welcome_email'", max_length=255)), ('description', models.TextField(help_text='Description of this template.', blank=True)), - ('subject', models.CharField(blank=True, max_length=255, validators=[post_office.validators.validate_template_syntax])), + ( + 'subject', + models.CharField( + blank=True, max_length=255, validators=[post_office.validators.validate_template_syntax] + ), + ), ('content', models.TextField(blank=True, validators=[post_office.validators.validate_template_syntax])), - ('html_content', models.TextField(blank=True, validators=[post_office.validators.validate_template_syntax])), + ( + 'html_content', + models.TextField(blank=True, validators=[post_office.validators.validate_template_syntax]), + ), ('created', models.DateTimeField(auto_now_add=True)), ('last_updated', models.DateTimeField(auto_now=True)), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.CreateModel( @@ -69,16 +85,25 @@ class Migration(migrations.Migration): ('status', models.PositiveSmallIntegerField(choices=[(0, 'sent'), (1, 'failed')])), ('exception_type', models.CharField(max_length=255, blank=True)), ('message', models.TextField()), - ('email', models.ForeignKey(related_name='logs', editable=False, on_delete=models.deletion.CASCADE, to='post_office.Email', )), + ( + 'email', + models.ForeignKey( + related_name='logs', + editable=False, + on_delete=models.deletion.CASCADE, + to='post_office.Email', + ), + ), ], - options={ - }, + options={}, bases=(models.Model,), ), migrations.AddField( model_name='email', name='template', - field=models.ForeignKey(blank=True, on_delete=models.deletion.SET_NULL, to='post_office.EmailTemplate', null=True), + field=models.ForeignKey( + blank=True, on_delete=models.deletion.SET_NULL, to='post_office.EmailTemplate', null=True + ), preserve_default=True, ), migrations.AddField( diff --git a/post_office/migrations/0002_add_i18n_and_backend_alias.py b/post_office/migrations/0002_add_i18n_and_backend_alias.py index 79a7f5d2..be8c2177 100644 --- a/post_office/migrations/0002_add_i18n_and_backend_alias.py +++ b/post_office/migrations/0002_add_i18n_and_backend_alias.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ('post_office', '0001_initial'), ] @@ -22,12 +21,111 @@ class Migration(migrations.Migration): migrations.AddField( model_name='emailtemplate', name='default_template', - field=models.ForeignKey(related_name='translated_templates', default=None, to='post_office.EmailTemplate', null=True, on_delete=models.deletion.SET_NULL), + field=models.ForeignKey( + related_name='translated_templates', + default=None, + to='post_office.EmailTemplate', + null=True, + on_delete=models.deletion.SET_NULL, + ), ), migrations.AddField( model_name='emailtemplate', name='language', - field=models.CharField(default='', help_text='Render template in alternative language', max_length=12, blank=True, choices=[('af', 'Afrikaans'), ('ar', 'Arabic'), ('ast', 'Asturian'), ('az', 'Azerbaijani'), ('bg', 'Bulgarian'), ('be', 'Belarusian'), ('bn', 'Bengali'), ('br', 'Breton'), ('bs', 'Bosnian'), ('ca', 'Catalan'), ('cs', 'Czech'), ('cy', 'Welsh'), ('da', 'Danish'), ('de', 'German'), ('el', 'Greek'), ('en', 'English'), ('en-au', 'Australian English'), ('en-gb', 'British English'), ('eo', 'Esperanto'), ('es', 'Spanish'), ('es-ar', 'Argentinian Spanish'), ('es-mx', 'Mexican Spanish'), ('es-ni', 'Nicaraguan Spanish'), ('es-ve', 'Venezuelan Spanish'), ('et', 'Estonian'), ('eu', 'Basque'), ('fa', 'Persian'), ('fi', 'Finnish'), ('fr', 'French'), ('fy', 'Frisian'), ('ga', 'Irish'), ('gl', 'Galician'), ('he', 'Hebrew'), ('hi', 'Hindi'), ('hr', 'Croatian'), ('hu', 'Hungarian'), ('ia', 'Interlingua'), ('id', 'Indonesian'), ('io', 'Ido'), ('is', 'Icelandic'), ('it', 'Italian'), ('ja', 'Japanese'), ('ka', 'Georgian'), ('kk', 'Kazakh'), ('km', 'Khmer'), ('kn', 'Kannada'), ('ko', 'Korean'), ('lb', 'Luxembourgish'), ('lt', 'Lithuanian'), ('lv', 'Latvian'), ('mk', 'Macedonian'), ('ml', 'Malayalam'), ('mn', 'Mongolian'), ('mr', 'Marathi'), ('my', 'Burmese'), ('nb', 'Norwegian Bokmal'), ('ne', 'Nepali'), ('nl', 'Dutch'), ('nn', 'Norwegian Nynorsk'), ('os', 'Ossetic'), ('pa', 'Punjabi'), ('pl', 'Polish'), ('pt', 'Portuguese'), ('pt-br', 'Brazilian Portuguese'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sk', 'Slovak'), ('sl', 'Slovenian'), ('sq', 'Albanian'), ('sr', 'Serbian'), ('sr-latn', 'Serbian Latin'), ('sv', 'Swedish'), ('sw', 'Swahili'), ('ta', 'Tamil'), ('te', 'Telugu'), ('th', 'Thai'), ('tr', 'Turkish'), ('tt', 'Tatar'), ('udm', 'Udmurt'), ('uk', 'Ukrainian'), ('ur', 'Urdu'), ('vi', 'Vietnamese'), ('zh-cn', 'Simplified Chinese'), ('zh-hans', 'Simplified Chinese'), ('zh-hant', 'Traditional Chinese'), ('zh-tw', 'Traditional Chinese')]), + field=models.CharField( + default='', + help_text='Render template in alternative language', + max_length=12, + blank=True, + choices=[ + ('af', 'Afrikaans'), + ('ar', 'Arabic'), + ('ast', 'Asturian'), + ('az', 'Azerbaijani'), + ('bg', 'Bulgarian'), + ('be', 'Belarusian'), + ('bn', 'Bengali'), + ('br', 'Breton'), + ('bs', 'Bosnian'), + ('ca', 'Catalan'), + ('cs', 'Czech'), + ('cy', 'Welsh'), + ('da', 'Danish'), + ('de', 'German'), + ('el', 'Greek'), + ('en', 'English'), + ('en-au', 'Australian English'), + ('en-gb', 'British English'), + ('eo', 'Esperanto'), + ('es', 'Spanish'), + ('es-ar', 'Argentinian Spanish'), + ('es-mx', 'Mexican Spanish'), + ('es-ni', 'Nicaraguan Spanish'), + ('es-ve', 'Venezuelan Spanish'), + ('et', 'Estonian'), + ('eu', 'Basque'), + ('fa', 'Persian'), + ('fi', 'Finnish'), + ('fr', 'French'), + ('fy', 'Frisian'), + ('ga', 'Irish'), + ('gl', 'Galician'), + ('he', 'Hebrew'), + ('hi', 'Hindi'), + ('hr', 'Croatian'), + ('hu', 'Hungarian'), + ('ia', 'Interlingua'), + ('id', 'Indonesian'), + ('io', 'Ido'), + ('is', 'Icelandic'), + ('it', 'Italian'), + ('ja', 'Japanese'), + ('ka', 'Georgian'), + ('kk', 'Kazakh'), + ('km', 'Khmer'), + ('kn', 'Kannada'), + ('ko', 'Korean'), + ('lb', 'Luxembourgish'), + ('lt', 'Lithuanian'), + ('lv', 'Latvian'), + ('mk', 'Macedonian'), + ('ml', 'Malayalam'), + ('mn', 'Mongolian'), + ('mr', 'Marathi'), + ('my', 'Burmese'), + ('nb', 'Norwegian Bokmal'), + ('ne', 'Nepali'), + ('nl', 'Dutch'), + ('nn', 'Norwegian Nynorsk'), + ('os', 'Ossetic'), + ('pa', 'Punjabi'), + ('pl', 'Polish'), + ('pt', 'Portuguese'), + ('pt-br', 'Brazilian Portuguese'), + ('ro', 'Romanian'), + ('ru', 'Russian'), + ('sk', 'Slovak'), + ('sl', 'Slovenian'), + ('sq', 'Albanian'), + ('sr', 'Serbian'), + ('sr-latn', 'Serbian Latin'), + ('sv', 'Swedish'), + ('sw', 'Swahili'), + ('ta', 'Tamil'), + ('te', 'Telugu'), + ('th', 'Thai'), + ('tr', 'Turkish'), + ('tt', 'Tatar'), + ('udm', 'Udmurt'), + ('uk', 'Ukrainian'), + ('ur', 'Urdu'), + ('vi', 'Vietnamese'), + ('zh-cn', 'Simplified Chinese'), + ('zh-hans', 'Simplified Chinese'), + ('zh-hant', 'Traditional Chinese'), + ('zh-tw', 'Traditional Chinese'), + ], + ), ), migrations.AlterField( model_name='email', @@ -42,7 +140,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='email', name='from_email', - field=models.CharField(max_length=254, verbose_name='Email From', validators=[post_office.validators.validate_email_with_name]), + field=models.CharField( + max_length=254, verbose_name='Email From', validators=[post_office.validators.validate_email_with_name] + ), ), migrations.AlterField( model_name='email', @@ -67,17 +167,26 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='emailtemplate', name='content', - field=models.TextField(blank=True, verbose_name='Content', validators=[post_office.validators.validate_template_syntax]), + field=models.TextField( + blank=True, verbose_name='Content', validators=[post_office.validators.validate_template_syntax] + ), ), migrations.AlterField( model_name='emailtemplate', name='html_content', - field=models.TextField(blank=True, verbose_name='HTML content', validators=[post_office.validators.validate_template_syntax]), + field=models.TextField( + blank=True, verbose_name='HTML content', validators=[post_office.validators.validate_template_syntax] + ), ), migrations.AlterField( model_name='emailtemplate', name='subject', - field=models.CharField(blank=True, max_length=255, verbose_name='Subject', validators=[post_office.validators.validate_template_syntax]), + field=models.CharField( + blank=True, + max_length=255, + verbose_name='Subject', + validators=[post_office.validators.validate_template_syntax], + ), ), migrations.AlterUniqueTogether( name='emailtemplate', diff --git a/post_office/migrations/0003_longer_subject.py b/post_office/migrations/0003_longer_subject.py index c24cab42..a63a2f9e 100644 --- a/post_office/migrations/0003_longer_subject.py +++ b/post_office/migrations/0003_longer_subject.py @@ -3,7 +3,6 @@ class Migration(migrations.Migration): - dependencies = [ ('post_office', '0002_add_i18n_and_backend_alias'), ] @@ -17,6 +16,99 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='emailtemplate', name='language', - field=models.CharField(blank=True, choices=[('af', 'Afrikaans'), ('ar', 'Arabic'), ('ast', 'Asturian'), ('az', 'Azerbaijani'), ('bg', 'Bulgarian'), ('be', 'Belarusian'), ('bn', 'Bengali'), ('br', 'Breton'), ('bs', 'Bosnian'), ('ca', 'Catalan'), ('cs', 'Czech'), ('cy', 'Welsh'), ('da', 'Danish'), ('de', 'German'), ('el', 'Greek'), ('en', 'English'), ('en-au', 'Australian English'), ('en-gb', 'British English'), ('eo', 'Esperanto'), ('es', 'Spanish'), ('es-ar', 'Argentinian Spanish'), ('es-co', 'Colombian Spanish'), ('es-mx', 'Mexican Spanish'), ('es-ni', 'Nicaraguan Spanish'), ('es-ve', 'Venezuelan Spanish'), ('et', 'Estonian'), ('eu', 'Basque'), ('fa', 'Persian'), ('fi', 'Finnish'), ('fr', 'French'), ('fy', 'Frisian'), ('ga', 'Irish'), ('gd', 'Scottish Gaelic'), ('gl', 'Galician'), ('he', 'Hebrew'), ('hi', 'Hindi'), ('hr', 'Croatian'), ('hu', 'Hungarian'), ('ia', 'Interlingua'), ('id', 'Indonesian'), ('io', 'Ido'), ('is', 'Icelandic'), ('it', 'Italian'), ('ja', 'Japanese'), ('ka', 'Georgian'), ('kk', 'Kazakh'), ('km', 'Khmer'), ('kn', 'Kannada'), ('ko', 'Korean'), ('lb', 'Luxembourgish'), ('lt', 'Lithuanian'), ('lv', 'Latvian'), ('mk', 'Macedonian'), ('ml', 'Malayalam'), ('mn', 'Mongolian'), ('mr', 'Marathi'), ('my', 'Burmese'), ('nb', 'Norwegian Bokmal'), ('ne', 'Nepali'), ('nl', 'Dutch'), ('nn', 'Norwegian Nynorsk'), ('os', 'Ossetic'), ('pa', 'Punjabi'), ('pl', 'Polish'), ('pt', 'Portuguese'), ('pt-br', 'Brazilian Portuguese'), ('ro', 'Romanian'), ('ru', 'Russian'), ('sk', 'Slovak'), ('sl', 'Slovenian'), ('sq', 'Albanian'), ('sr', 'Serbian'), ('sr-latn', 'Serbian Latin'), ('sv', 'Swedish'), ('sw', 'Swahili'), ('ta', 'Tamil'), ('te', 'Telugu'), ('th', 'Thai'), ('tr', 'Turkish'), ('tt', 'Tatar'), ('udm', 'Udmurt'), ('uk', 'Ukrainian'), ('ur', 'Urdu'), ('vi', 'Vietnamese'), ('zh-hans', 'Simplified Chinese'), ('zh-hant', 'Traditional Chinese')], default='', help_text='Render template in alternative language', max_length=12), + field=models.CharField( + blank=True, + choices=[ + ('af', 'Afrikaans'), + ('ar', 'Arabic'), + ('ast', 'Asturian'), + ('az', 'Azerbaijani'), + ('bg', 'Bulgarian'), + ('be', 'Belarusian'), + ('bn', 'Bengali'), + ('br', 'Breton'), + ('bs', 'Bosnian'), + ('ca', 'Catalan'), + ('cs', 'Czech'), + ('cy', 'Welsh'), + ('da', 'Danish'), + ('de', 'German'), + ('el', 'Greek'), + ('en', 'English'), + ('en-au', 'Australian English'), + ('en-gb', 'British English'), + ('eo', 'Esperanto'), + ('es', 'Spanish'), + ('es-ar', 'Argentinian Spanish'), + ('es-co', 'Colombian Spanish'), + ('es-mx', 'Mexican Spanish'), + ('es-ni', 'Nicaraguan Spanish'), + ('es-ve', 'Venezuelan Spanish'), + ('et', 'Estonian'), + ('eu', 'Basque'), + ('fa', 'Persian'), + ('fi', 'Finnish'), + ('fr', 'French'), + ('fy', 'Frisian'), + ('ga', 'Irish'), + ('gd', 'Scottish Gaelic'), + ('gl', 'Galician'), + ('he', 'Hebrew'), + ('hi', 'Hindi'), + ('hr', 'Croatian'), + ('hu', 'Hungarian'), + ('ia', 'Interlingua'), + ('id', 'Indonesian'), + ('io', 'Ido'), + ('is', 'Icelandic'), + ('it', 'Italian'), + ('ja', 'Japanese'), + ('ka', 'Georgian'), + ('kk', 'Kazakh'), + ('km', 'Khmer'), + ('kn', 'Kannada'), + ('ko', 'Korean'), + ('lb', 'Luxembourgish'), + ('lt', 'Lithuanian'), + ('lv', 'Latvian'), + ('mk', 'Macedonian'), + ('ml', 'Malayalam'), + ('mn', 'Mongolian'), + ('mr', 'Marathi'), + ('my', 'Burmese'), + ('nb', 'Norwegian Bokmal'), + ('ne', 'Nepali'), + ('nl', 'Dutch'), + ('nn', 'Norwegian Nynorsk'), + ('os', 'Ossetic'), + ('pa', 'Punjabi'), + ('pl', 'Polish'), + ('pt', 'Portuguese'), + ('pt-br', 'Brazilian Portuguese'), + ('ro', 'Romanian'), + ('ru', 'Russian'), + ('sk', 'Slovak'), + ('sl', 'Slovenian'), + ('sq', 'Albanian'), + ('sr', 'Serbian'), + ('sr-latn', 'Serbian Latin'), + ('sv', 'Swedish'), + ('sw', 'Swahili'), + ('ta', 'Tamil'), + ('te', 'Telugu'), + ('th', 'Thai'), + ('tr', 'Turkish'), + ('tt', 'Tatar'), + ('udm', 'Udmurt'), + ('uk', 'Ukrainian'), + ('ur', 'Urdu'), + ('vi', 'Vietnamese'), + ('zh-hans', 'Simplified Chinese'), + ('zh-hant', 'Traditional Chinese'), + ], + default='', + help_text='Render template in alternative language', + max_length=12, + ), ), ] diff --git a/post_office/migrations/0004_auto_20160607_0901.py b/post_office/migrations/0004_auto_20160607_0901.py index 30b21590..6b31a287 100644 --- a/post_office/migrations/0004_auto_20160607_0901.py +++ b/post_office/migrations/0004_auto_20160607_0901.py @@ -6,7 +6,6 @@ class Migration(migrations.Migration): - dependencies = [ ('post_office', '0003_longer_subject'), ] @@ -27,7 +26,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='attachment', name='emails', - field=models.ManyToManyField(related_name='attachments', to='post_office.Email', verbose_name='Email addresses'), + field=models.ManyToManyField( + related_name='attachments', to='post_office.Email', verbose_name='Email addresses' + ), ), migrations.AlterField( model_name='attachment', @@ -57,7 +58,12 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='email', name='priority', - field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'low'), (1, 'medium'), (2, 'high'), (3, 'now')], null=True, verbose_name='Priority'), + field=models.PositiveSmallIntegerField( + blank=True, + choices=[(0, 'low'), (1, 'medium'), (2, 'high'), (3, 'now')], + null=True, + verbose_name='Priority', + ), ), migrations.AlterField( model_name='email', @@ -67,17 +73,36 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='email', name='status', - field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'sent'), (1, 'failed'), (2, 'queued')], db_index=True, null=True, verbose_name='Status'), + field=models.PositiveSmallIntegerField( + blank=True, + choices=[(0, 'sent'), (1, 'failed'), (2, 'queued')], + db_index=True, + null=True, + verbose_name='Status', + ), ), migrations.AlterField( model_name='email', name='template', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, to='post_office.EmailTemplate', verbose_name='Email template'), + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.CASCADE, + to='post_office.EmailTemplate', + verbose_name='Email template', + ), ), migrations.AlterField( model_name='emailtemplate', name='default_template', - field=models.ForeignKey(default=None, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='translated_templates', to='post_office.EmailTemplate', verbose_name='Default template'), + field=models.ForeignKey( + default=None, + null=True, + on_delete=django.db.models.deletion.CASCADE, + related_name='translated_templates', + to='post_office.EmailTemplate', + verbose_name='Default template', + ), ), migrations.AlterField( model_name='emailtemplate', @@ -87,7 +112,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='emailtemplate', name='language', - field=models.CharField(blank=True, default='', help_text='Render template in alternative language', max_length=12, verbose_name='Language'), + field=models.CharField( + blank=True, + default='', + help_text='Render template in alternative language', + max_length=12, + verbose_name='Language', + ), ), migrations.AlterField( model_name='emailtemplate', @@ -97,7 +128,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='log', name='email', - field=models.ForeignKey(editable=False, on_delete=django.db.models.deletion.CASCADE, related_name='logs', to='post_office.Email', verbose_name='Email address'), + field=models.ForeignKey( + editable=False, + on_delete=django.db.models.deletion.CASCADE, + related_name='logs', + to='post_office.Email', + verbose_name='Email address', + ), ), migrations.AlterField( model_name='log', diff --git a/post_office/migrations/0005_auto_20170515_0013.py b/post_office/migrations/0005_auto_20170515_0013.py index 70b343a2..fdf34db8 100644 --- a/post_office/migrations/0005_auto_20170515_0013.py +++ b/post_office/migrations/0005_auto_20170515_0013.py @@ -3,7 +3,6 @@ class Migration(migrations.Migration): - dependencies = [ ('post_office', '0004_auto_20160607_0901'), ] diff --git a/post_office/migrations/0006_attachment_mimetype.py b/post_office/migrations/0006_attachment_mimetype.py index ef41e32d..f217cbe3 100644 --- a/post_office/migrations/0006_attachment_mimetype.py +++ b/post_office/migrations/0006_attachment_mimetype.py @@ -2,7 +2,6 @@ class Migration(migrations.Migration): - dependencies = [ ('post_office', '0005_auto_20170515_0013'), ] diff --git a/post_office/migrations/0007_auto_20170731_1342.py b/post_office/migrations/0007_auto_20170731_1342.py index a3c32fec..22a7bc6c 100644 --- a/post_office/migrations/0007_auto_20170731_1342.py +++ b/post_office/migrations/0007_auto_20170731_1342.py @@ -3,7 +3,6 @@ class Migration(migrations.Migration): - dependencies = [ ('post_office', '0006_attachment_mimetype'), ] diff --git a/post_office/migrations/0008_attachment_headers.py b/post_office/migrations/0008_attachment_headers.py index 1b014123..44d18969 100644 --- a/post_office/migrations/0008_attachment_headers.py +++ b/post_office/migrations/0008_attachment_headers.py @@ -3,7 +3,6 @@ class Migration(migrations.Migration): - dependencies = [ ('post_office', '0007_auto_20170731_1342'), ] diff --git a/post_office/migrations/0009_requeued_mode.py b/post_office/migrations/0009_requeued_mode.py index 0b974810..c877fc32 100644 --- a/post_office/migrations/0009_requeued_mode.py +++ b/post_office/migrations/0009_requeued_mode.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ('post_office', '0008_attachment_headers'), ] @@ -18,7 +17,13 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='email', name='status', - field=models.PositiveSmallIntegerField(blank=True, choices=[(0, 'sent'), (1, 'failed'), (2, 'queued'), (3, 'requeued')], db_index=True, null=True, verbose_name='Status'), + field=models.PositiveSmallIntegerField( + blank=True, + choices=[(0, 'sent'), (1, 'failed'), (2, 'queued'), (3, 'requeued')], + db_index=True, + null=True, + verbose_name='Status', + ), ), migrations.AddField( model_name='email', diff --git a/post_office/migrations/0010_message_id.py b/post_office/migrations/0010_message_id.py index cb5d8928..f7966143 100644 --- a/post_office/migrations/0010_message_id.py +++ b/post_office/migrations/0010_message_id.py @@ -19,7 +19,6 @@ def forwards(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ('post_office', '0009_requeued_mode'), ] @@ -33,7 +32,9 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='email', name='expires_at', - field=models.DateTimeField(blank=True, help_text="Email won't be sent after this timestamp", null=True, verbose_name='Expires at'), + field=models.DateTimeField( + blank=True, help_text="Email won't be sent after this timestamp", null=True, verbose_name='Expires at' + ), ), migrations.RunPython(forwards, reverse_code=migrations.RunPython.noop), ] diff --git a/post_office/migrations/0011_models_help_text.py b/post_office/migrations/0011_models_help_text.py index 7e900d9a..e4426913 100644 --- a/post_office/migrations/0011_models_help_text.py +++ b/post_office/migrations/0011_models_help_text.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ('post_office', '0010_message_id'), ] @@ -18,11 +17,19 @@ class Migration(migrations.Migration): migrations.AlterField( model_name='email', name='expires_at', - field=models.DateTimeField(blank=True, help_text="Email won't be sent after this timestamp", null=True, verbose_name='Expires'), + field=models.DateTimeField( + blank=True, help_text="Email won't be sent after this timestamp", null=True, verbose_name='Expires' + ), ), migrations.AlterField( model_name='email', name='scheduled_time', - field=models.DateTimeField(blank=True, db_index=True, help_text='The scheduled sending time', null=True, verbose_name='Scheduled Time'), + field=models.DateTimeField( + blank=True, + db_index=True, + help_text='The scheduled sending time', + null=True, + verbose_name='Scheduled Time', + ), ), ] diff --git a/post_office/models.py b/post_office/models.py index d0e3f158..aece7604 100644 --- a/post_office/models.py +++ b/post_office/models.py @@ -20,7 +20,7 @@ from .validators import validate_email_with_name, validate_template_syntax -logger = setup_loghandlers("INFO") +logger = setup_loghandlers('INFO') PRIORITY = namedtuple('PRIORITY', 'low medium high now')._make(range(4)) @@ -32,53 +32,54 @@ class Email(models.Model): A model to hold email information. """ - PRIORITY_CHOICES = [(PRIORITY.low, _("low")), (PRIORITY.medium, _("medium")), - (PRIORITY.high, _("high")), (PRIORITY.now, _("now"))] - STATUS_CHOICES = [(STATUS.sent, _("sent")), (STATUS.failed, _("failed")), - (STATUS.queued, _("queued")), (STATUS.requeued, _("requeued"))] - - from_email = models.CharField(_("Email From"), max_length=254, - validators=[validate_email_with_name]) - to = CommaSeparatedEmailField(_("Email To")) - cc = CommaSeparatedEmailField(_("Cc")) - bcc = CommaSeparatedEmailField(_("Bcc")) - subject = models.CharField(_("Subject"), max_length=989, blank=True) - message = models.TextField(_("Message"), blank=True) - html_message = models.TextField(_("HTML Message"), blank=True) + PRIORITY_CHOICES = [ + (PRIORITY.low, _('low')), + (PRIORITY.medium, _('medium')), + (PRIORITY.high, _('high')), + (PRIORITY.now, _('now')), + ] + STATUS_CHOICES = [ + (STATUS.sent, _('sent')), + (STATUS.failed, _('failed')), + (STATUS.queued, _('queued')), + (STATUS.requeued, _('requeued')), + ] + + from_email = models.CharField(_('Email From'), max_length=254, validators=[validate_email_with_name]) + to = CommaSeparatedEmailField(_('Email To')) + cc = CommaSeparatedEmailField(_('Cc')) + bcc = CommaSeparatedEmailField(_('Bcc')) + subject = models.CharField(_('Subject'), max_length=989, blank=True) + message = models.TextField(_('Message'), blank=True) + html_message = models.TextField(_('HTML Message'), blank=True) """ Emails with 'queued' status will get processed by ``send_queued`` command. Status field will then be set to ``failed`` or ``sent`` depending on whether it's successfully delivered. """ - status = models.PositiveSmallIntegerField( - _("Status"), - choices=STATUS_CHOICES, db_index=True, - blank=True, null=True) - priority = models.PositiveSmallIntegerField(_("Priority"), - choices=PRIORITY_CHOICES, - blank=True, null=True) + status = models.PositiveSmallIntegerField(_('Status'), choices=STATUS_CHOICES, db_index=True, blank=True, null=True) + priority = models.PositiveSmallIntegerField(_('Priority'), choices=PRIORITY_CHOICES, blank=True, null=True) created = models.DateTimeField(auto_now_add=True, db_index=True) last_updated = models.DateTimeField(db_index=True, auto_now=True) - scheduled_time = models.DateTimeField(_("Scheduled Time"), - blank=True, null=True, db_index=True, - help_text=_("The scheduled sending time")) - expires_at = models.DateTimeField(_("Expires"), - blank=True, null=True, - help_text=_("Email won't be sent after this timestamp")) - message_id = models.CharField("Message-ID", null=True, max_length=255, editable=False) + scheduled_time = models.DateTimeField( + _('Scheduled Time'), blank=True, null=True, db_index=True, help_text=_('The scheduled sending time') + ) + expires_at = models.DateTimeField( + _('Expires'), blank=True, null=True, help_text=_("Email won't be sent after this timestamp") + ) + message_id = models.CharField('Message-ID', null=True, max_length=255, editable=False) number_of_retries = models.PositiveIntegerField(null=True, blank=True) headers = models.JSONField(_('Headers'), blank=True, null=True) - template = models.ForeignKey('post_office.EmailTemplate', blank=True, - null=True, verbose_name=_("Email template"), - on_delete=models.CASCADE) + template = models.ForeignKey( + 'post_office.EmailTemplate', blank=True, null=True, verbose_name=_('Email template'), on_delete=models.CASCADE + ) context = context_field_class(_('Context'), blank=True, null=True) - backend_alias = models.CharField(_("Backend alias"), blank=True, default='', - max_length=64) + backend_alias = models.CharField(_('Backend alias'), blank=True, default='', max_length=64) class Meta: app_label = 'post_office' - verbose_name = pgettext_lazy("Email address", "Email") - verbose_name_plural = pgettext_lazy("Email addresses", "Emails") + verbose_name = pgettext_lazy('Email address', 'Email') + verbose_name_plural = pgettext_lazy('Email addresses', 'Emails') def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -121,7 +122,7 @@ def prepare_email_message(self): if isinstance(self.headers, dict) or self.expires_at or self.message_id: headers = dict(self.headers or {}) if self.expires_at: - headers.update({'Expires': self.expires_at.strftime("%a, %-d %b %H:%M:%S %z")}) + headers.update({'Expires': self.expires_at.strftime('%a, %-d %b %H:%M:%S %z')}) if self.message_id: headers.update({'Message-ID': self.message_id}) else: @@ -130,24 +131,42 @@ def prepare_email_message(self): if html_message: if plaintext_message: msg = EmailMultiAlternatives( - subject=subject, body=plaintext_message, from_email=self.from_email, - to=self.to, bcc=self.bcc, cc=self.cc, - headers=headers, connection=connection) - msg.attach_alternative(html_message, "text/html") + subject=subject, + body=plaintext_message, + from_email=self.from_email, + to=self.to, + bcc=self.bcc, + cc=self.cc, + headers=headers, + connection=connection, + ) + msg.attach_alternative(html_message, 'text/html') else: msg = EmailMultiAlternatives( - subject=subject, body=html_message, from_email=self.from_email, - to=self.to, bcc=self.bcc, cc=self.cc, - headers=headers, connection=connection) + subject=subject, + body=html_message, + from_email=self.from_email, + to=self.to, + bcc=self.bcc, + cc=self.cc, + headers=headers, + connection=connection, + ) msg.content_subtype = 'html' if hasattr(multipart_template, 'attach_related'): multipart_template.attach_related(msg) else: msg = EmailMessage( - subject=subject, body=plaintext_message, from_email=self.from_email, - to=self.to, bcc=self.bcc, cc=self.cc, - headers=headers, connection=connection) + subject=subject, + body=plaintext_message, + from_email=self.from_email, + to=self.to, + bcc=self.bcc, + cc=self.cc, + headers=headers, + connection=connection, + ) for attachment in self.attachments.all(): if attachment.headers: @@ -166,8 +185,7 @@ def prepare_email_message(self): self._cached_email_message = msg return msg - def dispatch(self, log_level=None, - disconnect_after_delivery=True, commit=True): + def dispatch(self, log_level=None, disconnect_after_delivery=True, commit=True): """ Sends email and log the result. """ @@ -202,17 +220,15 @@ def dispatch(self, log_level=None, # and 2 means log both successes and failures if log_level == 1: if status == STATUS.failed: - self.logs.create(status=status, message=message, - exception_type=exception_type) + self.logs.create(status=status, message=message, exception_type=exception_type) elif log_level == 2: - self.logs.create(status=status, message=message, - exception_type=exception_type) + self.logs.create(status=status, message=message, exception_type=exception_type) return status def clean(self): if self.scheduled_time and self.expires_at and self.scheduled_time > self.expires_at: - raise ValidationError(_("The scheduled time may not be later than the expires time.")) + raise ValidationError(_('The scheduled time may not be later than the expires time.')) def save(self, *args, **kwargs): self.full_clean() @@ -224,10 +240,11 @@ class Log(models.Model): A model to record sending email sending activities. """ - STATUS_CHOICES = [(STATUS.sent, _("sent")), (STATUS.failed, _("failed"))] + STATUS_CHOICES = [(STATUS.sent, _('sent')), (STATUS.failed, _('failed'))] - email = models.ForeignKey(Email, editable=False, related_name='logs', - verbose_name=_('Email address'), on_delete=models.CASCADE) + email = models.ForeignKey( + Email, editable=False, related_name='logs', verbose_name=_('Email address'), on_delete=models.CASCADE + ) date = models.DateTimeField(auto_now_add=True) status = models.PositiveSmallIntegerField(_('Status'), choices=STATUS_CHOICES) exception_type = models.CharField(_('Exception type'), max_length=255, blank=True) @@ -235,8 +252,8 @@ class Log(models.Model): class Meta: app_label = 'post_office' - verbose_name = _("Log") - verbose_name_plural = _("Logs") + verbose_name = _('Log') + verbose_name_plural = _('Logs') def __str__(self): return str(self.date) @@ -251,31 +268,39 @@ class EmailTemplate(models.Model): """ Model to hold template information from db """ + name = models.CharField(_('Name'), max_length=255, help_text=_("e.g: 'welcome_email'")) - description = models.TextField(_('Description'), blank=True, - help_text=_("Description of this template.")) + description = models.TextField(_('Description'), blank=True, help_text=_('Description of this template.')) created = models.DateTimeField(auto_now_add=True) last_updated = models.DateTimeField(auto_now=True) - subject = models.CharField(max_length=255, blank=True, - verbose_name=_("Subject"), validators=[validate_template_syntax]) - content = models.TextField(blank=True, - verbose_name=_("Content"), validators=[validate_template_syntax]) - html_content = models.TextField(blank=True, - verbose_name=_("HTML content"), validators=[validate_template_syntax]) - language = models.CharField(max_length=12, - verbose_name=_("Language"), - help_text=_("Render template in alternative language"), - default='', blank=True) - default_template = models.ForeignKey('self', related_name='translated_templates', - null=True, default=None, verbose_name=_('Default template'), on_delete=models.CASCADE) + subject = models.CharField( + max_length=255, blank=True, verbose_name=_('Subject'), validators=[validate_template_syntax] + ) + content = models.TextField(blank=True, verbose_name=_('Content'), validators=[validate_template_syntax]) + html_content = models.TextField(blank=True, verbose_name=_('HTML content'), validators=[validate_template_syntax]) + language = models.CharField( + max_length=12, + verbose_name=_('Language'), + help_text=_('Render template in alternative language'), + default='', + blank=True, + ) + default_template = models.ForeignKey( + 'self', + related_name='translated_templates', + null=True, + default=None, + verbose_name=_('Default template'), + on_delete=models.CASCADE, + ) objects = EmailTemplateManager() class Meta: app_label = 'post_office' unique_together = ('name', 'language', 'default_template') - verbose_name = _("Email Template") - verbose_name_plural = _("Email Templates") + verbose_name = _('Email Template') + verbose_name_plural = _('Email Templates') ordering = ['name'] def __str__(self): @@ -299,28 +324,26 @@ def get_upload_path(instance, filename): if not instance.name: instance.name = filename # set original filename date = timezone.now().date() - filename = '{name}.{ext}'.format(name=uuid4().hex, - ext=filename.split('.')[-1]) + filename = '{name}.{ext}'.format(name=uuid4().hex, ext=filename.split('.')[-1]) - return os.path.join('post_office_attachments', str(date.year), - str(date.month), str(date.day), filename) + return os.path.join('post_office_attachments', str(date.year), str(date.month), str(date.day), filename) class Attachment(models.Model): """ A model describing an email attachment. """ + file = models.FileField(_('File'), upload_to=get_upload_path) - name = models.CharField(_('Name'), max_length=255, help_text=_("The original filename")) - emails = models.ManyToManyField(Email, related_name='attachments', - verbose_name=_('Emails')) + name = models.CharField(_('Name'), max_length=255, help_text=_('The original filename')) + emails = models.ManyToManyField(Email, related_name='attachments', verbose_name=_('Emails')) mimetype = models.CharField(max_length=255, default='', blank=True) headers = models.JSONField(_('Headers'), blank=True, null=True) class Meta: app_label = 'post_office' - verbose_name = _("Attachment") - verbose_name_plural = _("Attachments") + verbose_name = _('Attachment') + verbose_name_plural = _('Attachments') def __str__(self): return self.name diff --git a/post_office/sanitizer.py b/post_office/sanitizer.py index 887c7d88..71ed49df 100644 --- a/post_office/sanitizer.py +++ b/post_office/sanitizer.py @@ -6,16 +6,27 @@ except ImportError: # if bleach is not installed, render HTML as escaped text to prevent XSS attacks heading = gettext_lazy("Install 'bleach' to render HTML properly.") - clean_html = lambda body: format_html('

{heading}

\n
{body}
', - heading=heading, body=body) + clean_html = lambda body: format_html('

{heading}

\n
{body}
', heading=heading, body=body) else: styles = [ - 'border', 'border-top', 'border-right', 'border-bottom', 'border-left', + 'border', + 'border-top', + 'border-right', + 'border-bottom', + 'border-left', 'border-radius', 'box-shadow', 'height', - 'margin', 'margin-top', 'margin-right', 'margin-bottom', 'margin-left', - 'padding', 'padding-top', 'padding-right', 'padding-bottom', 'padding-left', + 'margin', + 'margin-top', + 'margin-right', + 'margin-bottom', + 'margin-left', + 'padding', + 'padding-top', + 'padding-right', + 'padding-bottom', + 'padding-left', 'width', 'max-width', 'min-width', @@ -67,7 +78,7 @@ 'list-style-position', 'list-style-tyle', ] - tags=[ + tags = [ 'a', 'abbr', 'acronym', @@ -80,7 +91,12 @@ 'em', 'div', 'font', - 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', + 'h1', + 'h2', + 'h3', + 'h4', + 'h5', + 'h6', 'head', 'hr', 'i', @@ -92,11 +108,17 @@ 'pre', 'span', 'strong', - 'table', 'tbody', 'tfoot', 'td', 'th', 'thead', 'tr', + 'table', + 'tbody', + 'tfoot', + 'td', + 'th', + 'thead', + 'tr', 'u', 'ul', ] - attributes={ + attributes = { 'a': ['class', 'href', 'id', 'style', 'target'], 'abbr': ['class', 'id', 'style'], 'acronym': ['class', 'id', 'style'], @@ -126,11 +148,56 @@ 'pre': ['class', 'id', 'style'], 'span': ['class', 'id', 'style'], 'strong': ['class', 'id', 'style'], - 'table': ['class', 'id', 'style', 'align', 'bgcolor', 'border', 'cellpadding', 'cellspacing', 'dir', 'frame', 'rules', 'width'], + 'table': [ + 'class', + 'id', + 'style', + 'align', + 'bgcolor', + 'border', + 'cellpadding', + 'cellspacing', + 'dir', + 'frame', + 'rules', + 'width', + ], 'tbody': ['class', 'id', 'style'], 'tfoot': ['class', 'id', 'style'], - 'td': ['class', 'id', 'style', 'abbr', 'align', 'bgcolor', 'colspan', 'dir', 'height', 'lang', 'rowspan', 'scope', 'style', 'valign', 'width'], - 'th': ['class', 'id', 'style', 'abbr', 'align', 'background', 'bgcolor', 'colspan', 'dir', 'height', 'lang', 'scope', 'style', 'valign', 'width'], + 'td': [ + 'class', + 'id', + 'style', + 'abbr', + 'align', + 'bgcolor', + 'colspan', + 'dir', + 'height', + 'lang', + 'rowspan', + 'scope', + 'style', + 'valign', + 'width', + ], + 'th': [ + 'class', + 'id', + 'style', + 'abbr', + 'align', + 'background', + 'bgcolor', + 'colspan', + 'dir', + 'height', + 'lang', + 'scope', + 'style', + 'valign', + 'width', + ], 'thead': ['class', 'id', 'style'], 'tr': ['class', 'id', 'style', 'align', 'bgcolor', 'dir', 'style', 'valign'], 'u': ['class', 'id', 'style'], @@ -138,24 +205,29 @@ } try: from bleach.css_sanitizer import CSSSanitizer + css_sanitizer = CSSSanitizer( - allowed_css_properties=styles, + allowed_css_properties=styles, + ) + clean_html = lambda body: mark_safe( + bleach.clean( + body, + tags=tags, + attributes=attributes, + strip=True, + strip_comments=True, + css_sanitizer=css_sanitizer, + ) ) - clean_html = lambda body: mark_safe(bleach.clean( - body, - tags=tags, - attributes=attributes, - strip=True, - strip_comments=True, - css_sanitizer=css_sanitizer, - )) except ModuleNotFoundError: # if bleach version is prior to 5.0.0 - clean_html = lambda body: mark_safe(bleach.clean( - body, - tags=tags, - attributes=attributes, - strip=True, - strip_comments=True, - styles=styles, - )) + clean_html = lambda body: mark_safe( + bleach.clean( + body, + tags=tags, + attributes=attributes, + strip=True, + strip_comments=True, + styles=styles, + ) + ) diff --git a/post_office/settings.py b/post_office/settings.py index 19aa7617..25b4c3c2 100644 --- a/post_office/settings.py +++ b/post_office/settings.py @@ -16,7 +16,7 @@ def get_backend(alias='default'): def get_available_backends(): - """ Returns a dictionary of defined backend classes. For example: + """Returns a dictionary of defined backend classes. For example: { 'default': 'django.core.mail.backends.smtp.EmailBackend', 'locmem': 'django.core.mail.backends.locmem.EmailBackend', @@ -33,16 +33,13 @@ def get_available_backends(): # } backend = get_config().get('EMAIL_BACKEND') if backend: - warnings.warn('Please use the new POST_OFFICE["BACKENDS"] settings', - DeprecationWarning) + warnings.warn('Please use the new POST_OFFICE["BACKENDS"] settings', DeprecationWarning) backends['default'] = backend return backends # Fall back to Django's EMAIL_BACKEND definition - backends['default'] = getattr( - settings, 'EMAIL_BACKEND', - 'django.core.mail.backends.smtp.EmailBackend') + backends['default'] = getattr(settings, 'EMAIL_BACKEND', 'django.core.mail.backends.smtp.EmailBackend') # If EMAIL_BACKEND is set to use PostOfficeBackend # and POST_OFFICE_BACKEND is not set, fall back to SMTP @@ -54,12 +51,12 @@ def get_available_backends(): def get_cache_backend(): if hasattr(settings, 'CACHES'): - if "post_office" in settings.CACHES: - return caches["post_office"] + if 'post_office' in settings.CACHES: + return caches['post_office'] else: # Sometimes this raises InvalidCacheBackendError, which is ok too try: - return caches["default"] + return caches['default'] except InvalidCacheBackendError: pass return None @@ -133,6 +130,5 @@ def get_batch_delivery_timeout(): return get_config().get('BATCH_DELIVERY_TIMEOUT', 180) -CONTEXT_FIELD_CLASS = get_config().get('CONTEXT_FIELD_CLASS', - 'django.db.models.JSONField') +CONTEXT_FIELD_CLASS = get_config().get('CONTEXT_FIELD_CLASS', 'django.db.models.JSONField') context_field_class = import_string(CONTEXT_FIELD_CLASS) diff --git a/post_office/tasks.py b/post_office/tasks.py index d0594c3c..a931cce5 100644 --- a/post_office/tasks.py +++ b/post_office/tasks.py @@ -4,6 +4,7 @@ example by other task queue systems such as Huey, which use the same pattern of auto-discovering tasks in "tasks" submodules. """ + import datetime from django.utils.timezone import now @@ -19,12 +20,14 @@ else: raise NotImplementedError() except (ImportError, NotImplementedError): + def queued_mail_handler(sender, **kwargs): """ To be called by :func:`post_office.signals.email_queued.send()` for triggering asynchronous mail delivery – if provided by an external queue, such as Celery. """ else: + @shared_task(ignore_result=True) def send_queued_mail(*args, **kwargs): """ diff --git a/post_office/template/backends/post_office.py b/post_office/template/backends/post_office.py index 624a4218..5fd4ae2d 100644 --- a/post_office/template/backends/post_office.py +++ b/post_office/template/backends/post_office.py @@ -12,7 +12,7 @@ def __init__(self, template, backend): super().__init__(template, backend) def attach_related(self, email_message): - assert isinstance(email_message, EmailMultiAlternatives), "Parameter must be of type EmailMultiAlternatives" + assert isinstance(email_message, EmailMultiAlternatives), 'Parameter must be of type EmailMultiAlternatives' email_message.mixed_subtype = 'related' for attachment in self.template._attached_images: email_message.attach(attachment) @@ -23,6 +23,7 @@ class PostOfficeTemplates(BaseEngine): Customized Template Engine which keeps track on referenced images and stores them as attachments to be used in multipart email messages. """ + app_dirname = 'templates' def __init__(self, params): @@ -32,9 +33,7 @@ def __init__(self, params): options.setdefault('debug', settings.DEBUG) options.setdefault( 'file_charset', - settings.FILE_CHARSET - if settings.is_overridden('FILE_CHARSET') - else 'utf-8', + settings.FILE_CHARSET if settings.is_overridden('FILE_CHARSET') else 'utf-8', ) libraries = options.get('libraries', {}) options['libraries'] = self.get_templatetag_libraries(libraries) diff --git a/post_office/templatetags/post_office.py b/post_office/templatetags/post_office.py index 3b75c9df..f5068188 100644 --- a/post_office/templatetags/post_office.py +++ b/post_office/templatetags/post_office.py @@ -13,8 +13,9 @@ @register.simple_tag(takes_context=True) def inline_image(context, file): - assert hasattr(context.template, '_attached_images'), \ - "You must use template engine 'post_office' when rendering images using templatetag 'inline_image'." + assert hasattr( + context.template, '_attached_images' + ), "You must use template engine 'post_office' when rendering images using templatetag 'inline_image'." if isinstance(file, ImageFile): fileobj = file elif os.path.isabs(file) and os.path.exists(file): @@ -23,7 +24,7 @@ def inline_image(context, file): try: absfilename = finders.find(file) if absfilename is None: - raise FileNotFoundError(f"No such file: {file}") + raise FileNotFoundError(f'No such file: {file}') except Exception: if settings.DEBUG: raise diff --git a/post_office/test_settings.py b/post_office/test_settings.py index d1da56c4..ca78db6a 100644 --- a/post_office/test_settings.py +++ b/post_office/test_settings.py @@ -1,10 +1,11 @@ import os import platform -if platform.system() in ["Darwin"]: +if platform.system() in ['Darwin']: from multiprocessing import set_start_method + # required since Python-3.8. See #319 - set_start_method("fork") + set_start_method('fork') BASE_DIR = os.path.dirname(os.path.abspath(__file__)) @@ -29,7 +30,7 @@ 'BACKEND': 'django.core.cache.backends.locmem.LocMemCache', 'TIMEOUT': 36000, 'KEY_PREFIX': 'post-office', - } + }, } POST_OFFICE = { @@ -87,7 +88,8 @@ 'django.contrib.messages.context_processors.messages', ], }, - }, { + }, + { 'BACKEND': 'post_office.template.backends.post_office.PostOfficeTemplates', 'APP_DIRS': True, 'DIRS': [os.path.join(BASE_DIR, 'tests/templates')], @@ -101,11 +103,10 @@ 'django.template.context_processors.tz', 'django.template.context_processors.request', ] - } - } + }, + }, ] STATICFILES_DIRS = [os.path.join(BASE_DIR, 'tests/static')] DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' - diff --git a/post_office/tests/test_backends.py b/post_office/tests/test_backends.py index 32618078..e19b2116 100644 --- a/post_office/tests/test_backends.py +++ b/post_office/tests/test_backends.py @@ -24,7 +24,6 @@ def send_messages(self, email_messages): class BackendTest(TestCase): - @override_settings(EMAIL_BACKEND='post_office.EmailBackend') def test_email_backend(self): """ @@ -37,9 +36,7 @@ def test_email_backend(self): self.assertEqual(email.priority, PRIORITY.medium) def test_email_backend_setting(self): - """ - - """ + """ """ old_email_backend = getattr(settings, 'EMAIL_BACKEND', None) old_post_office_backend = getattr(settings, 'POST_OFFICE_BACKEND', None) if hasattr(settings, 'EMAIL_BACKEND'): @@ -72,9 +69,8 @@ def test_sending_html_email(self): """ "text/html" attachments to Email should be persisted into the database """ - message = EmailMultiAlternatives('subject', 'body', 'from@example.com', - ['recipient@example.com']) - message.attach_alternative('html', "text/html") + message = EmailMultiAlternatives('subject', 'body', 'from@example.com', ['recipient@example.com']) + message.attach_alternative('html', 'text/html') message.send() email = Email.objects.latest('id') self.assertEqual(email.html_message, 'html') @@ -84,9 +80,9 @@ def test_headers_sent(self): """ Test that headers are correctly set on the outgoing emails. """ - message = EmailMessage('subject', 'body', 'from@example.com', - ['recipient@example.com'], - headers={'Reply-To': 'reply@example.com'}) + message = EmailMessage( + 'subject', 'body', 'from@example.com', ['recipient@example.com'], headers={'Reply-To': 'reply@example.com'} + ) message.send() email = Email.objects.latest('id') self.assertEqual(email.headers, {'Reply-To': 'reply@example.com'}) @@ -97,9 +93,15 @@ def test_reply_to_added_as_header(self): Test that 'Reply-To' headers are correctly set on the outgoing emails, when EmailMessage property reply_to is set. """ - message = EmailMessage('subject', 'body', 'from@example.com', - ['recipient@example.com'], - reply_to=['replyto@example.com', ],) + message = EmailMessage( + 'subject', + 'body', + 'from@example.com', + ['recipient@example.com'], + reply_to=[ + 'replyto@example.com', + ], + ) message.send() email = Email.objects.latest('id') self.assertEqual(email.headers, {'Reply-To': 'replyto@example.com'}) @@ -112,18 +114,21 @@ def test_reply_to_favors_explict_header(self): Then the explicit header value is favored over the message property reply_to, adopting the behaviour of message() in django.core.mail.message.EmailMessage. """ - message = EmailMessage('subject', 'body', 'from@example.com', - ['recipient@example.com'], - reply_to=['replyto-from-property@example.com'], - headers={'Reply-To': 'replyto-from-header@example.com'}) + message = EmailMessage( + 'subject', + 'body', + 'from@example.com', + ['recipient@example.com'], + reply_to=['replyto-from-property@example.com'], + headers={'Reply-To': 'replyto-from-header@example.com'}, + ) message.send() email = Email.objects.latest('id') self.assertEqual(email.headers, {'Reply-To': 'replyto-from-header@example.com'}) @override_settings(EMAIL_BACKEND='post_office.EmailBackend') def test_backend_attachments(self): - message = EmailMessage('subject', 'body', 'from@example.com', - ['recipient@example.com']) + message = EmailMessage('subject', 'body', 'from@example.com', ['recipient@example.com']) message.attach('attachment.txt', b'attachment content') message.send() @@ -135,8 +140,7 @@ def test_backend_attachments(self): @override_settings(EMAIL_BACKEND='post_office.EmailBackend') def test_backend_image_attachments(self): - message = EmailMessage('subject', 'body', 'from@example.com', - ['recipient@example.com']) + message = EmailMessage('subject', 'body', 'from@example.com', ['recipient@example.com']) filename = os.path.join(os.path.dirname(__file__), 'static/dummy.png') fileobj = File(open(filename, 'rb'), name='dummy.png') @@ -157,8 +161,8 @@ def test_backend_image_attachments(self): EMAIL_BACKEND='post_office.EmailBackend', POST_OFFICE={ 'DEFAULT_PRIORITY': 'now', - 'BACKENDS': {'default': 'django.core.mail.backends.dummy.EmailBackend'} - } + 'BACKENDS': {'default': 'django.core.mail.backends.dummy.EmailBackend'}, + }, ) def test_default_priority_now(self): # If DEFAULT_PRIORITY is "now", mails should be sent right away @@ -171,8 +175,8 @@ def test_default_priority_now(self): EMAIL_BACKEND='post_office.EmailBackend', POST_OFFICE={ 'DEFAULT_PRIORITY': 'medium', - 'BACKENDS': {'default': 'django.core.mail.backends.dummy.EmailBackend'} - } + 'BACKENDS': {'default': 'django.core.mail.backends.dummy.EmailBackend'}, + }, ) @mock.patch('post_office.signals.email_queued.send') def test_email_queued_signal(self, mock): diff --git a/post_office/tests/test_cache.py b/post_office/tests/test_cache.py index bde6dbd1..9356e5cd 100644 --- a/post_office/tests/test_cache.py +++ b/post_office/tests/test_cache.py @@ -6,7 +6,6 @@ class CacheTest(TestCase): - def test_get_backend_settings(self): """Test basic get backend function and its settings""" # Sanity check @@ -14,7 +13,7 @@ def test_get_backend_settings(self): self.assertTrue(get_cache_backend()) # If no post office key is defined, it should return default - del(settings.CACHES['post_office']) + del settings.CACHES['post_office'] self.assertTrue(get_cache_backend()) # If no caches key in settings, it should return None @@ -23,14 +22,14 @@ def test_get_backend_settings(self): def test_get_cache_key(self): """ - Test for converting names to cache key + Test for converting names to cache key """ self.assertEqual('post_office:template:test', cache.get_cache_key('test')) self.assertEqual('post_office:template:test-slugify', cache.get_cache_key('test slugify')) def test_basic_cache_operations(self): """ - Test basic cache operations + Test basic cache operations """ # clean test cache cache.cache_backend.clear() diff --git a/post_office/tests/test_commands.py b/post_office/tests/test_commands.py index 49486064..e5322ec5 100644 --- a/post_office/tests/test_commands.py +++ b/post_office/tests/test_commands.py @@ -11,20 +11,15 @@ class CommandTest(TestCase): - def test_cleanup_mail_with_orphaned_attachments(self): self.assertEqual(Email.objects.count(), 0) - email = Email.objects.create(to=['to@example.com'], - from_email='from@example.com', - subject='Subject') + email = Email.objects.create(to=['to@example.com'], from_email='from@example.com', subject='Subject') email.created = now() - datetime.timedelta(31) email.save() attachment = Attachment() - attachment.file.save( - 'test.txt', content=ContentFile('test file content'), save=True - ) + attachment.file.save('test.txt', content=ContentFile('test file content'), save=True) email.attachments.add(attachment) attachment_path = attachment.file.name @@ -43,16 +38,12 @@ def test_cleanup_mail_with_orphaned_attachments(self): # Check if the email attachment's actual file have been deleted Email.objects.all().delete() - email = Email.objects.create(to=['to@example.com'], - from_email='from@example.com', - subject='Subject') + email = Email.objects.create(to=['to@example.com'], from_email='from@example.com', subject='Subject') email.created = now() - datetime.timedelta(31) email.save() attachment = Attachment() - attachment.file.save( - 'test.txt', content=ContentFile('test file content'), save=True - ) + attachment.file.save('test.txt', content=ContentFile('test file content'), save=True) email.attachments.add(attachment) attachment_path = attachment.file.name @@ -64,7 +55,6 @@ def test_cleanup_mail_with_orphaned_attachments(self): self.assertEqual(Email.objects.count(), 0) self.assertEqual(Attachment.objects.count(), 0) - def test_cleanup_mail(self): """ The ``cleanup_mail`` command deletes mails older than a specified @@ -73,8 +63,7 @@ def test_cleanup_mail(self): self.assertEqual(Email.objects.count(), 0) # The command shouldn't delete today's email - email = Email.objects.create(from_email='from@example.com', - to=['to@example.com']) + email = Email.objects.create(from_email='from@example.com', to=['to@example.com']) call_command('cleanup_mail', days=30) self.assertEqual(Email.objects.count(), 1) @@ -88,7 +77,7 @@ def test_cleanup_mail(self): 'BACKENDS': { 'default': 'django.core.mail.backends.dummy.EmailBackend', }, - 'BATCH_SIZE': 1 + 'BATCH_SIZE': 1, } @override_settings(POST_OFFICE=TEST_SETTINGS) @@ -100,10 +89,8 @@ def test_send_queued_mail(self): # Make sure that send_queued_mail with empty queue does not raise error call_command('send_queued_mail', processes=1) - Email.objects.create(from_email='from@example.com', - to=['to@example.com'], status=STATUS.queued) - Email.objects.create(from_email='from@example.com', - to=['to@example.com'], status=STATUS.queued) + Email.objects.create(from_email='from@example.com', to=['to@example.com'], status=STATUS.queued) + Email.objects.create(from_email='from@example.com', to=['to@example.com'], status=STATUS.queued) call_command('send_queued_mail', processes=1) self.assertEqual(Email.objects.filter(status=STATUS.sent).count(), 2) self.assertEqual(Email.objects.filter(status=STATUS.queued).count(), 0) @@ -112,18 +99,15 @@ def test_successful_deliveries_logging(self): """ Successful deliveries are only logged when log_level is 2. """ - email = Email.objects.create(from_email='from@example.com', - to=['to@example.com'], status=STATUS.queued) + email = Email.objects.create(from_email='from@example.com', to=['to@example.com'], status=STATUS.queued) call_command('send_queued_mail', log_level=0) self.assertEqual(email.logs.count(), 0) - email = Email.objects.create(from_email='from@example.com', - to=['to@example.com'], status=STATUS.queued) + email = Email.objects.create(from_email='from@example.com', to=['to@example.com'], status=STATUS.queued) call_command('send_queued_mail', log_level=1) self.assertEqual(email.logs.count(), 0) - email = Email.objects.create(from_email='from@example.com', - to=['to@example.com'], status=STATUS.queued) + email = Email.objects.create(from_email='from@example.com', to=['to@example.com'], status=STATUS.queued) call_command('send_queued_mail', log_level=2) self.assertEqual(email.logs.count(), 1) @@ -131,20 +115,20 @@ def test_failed_deliveries_logging(self): """ Failed deliveries are logged when log_level is 1 and 2. """ - email = Email.objects.create(from_email='from@example.com', - to=['to@example.com'], status=STATUS.queued, - backend_alias='error') + email = Email.objects.create( + from_email='from@example.com', to=['to@example.com'], status=STATUS.queued, backend_alias='error' + ) call_command('send_queued_mail', log_level=0) self.assertEqual(email.logs.count(), 0) - email = Email.objects.create(from_email='from@example.com', - to=['to@example.com'], status=STATUS.queued, - backend_alias='error') + email = Email.objects.create( + from_email='from@example.com', to=['to@example.com'], status=STATUS.queued, backend_alias='error' + ) call_command('send_queued_mail', log_level=1) self.assertEqual(email.logs.count(), 1) - email = Email.objects.create(from_email='from@example.com', - to=['to@example.com'], status=STATUS.queued, - backend_alias='error') + email = Email.objects.create( + from_email='from@example.com', to=['to@example.com'], status=STATUS.queued, backend_alias='error' + ) call_command('send_queued_mail', log_level=2) self.assertEqual(email.logs.count(), 1) diff --git a/post_office/tests/test_connections.py b/post_office/tests/test_connections.py index aecae757..3ed7110f 100644 --- a/post_office/tests/test_connections.py +++ b/post_office/tests/test_connections.py @@ -6,7 +6,6 @@ class ConnectionTest(TestCase): - def test_get_connection(self): # Ensure ConnectionHandler returns the right connection self.assertTrue(isinstance(connections['error'], ErrorRaisingBackend)) diff --git a/post_office/tests/test_forms.py b/post_office/tests/test_forms.py index fec63097..29a3d825 100644 --- a/post_office/tests/test_forms.py +++ b/post_office/tests/test_forms.py @@ -11,10 +11,9 @@ class EmailTemplateFormTest(TestCase): def setUp(self) -> None: - self.form_set = formset_factory(EmailTemplateAdminForm, - extra=2) + self.form_set = formset_factory(EmailTemplateAdminForm, extra=2) self.client = Client() - self.user = User.objects.create_superuser(username='testuser', password='abc123456', email="testemail@test.com") + self.user = User.objects.create_superuser(username='testuser', password='abc123456', email='testemail@test.com') self.client.force_login(self.user) def test_can_create_a_email_template_with_the_same_attributes(self): @@ -23,26 +22,42 @@ def test_can_create_a_email_template_with_the_same_attributes(self): 'form-INITIAL_FORMS': '0', 'form-MAX_NUM_FORMS': '', 'name': 'Test', - 'email_photos-TOTAL_FORMS': '1', 'email_photos-INITIAL_FORMS': '0', - 'email_photos-MIN_NUM_FORMS': '0', 'email_photos-MAX_NUM_FORMS': '1', 'email_photos-0-id': '', - 'email_photos-0-email_template': '', 'email_photos-0-photo': '', 'email_photos-__prefix__-id': '', - 'email_photos-__prefix__-email_template': '', 'email_photos-__prefix__-photo': '', - 'translated_templates-TOTAL_FORMS': '2', 'translated_templates-INITIAL_FORMS': '0', - 'translated_templates-MIN_NUM_FORMS': '0', 'translated_templates-MAX_NUM_FORMS': '2', - 'translated_templates-0-language': 'es', 'translated_templates-0-subject': '', - 'translated_templates-0-content': '', 'translated_templates-0-html_content': '', - 'translated_templates-0-id': '', 'translated_templates-0-default_template': '', - 'translated_templates-1-language': 'es', 'translated_templates-1-subject': '', - 'translated_templates-1-content': '', 'translated_templates-1-html_content': '', - 'translated_templates-1-id': '', 'translated_templates-1-default_template': '', - 'translated_templates-__prefix__-language': 'es', 'translated_templates-__prefix__-subject': '', - 'translated_templates-__prefix__-content': '', 'translated_templates-__prefix__-html_content': '', - 'translated_templates-__prefix__-id': '', 'translated_templates-__prefix__-default_template': '', - '_save': 'Save' + 'email_photos-TOTAL_FORMS': '1', + 'email_photos-INITIAL_FORMS': '0', + 'email_photos-MIN_NUM_FORMS': '0', + 'email_photos-MAX_NUM_FORMS': '1', + 'email_photos-0-id': '', + 'email_photos-0-email_template': '', + 'email_photos-0-photo': '', + 'email_photos-__prefix__-id': '', + 'email_photos-__prefix__-email_template': '', + 'email_photos-__prefix__-photo': '', + 'translated_templates-TOTAL_FORMS': '2', + 'translated_templates-INITIAL_FORMS': '0', + 'translated_templates-MIN_NUM_FORMS': '0', + 'translated_templates-MAX_NUM_FORMS': '2', + 'translated_templates-0-language': 'es', + 'translated_templates-0-subject': '', + 'translated_templates-0-content': '', + 'translated_templates-0-html_content': '', + 'translated_templates-0-id': '', + 'translated_templates-0-default_template': '', + 'translated_templates-1-language': 'es', + 'translated_templates-1-subject': '', + 'translated_templates-1-content': '', + 'translated_templates-1-html_content': '', + 'translated_templates-1-id': '', + 'translated_templates-1-default_template': '', + 'translated_templates-__prefix__-language': 'es', + 'translated_templates-__prefix__-subject': '', + 'translated_templates-__prefix__-content': '', + 'translated_templates-__prefix__-html_content': '', + 'translated_templates-__prefix__-id': '', + 'translated_templates-__prefix__-default_template': '', + '_save': 'Save', } add_template_url = reverse('admin:post_office_emailtemplate_add') response = self.client.post(add_template_url, email_template, follow=True) - self.assertContains(response, "Duplicate template for language 'Spanish'.", - html=True) + self.assertContains(response, 'Duplicate template for language 'Spanish'.', html=True) diff --git a/post_office/tests/test_html_email.py b/post_office/tests/test_html_email.py index 13334350..507a4fbd 100644 --- a/post_office/tests/test_html_email.py +++ b/post_office/tests/test_html_email.py @@ -17,22 +17,24 @@ class HTMLMailTest(TestCase): - def test_text(self): template = get_template('hello.html', using='post_office') self.assertIsInstance(template.backend, PostOfficeTemplates) - context = {'foo': "Bar"} + context = {'foo': 'Bar'} content = template.render(context) self.assertHTMLEqual(content, '

Bar

') def test_html(self): template = get_template('image.html', using='post_office') body = template.render({'imgsrc': 'dummy.png'}) - self.assertHTMLEqual(body, """ + self.assertHTMLEqual( + body, + """

Testing image attachments

-""") - subject = "[Django Post-Office unit tests] attached image" +""", + ) + subject = '[Django Post-Office unit tests] attached image' msg = EmailMultiAlternatives(subject, body, to=['john@example.com']) template.attach_related(msg) msg.content_subtype = 'html' @@ -53,9 +55,9 @@ def test_html(self): self.assertEqual(part['Content-ID'], '') def test_mixed(self): - body = "Testing mixed text and html attachments" + body = 'Testing mixed text and html attachments' html, attached_images = render_to_string('image.html', {'imgsrc': 'dummy.png'}, using='post_office') - subject = "[django-SHOP unit tests] attached image" + subject = '[django-SHOP unit tests] attached image' msg = EmailMultiAlternatives(subject, body, to=['john@example.com']) msg.attach_alternative(html, 'text/html') for attachment in attached_images: @@ -85,11 +87,14 @@ def test_image(self): imagefile = ImageFile(open(filename, 'rb'), name=relfilename) template = get_template('image.html', using='post_office') body = template.render({'imgsrc': imagefile}) - self.assertHTMLEqual(body, """ + self.assertHTMLEqual( + body, + """

Testing image attachments

-""") - subject = "[Django Post-Office unit tests] attached image" +""", + ) + subject = '[Django Post-Office unit tests] attached image' msg = EmailMultiAlternatives(subject, body, to=['john@example.com']) template.attach_related(msg) # this message can be send by email @@ -107,23 +112,31 @@ def test_image(self): self.assertEqual(part.get_filename(), 'f5c66340b8af7dc946cd25d84fdf8c90') self.assertEqual(part['Content-ID'], '') - @override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend', POST_OFFICE={ - 'BACKENDS': {'locmem': 'django.core.mail.backends.locmem.EmailBackend'}, - 'TEMPLATE_ENGINE': 'post_office', - }) + @override_settings( + EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend', + POST_OFFICE={ + 'BACKENDS': {'locmem': 'django.core.mail.backends.locmem.EmailBackend'}, + 'TEMPLATE_ENGINE': 'post_office', + }, + ) def test_send_with_html_template(self): template = EmailTemplate.objects.create( - name="Test Inlined Images", - subject="[django-SHOP unit tests] attached image", + name='Test Inlined Images', + subject='[django-SHOP unit tests] attached image', html_content=""" {% load post_office %}

Testing image attachments

-""" +""", ) filename = os.path.join(os.path.dirname(__file__), 'static/dummy.png') context = {'imgsrc': filename} - queued_mail = send(recipients=['to@example.com'], sender='from@example.com', - template=template, context=context, render_on_delivery=True) + queued_mail = send( + recipients=['to@example.com'], + sender='from@example.com', + template=template, + context=context, + render_on_delivery=True, + ) queued_mail = Email.objects.get(id=queued_mail.id) send_queued() self.assertEqual(Email.objects.get(id=queued_mail.id).status, STATUS.sent) @@ -132,16 +145,16 @@ def test_send_with_html_template(self): class EmailAdminTest(TestCase): def setUp(self) -> None: self.client = Client() - self.user = get_user_model().objects.create_superuser(username='testuser', - password='secret', - email="test@example.com") + self.user = get_user_model().objects.create_superuser( + username='testuser', password='secret', email='test@example.com' + ) self.client.force_login(self.user) @override_settings(EMAIL_BACKEND='post_office.EmailBackend') def test_email_change_view(self): template = get_template('image.html', using='post_office') body = template.render({'imgsrc': 'dummy.png'}) - subject = "[Django Post-Office unit tests] attached image" + subject = '[Django Post-Office unit tests] attached image' msg = EmailMultiAlternatives(subject, body, to=['john@example.com']) msg.content_subtype = 'html' template.attach_related(msg) @@ -160,14 +173,15 @@ def test_email_change_view(self): content_id = part['Content-Id'][1:33] email_change_url = reverse('admin:post_office_email_change', args=(email.pk,)) response = self.client.get(email_change_url, follow=True) - self.assertContains(response, "[Django Post-Office unit tests] attached image") + self.assertContains(response, '[Django Post-Office unit tests] attached image') email_image_url = reverse('admin:post_office_email_image', kwargs={'pk': email.pk, 'content_id': content_id}) try: import bleach - self.assertContains(response, "

Testing image attachments

") + + self.assertContains(response, '

Testing image attachments

') self.assertContains(response, f'', email.message_id)) def test_send_many(self): - """Test send_many creates the right emails """ + """Test send_many creates the right emails""" kwargs_list = [ {'sender': 'from@example.com', 'recipients': ['a@example.com']}, {'sender': 'from@example.com', 'recipients': ['b@example.com']}, @@ -246,9 +249,13 @@ def test_send_with_attachments(self): 'attachment_file1.txt': ContentFile('content'), 'attachment_file2.txt': ContentFile('content'), } - email = send(recipients=['a@example.com', 'b@example.com'], - sender='from@example.com', message='message', - subject='subject', attachments=attachments) + email = send( + recipients=['a@example.com', 'b@example.com'], + sender='from@example.com', + message='message', + subject='subject', + attachments=attachments, + ) self.assertTrue(email.pk) self.assertEqual(email.attachments.count(), 2) @@ -259,23 +266,19 @@ def test_send_with_render_on_delivery(self): fields being saved """ template = EmailTemplate.objects.create( - subject='Subject {{ name }}', - content='Content {{ name }}', - html_content='HTML {{ name }}' + subject='Subject {{ name }}', content='Content {{ name }}', html_content='HTML {{ name }}' ) context = {'name': 'test'} - email = send(recipients=['a@example.com', 'b@example.com'], - template=template, context=context, - render_on_delivery=True) + email = send( + recipients=['a@example.com', 'b@example.com'], template=template, context=context, render_on_delivery=True + ) self.assertEqual(email.subject, '') self.assertEqual(email.message, '') self.assertEqual(email.html_message, '') self.assertEqual(email.template, template) # context shouldn't be persisted when render_on_delivery = False - email = send(recipients=['a@example.com'], - template=template, context=context, - render_on_delivery=False) + email = send(recipients=['a@example.com'], template=template, context=context, render_on_delivery=False) self.assertEqual(email.context, None) def test_send_with_attachments_multiple_recipients(self): @@ -284,9 +287,13 @@ def test_send_with_attachments_multiple_recipients(self): 'attachment_file1.txt': ContentFile('content'), 'attachment_file2.txt': ContentFile('content'), } - email = send(recipients=['a@example.com', 'b@example.com'], - sender='from@example.com', message='message', - subject='subject', attachments=attachments) + email = send( + recipients=['a@example.com', 'b@example.com'], + sender='from@example.com', + message='message', + subject='subject', + attachments=attachments, + ) self.assertEqual(email.attachments.count(), 2) self.assertEqual(Attachment.objects.count(), 2) @@ -296,14 +303,15 @@ def test_create_with_template(self): won't be rendered, context also won't be saved.""" template = EmailTemplate.objects.create( - subject='Subject {{ name }}', - content='Content {{ name }}', - html_content='HTML {{ name }}' + subject='Subject {{ name }}', content='Content {{ name }}', html_content='HTML {{ name }}' ) context = {'name': 'test'} email = create( - sender='from@example.com', recipients=['to@example.com'], - template=template, context=context, render_on_delivery=True + sender='from@example.com', + recipients=['to@example.com'], + template=template, + context=context, + render_on_delivery=True, ) self.assertEqual(email.subject, '') self.assertEqual(email.message, '') @@ -316,15 +324,10 @@ def test_create_with_template_and_empty_context(self): will be rendered, context won't be saved.""" template = EmailTemplate.objects.create( - subject='Subject {% now "Y" %}', - content='Content {% now "Y" %}', - html_content='HTML {% now "Y" %}' + subject='Subject {% now "Y" %}', content='Content {% now "Y" %}', html_content='HTML {% now "Y" %}' ) context = None - email = create( - sender='from@example.com', recipients=['to@example.com'], - template=template, context=context - ) + email = create(sender='from@example.com', recipients=['to@example.com'], template=template, context=context) today = timezone.datetime.today() current_year = today.year self.assertEqual(email.subject, 'Subject %d' % current_year) @@ -336,19 +339,26 @@ def test_create_with_template_and_empty_context(self): def test_backend_alias(self): """Test backend_alias field is properly set.""" - email = send(recipients=['a@example.com'], - sender='from@example.com', message='message', - subject='subject') + email = send(recipients=['a@example.com'], sender='from@example.com', message='message', subject='subject') self.assertEqual(email.backend_alias, '') - email = send(recipients=['a@example.com'], - sender='from@example.com', message='message', - subject='subject', backend='locmem') + email = send( + recipients=['a@example.com'], + sender='from@example.com', + message='message', + subject='subject', + backend='locmem', + ) self.assertEqual(email.backend_alias, 'locmem') with self.assertRaises(ValueError): - send(recipients=['a@example.com'], sender='from@example.com', - message='message', subject='subject', backend='foo') + send( + recipients=['a@example.com'], + sender='from@example.com', + message='message', + subject='subject', + backend='foo', + ) @override_settings(LANGUAGES=(('en', 'English'), ('ru', 'Russian'))) def test_send_with_template(self): @@ -356,22 +366,19 @@ def test_send_with_template(self): will be rendered, context won't be saved.""" template = EmailTemplate.objects.create( - subject='Subject {{ name }}', - content='Content {{ name }}', - html_content='HTML {{ name }}' + subject='Subject {{ name }}', content='Content {{ name }}', html_content='HTML {{ name }}' ) russian_template = EmailTemplate( default_template=template, language='ru', subject='предмет {{ name }}', content='содержание {{ name }}', - html_content='HTML {{ name }}' + html_content='HTML {{ name }}', ) russian_template.save() context = {'name': 'test'} - email = send(recipients=['to@example.com'], sender='from@example.com', - template=template, context=context) + email = send(recipients=['to@example.com'], sender='from@example.com', template=template, context=context) email = Email.objects.get(id=email.id) self.assertEqual(email.subject, 'Subject test') self.assertEqual(email.message, 'Content test') @@ -380,8 +387,9 @@ def test_send_with_template(self): self.assertIsNotNone(email.template) # check, if we use the Russian version - email = send(recipients=['to@example.com'], sender='from@example.com', - template=russian_template, context=context) + email = send( + recipients=['to@example.com'], sender='from@example.com', template=russian_template, context=context + ) email = Email.objects.get(id=email.id) self.assertEqual(email.subject, 'предмет test') self.assertEqual(email.message, 'содержание test') @@ -390,24 +398,29 @@ def test_send_with_template(self): self.assertIsNotNone(email.template) # Check that send picks template with the right language - email = send(recipients=['to@example.com'], sender='from@example.com', - template=template, context=context, language='ru') + email = send( + recipients=['to@example.com'], sender='from@example.com', template=template, context=context, language='ru' + ) email = Email.objects.get(id=email.id) self.assertEqual(email.subject, 'предмет test') - email = send(recipients=['to@example.com'], sender='from@example.com', - template=template, context=context, language='ru', - render_on_delivery=True) + email = send( + recipients=['to@example.com'], + sender='from@example.com', + template=template, + context=context, + language='ru', + render_on_delivery=True, + ) self.assertEqual(email.template.language, 'ru') def test_send_bulk_with_faulty_template(self): template = EmailTemplate.objects.create( - subject='{% if foo %}Subject {{ name }}', - content='Content {{ name }}', - html_content='HTML {{ name }}' + subject='{% if foo %}Subject {{ name }}', content='Content {{ name }}', html_content='HTML {{ name }}' + ) + email = Email.objects.create( + to='to@example.com', from_email='from@example.com', template=template, status=STATUS.queued ) - email = Email.objects.create(to='to@example.com', from_email='from@example.com', - template=template, status=STATUS.queued) _send_bulk([email], uses_multiprocessing=False) email = Email.objects.get(id=email.id) self.assertEqual(email.status, STATUS.sent) @@ -419,8 +432,9 @@ def test_retry_failed(self): # attempt to send email for the first time with patch('django.utils.timezone.now', side_effect=lambda: timezone.datetime(2020, 5, 18, 8, 0, 0)): - email = create('from@example.com', recipients=['to@example.com'], subject='subject', message='message', - backend='error') + email = create( + 'from@example.com', recipients=['to@example.com'], subject='subject', message='message', backend='error' + ) self.assertIsNotNone(email.pk) self.assertEqual(email.created, timezone.datetime(2020, 5, 18, 8, 0, 0)) self.assertEqual(email.status, STATUS.queued) @@ -462,21 +476,30 @@ def test_retry_failed(self): @override_settings(USE_TZ=True) def test_expired(self): tzinfo = ZoneInfo('Asia/Jakarta') - email = create('from@example.com', recipients=['to@example.com'], subject='subject', message='message', - expires_at=timezone.datetime(2020, 5, 18, 9, 0, 1, tzinfo=tzinfo)) + email = create( + 'from@example.com', + recipients=['to@example.com'], + subject='subject', + message='message', + expires_at=timezone.datetime(2020, 5, 18, 9, 0, 1, tzinfo=tzinfo), + ) self.assertEqual(email.expires_at, timezone.datetime(2020, 5, 18, 9, 0, 1, tzinfo=tzinfo)) msg = email.prepare_email_message() self.assertEqual(msg.extra_headers['Expires'], 'Mon, 18 May 09:00:01 +0700') # check that email is not sent after its expire_at date - with patch('django.utils.timezone.now', side_effect=lambda: timezone.datetime(2020, 5, 18, 9, 0, 2, tzinfo=tzinfo)): + with patch( + 'django.utils.timezone.now', side_effect=lambda: timezone.datetime(2020, 5, 18, 9, 0, 2, tzinfo=tzinfo) + ): self.assertEqual(email.status, STATUS.queued) result = send_queued() self.assertTupleEqual(result, (0, 0, 0)) email.refresh_from_db() # check that email is sent before its expire_at date - with patch('django.utils.timezone.now', side_effect=lambda: timezone.datetime(2020, 5, 18, 9, 0, 0, tzinfo=tzinfo)): + with patch( + 'django.utils.timezone.now', side_effect=lambda: timezone.datetime(2020, 5, 18, 9, 0, 0, tzinfo=tzinfo) + ): self.assertEqual(email.status, STATUS.queued) result = send_queued() self.assertTupleEqual(result, (1, 0, 0)) @@ -485,18 +508,27 @@ def test_expired(self): def test_invalid_expired(self): with self.assertRaises(ValidationError): - create('from@example.com', recipients=['to@example.com'], subject='subject', - message='message', - scheduled_time=timezone.datetime(2020, 5, 18, 9, 0, 1), - expires_at=timezone.datetime(2020, 5, 18, 9, 0, 0)) + create( + 'from@example.com', + recipients=['to@example.com'], + subject='subject', + message='message', + scheduled_time=timezone.datetime(2020, 5, 18, 9, 0, 1), + expires_at=timezone.datetime(2020, 5, 18, 9, 0, 0), + ) def test_batch_delivery_timeout(self): """ Ensure that batch delivery timeout is respected. """ - email = Email.objects.create(to=['to@example.com'], - from_email='bob@example.com', subject='', - message='', status=STATUS.queued, backend_alias='slow_backend') + email = Email.objects.create( + to=['to@example.com'], + from_email='bob@example.com', + subject='', + message='', + status=STATUS.queued, + backend_alias='slow_backend', + ) start_time = timezone.now() # slow backend sleeps for 5 seconds, so we should get a timeout error since we set # BATCH_DELIVERY_TIMEOUT timeout to 2 seconds in test_settings.py @@ -511,7 +543,5 @@ def test_backend_signal(self, mock): """ Check that the post_office signal handler is fired """ - email = send(recipients=['a@example.com'], - sender='from@example.com', message='message', - subject='subject') + email = send(recipients=['a@example.com'], sender='from@example.com', message='message', subject='subject') mock.assert_called_once_with(sender=Email, emails=[email]) diff --git a/post_office/tests/test_models.py b/post_office/tests/test_models.py index a3630a68..c4bb30ed 100644 --- a/post_office/tests/test_models.py +++ b/post_office/tests/test_models.py @@ -17,7 +17,6 @@ class ModelTest(TestCase): - def test_email_message(self): """ Test to make sure that model's "email_message" method @@ -25,9 +24,13 @@ def test_email_message(self): """ # If ``html_message`` is set, ``EmailMultiAlternatives`` is expected - email = Email.objects.create(to=['to@example.com'], - from_email='from@example.com', subject='Subject', - message='Message', html_message='

HTML

') + email = Email.objects.create( + to=['to@example.com'], + from_email='from@example.com', + subject='Subject', + message='Message', + html_message='

HTML

', + ) message = email.email_message() self.assertEqual(type(message), EmailMultiAlternatives) self.assertEqual(message.from_email, 'from@example.com') @@ -37,9 +40,9 @@ def test_email_message(self): self.assertEqual(message.alternatives, [('

HTML

', 'text/html')]) # Without ``html_message``, ``EmailMessage`` class is expected - email = Email.objects.create(to=['to@example.com'], - from_email='from@example.com', subject='Subject', - message='Message') + email = Email.objects.create( + to=['to@example.com'], from_email='from@example.com', subject='Subject', message='Message' + ) message = email.email_message() self.assertEqual(type(message), EmailMessage) self.assertEqual(message.from_email, 'from@example.com') @@ -52,13 +55,10 @@ def test_email_message_render(self): Ensure Email instance with template is properly rendered. """ template = EmailTemplate.objects.create( - subject='Subject {{ name }}', - content='Content {{ name }}', - html_content='HTML {{ name }}' + subject='Subject {{ name }}', content='Content {{ name }}', html_content='HTML {{ name }}' ) context = {'name': 'test'} - email = Email.objects.create(to=['to@example.com'], template=template, - from_email='from@e.com', context=context) + email = Email.objects.create(to=['to@example.com'], template=template, from_email='from@e.com', context=context) message = email.email_message() self.assertEqual(message.subject, 'Subject test') self.assertEqual(message.body, 'Content test') @@ -69,10 +69,15 @@ def test_email_message_prepare_without_template_and_with_context(self): Ensure Email instance without template but with context is properly prepared. """ context = {'name': 'test'} - email = Email.objects.create(to=['to@example.com'], template=None, - subject='Subject test', message='Content test', - html_message='HTML test', - from_email='from@e.com', context=context) + email = Email.objects.create( + to=['to@example.com'], + template=None, + subject='Subject test', + message='Content test', + html_message='HTML test', + from_email='from@e.com', + context=context, + ) message = email.email_message() self.assertEqual(message.subject, 'Subject test') self.assertEqual(message.body, 'Content test') @@ -82,16 +87,26 @@ def test_dispatch(self): """ Ensure that email.dispatch() actually sends out the email """ - email = Email.objects.create(to=['to@example.com'], from_email='from@example.com', - subject='Test dispatch', message='Message', backend_alias='locmem') + email = Email.objects.create( + to=['to@example.com'], + from_email='from@example.com', + subject='Test dispatch', + message='Message', + backend_alias='locmem', + ) email.dispatch() self.assertEqual(mail.outbox[0].subject, 'Test dispatch') def test_dispatch_with_override_recipients(self): previous_settings = settings.POST_OFFICE setattr(settings, 'POST_OFFICE', {'OVERRIDE_RECIPIENTS': ['override@gmail.com']}) - email = Email.objects.create(to=['to@example.com'], from_email='from@example.com', - subject='Test dispatch', message='Message', backend_alias='locmem') + email = Email.objects.create( + to=['to@example.com'], + from_email='from@example.com', + subject='Test dispatch', + message='Message', + backend_alias='locmem', + ) email.dispatch() self.assertEqual(mail.outbox[0].to, ['override@gmail.com']) settings.POST_OFFICE = previous_settings @@ -100,8 +115,14 @@ def test_status_and_log(self): """ Ensure that status and log are set properly on successful sending """ - email = Email.objects.create(to=['to@example.com'], from_email='from@example.com', - subject='Test', message='Message', backend_alias='locmem', id=333) + email = Email.objects.create( + to=['to@example.com'], + from_email='from@example.com', + subject='Test', + message='Message', + backend_alias='locmem', + id=333, + ) # Ensure that after dispatch status and logs are correctly set email.dispatch() log = Log.objects.latest('id') @@ -112,9 +133,13 @@ def test_status_and_log_on_error(self): """ Ensure that status and log are set properly on sending failure """ - email = Email.objects.create(to=['to@example.com'], from_email='from@example.com', - subject='Test', message='Message', - backend_alias='error') + email = Email.objects.create( + to=['to@example.com'], + from_email='from@example.com', + subject='Test', + message='Message', + backend_alias='error', + ) # Ensure that after dispatch status and logs are correctly set email.dispatch() log = Log.objects.latest('id') @@ -128,9 +153,13 @@ def test_errors_while_getting_connection_are_logged(self): """ Ensure that status and log are set properly on sending failure """ - email = Email.objects.create(to=['to@example.com'], subject='Test', - from_email='from@example.com', - message='Message', backend_alias='random') + email = Email.objects.create( + to=['to@example.com'], + subject='Test', + from_email='from@example.com', + message='Message', + backend_alias='random', + ) # Ensure that after dispatch status and logs are correctly set email.dispatch() log = Log.objects.latest('id') @@ -141,8 +170,7 @@ def test_errors_while_getting_connection_are_logged(self): def test_default_sender(self): email = send(['to@example.com'], subject='foo') - self.assertEqual(email.from_email, - django_settings.DEFAULT_FROM_EMAIL) + self.assertEqual(email.from_email, django_settings.DEFAULT_FROM_EMAIL) def test_send_argument_checking(self): """ @@ -150,18 +178,16 @@ def test_send_argument_checking(self): - "template" is used with "subject", "message" or "html_message" - recipients is not in tuple or list format """ - self.assertRaises(ValueError, send, ['to@example.com'], 'from@a.com', - template='foo', subject='bar') - self.assertRaises(ValueError, send, ['to@example.com'], 'from@a.com', - template='foo', message='bar') - self.assertRaises(ValueError, send, ['to@example.com'], 'from@a.com', - template='foo', html_message='bar') - self.assertRaises(ValueError, send, 'to@example.com', 'from@a.com', - template='foo', html_message='bar') - self.assertRaises(ValueError, send, cc='cc@example.com', sender='from@a.com', - template='foo', html_message='bar') - self.assertRaises(ValueError, send, bcc='bcc@example.com', sender='from@a.com', - template='foo', html_message='bar') + self.assertRaises(ValueError, send, ['to@example.com'], 'from@a.com', template='foo', subject='bar') + self.assertRaises(ValueError, send, ['to@example.com'], 'from@a.com', template='foo', message='bar') + self.assertRaises(ValueError, send, ['to@example.com'], 'from@a.com', template='foo', html_message='bar') + self.assertRaises(ValueError, send, 'to@example.com', 'from@a.com', template='foo', html_message='bar') + self.assertRaises( + ValueError, send, cc='cc@example.com', sender='from@a.com', template='foo', html_message='bar' + ) + self.assertRaises( + ValueError, send, bcc='bcc@example.com', sender='from@a.com', template='foo', html_message='bar' + ) def test_send_with_template(self): """ @@ -169,33 +195,41 @@ def test_send_with_template(self): """ Email.objects.all().delete() headers = {'Reply-to': 'reply@email.com'} - email_template = EmailTemplate.objects.create(name='foo', subject='bar', - content='baz') + email_template = EmailTemplate.objects.create(name='foo', subject='bar', content='baz') scheduled_time = datetime.now() + timedelta(days=1) - email = send(recipients=['to1@example.com', 'to2@example.com'], sender='from@a.com', - headers=headers, template=email_template, - scheduled_time=scheduled_time) + email = send( + recipients=['to1@example.com', 'to2@example.com'], + sender='from@a.com', + headers=headers, + template=email_template, + scheduled_time=scheduled_time, + ) self.assertEqual(email.to, ['to1@example.com', 'to2@example.com']) self.assertEqual(email.headers, headers) self.assertEqual(email.scheduled_time, scheduled_time) # Test without header Email.objects.all().delete() - email = send(recipients=['to1@example.com', 'to2@example.com'], sender='from@a.com', - template=email_template) + email = send(recipients=['to1@example.com', 'to2@example.com'], sender='from@a.com', template=email_template) self.assertEqual(email.to, ['to1@example.com', 'to2@example.com']) self.assertEqual(email.headers, None) def test_send_without_template(self): headers = {'Reply-to': 'reply@email.com'} scheduled_time = datetime.now() + timedelta(days=1) - email = send(sender='from@a.com', - recipients=['to1@example.com', 'to2@example.com'], - cc=['cc1@example.com', 'cc2@example.com'], - bcc=['bcc1@example.com', 'bcc2@example.com'], - subject='foo', message='bar', html_message='baz', - context={'name': 'Alice'}, headers=headers, - scheduled_time=scheduled_time, priority=PRIORITY.low) + email = send( + sender='from@a.com', + recipients=['to1@example.com', 'to2@example.com'], + cc=['cc1@example.com', 'cc2@example.com'], + bcc=['bcc1@example.com', 'bcc2@example.com'], + subject='foo', + message='bar', + html_message='baz', + context={'name': 'Alice'}, + headers=headers, + scheduled_time=scheduled_time, + priority=PRIORITY.low, + ) self.assertEqual(email.to, ['to1@example.com', 'to2@example.com']) self.assertEqual(email.cc, ['cc1@example.com', 'cc2@example.com']) @@ -208,10 +242,15 @@ def test_send_without_template(self): self.assertEqual(email.scheduled_time, scheduled_time) # Same thing, but now with context - email = send(['to1@example.com'], 'from@a.com', - subject='Hi {{ name }}', message='Message {{ name }}', - html_message='{{ name }}', - context={'name': 'Bob'}, headers=headers) + email = send( + ['to1@example.com'], + 'from@a.com', + subject='Hi {{ name }}', + message='Message {{ name }}', + html_message='{{ name }}', + context={'name': 'Bob'}, + headers=headers, + ) self.assertEqual(email.to, ['to1@example.com']) self.assertEqual(email.subject, 'Hi Bob') self.assertEqual(email.message, 'Message Bob') @@ -227,22 +266,21 @@ def test_invalid_syntax(self): name='cost', subject='Hi there!{{ }}', content='Welcome {{ name|titl }} to the site.', - html_content='{% block content %}

Welcome to the site

' + html_content='{% block content %}

Welcome to the site

', ) - EmailTemplateForm = modelform_factory(EmailTemplate, - exclude=['template']) + EmailTemplateForm = modelform_factory(EmailTemplate, exclude=['template']) form = EmailTemplateForm(data) self.assertFalse(form.is_valid()) - self.assertEqual(form.errors['default_template'], ['This field is required.']) + self.assertEqual(form.errors['default_template'], ['This field is required.']) self.assertEqual(form.errors['content'], ["Invalid filter: 'titl'"]) - self.assertIn(form.errors['html_content'], - [['Unclosed tags: endblock '], - ["Unclosed tag on line 1: 'block'. Looking for one of: endblock."]]) - self.assertIn(form.errors['subject'], - [['Empty variable tag'], ['Empty variable tag on line 1']]) + self.assertIn( + form.errors['html_content'], + [['Unclosed tags: endblock '], ["Unclosed tag on line 1: 'block'. Looking for one of: endblock."]], + ) + self.assertIn(form.errors['subject'], [['Empty variable tag'], ['Empty variable tag on line 1']]) def test_string_priority(self): """ @@ -262,10 +300,7 @@ def test_string_priority_exception(self): with self.assertRaises(ValueError) as context: invalid_priority_send() - self.assertEqual( - str(context.exception), - 'Invalid priority, must be one of: low, medium, high, now' - ) + self.assertEqual(str(context.exception), 'Invalid priority, must be one of: low, medium, high, now') def test_send_recipient_display_name(self): """ @@ -278,50 +313,35 @@ def test_send_recipient_display_name(self): def test_attachment_filename(self): attachment = Attachment() - attachment.file.save( - 'test.txt', - content=ContentFile('test file content'), - save=True - ) + attachment.file.save('test.txt', content=ContentFile('test file content'), save=True) self.assertEqual(attachment.name, 'test.txt') # Test that it is saved to the correct subdirectory date = timezone.now().date() - expected_path = os.path.join('post_office_attachments', str(date.year), - str(date.month), str(date.day)) + expected_path = os.path.join('post_office_attachments', str(date.year), str(date.month), str(date.day)) self.assertTrue(expected_path in attachment.file.name) def test_attachments_email_message(self): - email = Email.objects.create(to=['to@example.com'], - from_email='from@example.com', - subject='Subject') + email = Email.objects.create(to=['to@example.com'], from_email='from@example.com', subject='Subject') attachment = Attachment() - attachment.file.save( - 'test.txt', content=ContentFile('test file content'), save=True - ) + attachment.file.save('test.txt', content=ContentFile('test file content'), save=True) email.attachments.add(attachment) message = email.email_message() - self.assertEqual(message.attachments, - [('test.txt', 'test file content', 'text/plain')]) + self.assertEqual(message.attachments, [('test.txt', 'test file content', 'text/plain')]) def test_attachments_email_message_with_mimetype(self): - email = Email.objects.create(to=['to@example.com'], - from_email='from@example.com', - subject='Subject') + email = Email.objects.create(to=['to@example.com'], from_email='from@example.com', subject='Subject') attachment = Attachment() - attachment.file.save( - 'test.txt', content=ContentFile('test file content'), save=True - ) + attachment.file.save('test.txt', content=ContentFile('test file content'), save=True) attachment.mimetype = 'text/plain' attachment.save() email.attachments.add(attachment) message = email.email_message() - self.assertEqual(message.attachments, - [('test.txt', 'test file content', 'text/plain')]) + self.assertEqual(message.attachments, [('test.txt', 'test file content', 'text/plain')]) def test_translated_template_uses_default_templates_name(self): template = EmailTemplate.objects.create(name='name') @@ -329,10 +349,8 @@ def test_translated_template_uses_default_templates_name(self): self.assertEqual(id_template.name, template.name) def test_models_repr(self): - self.assertEqual(repr(EmailTemplate(name='test', language='en')), - '') - self.assertEqual(repr(Email(to=['test@example.com'])), - "") + self.assertEqual(repr(EmailTemplate(name='test', language='en')), '') + self.assertEqual(repr(Email(to=['test@example.com'])), "") def test_natural_key(self): template = EmailTemplate.objects.create(name='name') diff --git a/post_office/tests/test_utils.py b/post_office/tests/test_utils.py index a7ce9541..38f589f4 100644 --- a/post_office/tests/test_utils.py +++ b/post_office/tests/test_utils.py @@ -5,26 +5,22 @@ from django.test.utils import override_settings from ..models import Email, STATUS, PRIORITY, EmailTemplate, Attachment -from ..utils import (create_attachments, get_email_template, parse_emails, - parse_priority, send_mail, split_emails) +from ..utils import create_attachments, get_email_template, parse_emails, parse_priority, send_mail, split_emails from ..validators import validate_email_with_name, validate_comma_separated_emails @override_settings(EMAIL_BACKEND='django.core.mail.backends.locmem.EmailBackend') class UtilsTest(TestCase): - def test_mail_status(self): """ Check that send_mail assigns the right status field to Email instances """ - send_mail('subject', 'message', 'from@example.com', ['to@example.com'], - priority=PRIORITY.medium) + send_mail('subject', 'message', 'from@example.com', ['to@example.com'], priority=PRIORITY.medium) email = Email.objects.latest('id') self.assertEqual(email.status, STATUS.queued) # Emails sent with "now" priority is sent right away - send_mail('subject', 'message', 'from@example.com', ['to@example.com'], - priority=PRIORITY.now) + send_mail('subject', 'message', 'from@example.com', ['to@example.com'], priority=PRIORITY.now) email = Email.objects.latest('id') self.assertEqual(email.status, STATUS.sent) @@ -32,8 +28,13 @@ def test_email_validator(self): # These should validate validate_email_with_name('email@example.com') validate_email_with_name('Alice Bob ') - Email.objects.create(to=['to@example.com'], from_email='Alice ', - subject='Test', message='Message', status=STATUS.sent) + Email.objects.create( + to=['to@example.com'], + from_email='Alice ', + subject='Test', + message='Message', + status=STATUS.sent, + ) # Should also support international domains validate_email_with_name('Alice Bob ') @@ -46,17 +47,16 @@ def test_email_validator(self): def test_comma_separated_email_list_validator(self): # These should validate validate_comma_separated_emails(['email@example.com']) - validate_comma_separated_emails( - ['email@example.com', 'email2@example.com', 'email3@example.com'] - ) + validate_comma_separated_emails(['email@example.com', 'email2@example.com', 'email3@example.com']) validate_comma_separated_emails(['Alice Bob ']) # Should also support international domains validate_comma_separated_emails(['email@example.co.id']) # These should raise ValidationError - self.assertRaises(ValidationError, validate_comma_separated_emails, - ['email@example.com', 'invalid_mail', 'email@example.com']) + self.assertRaises( + ValidationError, validate_comma_separated_emails, ['email@example.com', 'invalid_mail', 'email@example.com'] + ) def test_get_template_email(self): # Sanity Check @@ -73,8 +73,7 @@ def test_get_template_email(self): self.assertEqual(template, get_email_template(name)) # Repeat with language support - template = EmailTemplate.objects.create(name=name, content='test', - language='en') + template = EmailTemplate.objects.create(name=name, content='test', language='en') # First query should hit database self.assertNumQueries(1, lambda: get_email_template(name, 'en')) # Second query should hit cache instead @@ -87,23 +86,18 @@ def test_template_caching_settings(self): """Check if POST_OFFICE_CACHE and POST_OFFICE_TEMPLATE_CACHE understood correctly """ + def is_cache_used(suffix='', desired_cache=False): - """Raise exception if real cache usage not equal to desired_cache value - """ + """Raise exception if real cache usage not equal to desired_cache value""" # to avoid cache cleaning - just create new template name = 'can_i/suport_cache_settings%s' % suffix - self.assertRaises( - EmailTemplate.DoesNotExist, get_email_template, name - ) + self.assertRaises(EmailTemplate.DoesNotExist, get_email_template, name) EmailTemplate.objects.create(name=name, content='test') # First query should hit database anyway self.assertNumQueries(1, lambda: get_email_template(name)) # Second query should hit cache instead only if we want it - self.assertNumQueries( - 0 if desired_cache else 1, - lambda: get_email_template(name) - ) + self.assertNumQueries(0 if desired_cache else 1, lambda: get_email_template(name)) return # default - use cache @@ -113,14 +107,9 @@ def is_cache_used(suffix='', desired_cache=False): with self.settings(POST_OFFICE_CACHE=False): is_cache_used(suffix='cache_disabled_global', desired_cache=False) with self.settings(POST_OFFICE_TEMPLATE_CACHE=False): - is_cache_used( - suffix='cache_disabled_for_templates', desired_cache=False - ) + is_cache_used(suffix='cache_disabled_for_templates', desired_cache=False) with self.settings(POST_OFFICE_CACHE=True, POST_OFFICE_TEMPLATE_CACHE=False): - is_cache_used( - suffix='cache_disabled_for_templates_but_enabled_global', - desired_cache=False - ) + is_cache_used(suffix='cache_disabled_for_templates_but_enabled_global', desired_cache=False) return def test_split_emails(self): @@ -134,10 +123,12 @@ def test_split_emails(self): self.assertEqual(expected_size, [len(emails) for emails in email_list]) def test_create_attachments(self): - attachments = create_attachments({ - 'attachment_file1.txt': ContentFile('content'), - 'attachment_file2.txt': ContentFile('content'), - }) + attachments = create_attachments( + { + 'attachment_file1.txt': ContentFile('content'), + 'attachment_file2.txt': ContentFile('content'), + } + ) self.assertEqual(len(attachments), 2) self.assertIsInstance(attachments[0], Attachment) @@ -147,16 +138,12 @@ def test_create_attachments(self): self.assertEqual(attachments[0].mimetype, '') def test_create_attachments_with_mimetype(self): - attachments = create_attachments({ - 'attachment_file1.txt': { - 'file': ContentFile('content'), - 'mimetype': 'text/plain' - }, - 'attachment_file2.jpg': { - 'file': ContentFile('content'), - 'mimetype': 'text/plain' + attachments = create_attachments( + { + 'attachment_file1.txt': {'file': ContentFile('content'), 'mimetype': 'text/plain'}, + 'attachment_file2.jpg': {'file': ContentFile('content'), 'mimetype': 'text/plain'}, } - }) + ) self.assertEqual(len(attachments), 2) self.assertIsInstance(attachments[0], Attachment) @@ -166,9 +153,7 @@ def test_create_attachments_with_mimetype(self): self.assertEqual(attachments[0].mimetype, 'text/plain') def test_create_attachments_open_file(self): - attachments = create_attachments({ - 'attachment_file.py': __file__, - }) + attachments = create_attachments({'attachment_file.py': __file__}) self.assertEqual(len(attachments), 1) self.assertIsInstance(attachments[0], Attachment) @@ -185,20 +170,11 @@ def test_parse_priority(self): def test_parse_emails(self): # Converts a single email to list of email - self.assertEqual( - parse_emails('test@example.com'), - ['test@example.com'] - ) + self.assertEqual(parse_emails('test@example.com'), ['test@example.com']) # None is converted into an empty list self.assertEqual(parse_emails(None), []) # Raises ValidationError if email is invalid - self.assertRaises( - ValidationError, - parse_emails, 'invalid_email' - ) - self.assertRaises( - ValidationError, - parse_emails, ['invalid_email', 'test@example.com'] - ) + self.assertRaises(ValidationError, parse_emails, 'invalid_email') + self.assertRaises(ValidationError, parse_emails, ['invalid_email', 'test@example.com']) diff --git a/post_office/utils.py b/post_office/utils.py index db69e233..9c5321cb 100644 --- a/post_office/utils.py +++ b/post_office/utils.py @@ -10,8 +10,16 @@ from .validators import validate_email_with_name -def send_mail(subject, message, from_email, recipient_list, html_message='', - scheduled_time=None, headers=None, priority=PRIORITY.medium): +def send_mail( + subject, + message, + from_email, + recipient_list, + html_message='', + scheduled_time=None, + headers=None, + priority=PRIORITY.medium, +): """ Add a new message to the mail queue. This is a replacement for Django's ``send_mail`` core email method. @@ -21,10 +29,17 @@ def send_mail(subject, message, from_email, recipient_list, html_message='', status = None if priority == PRIORITY.now else STATUS.queued emails = [ Email.objects.create( - from_email=from_email, to=address, subject=subject, - message=message, html_message=html_message, status=status, - headers=headers, priority=priority, scheduled_time=scheduled_time - ) for address in recipient_list + from_email=from_email, + to=address, + subject=subject, + message=message, + html_message=html_message, + status=status, + headers=headers, + priority=priority, + scheduled_time=scheduled_time, + ) + for address in recipient_list ] if priority == PRIORITY.now: for email in emails: @@ -76,7 +91,6 @@ def create_attachments(attachment_files): """ attachments = [] for filename, filedata in attachment_files.items(): - if isinstance(filedata, dict): content = filedata.get('file', None) mimetype = filedata.get('mimetype', None) @@ -116,8 +130,7 @@ def parse_priority(priority): priority = getattr(PRIORITY, priority, None) if priority is None: - raise ValueError('Invalid priority, must be one of: %s' % - ', '.join(PRIORITY._fields)) + raise ValueError('Invalid priority, must be one of: %s' % ', '.join(PRIORITY._fields)) return priority diff --git a/pyproject.toml b/pyproject.toml index 9bcd8a5a..fa8051a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,4 +1,10 @@ -[tool.black] +[tool.ruff] line-length = 120 -target-version = ["py38"] -skip-string-normalization = true \ No newline at end of file +indent-width = 4 +target-version = "py39" + +[tool.ruff.format] +quote-style = "single" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" diff --git a/setup.py b/setup.py index 82416995..d697ca6e 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,6 @@ class Tox(TestCommand): - def initialize_options(self): TestCommand.initialize_options(self) self.tox_args = None @@ -20,6 +19,7 @@ def run_tests(self): # import here, cause outside the eggs aren't loaded import tox import shlex + args = self.tox_args if args: args = shlex.split(self.tox_args) @@ -77,5 +77,5 @@ def run_tests(self): 'test': TESTS_REQUIRE, 'prevent-XSS': ['bleach'], }, - cmdclass={'test': Tox} + cmdclass={'test': Tox}, )