forked from ui/django-post_office
-
Notifications
You must be signed in to change notification settings - Fork 4
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Refactor sendmail commands into a unified namespace.
Reorganized sendmail management commands under a single `sendmail` namespace for better structure and usability. Updated documentation, tests, and examples to reflect the new command structure, replacing `send_queued_mail` with `sendmail all` and introducing new subcommands like `batch` and `cleanup_mail`.
- Loading branch information
1 parent
8ffd844
commit 0b3eb53
Showing
10 changed files
with
223 additions
and
37 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,19 +1,21 @@ | ||
import datetime | ||
import os | ||
import timeit | ||
from unittest import mock | ||
|
||
import pytest | ||
from django.core.files.base import ContentFile | ||
from django.core.management import call_command | ||
from django.utils.timezone import now | ||
|
||
from sendmail.mail import send | ||
from sendmail.config import settings | ||
from sendmail.models.attachment import Attachment | ||
from sendmail.models.emailaddress import EmailAddress | ||
from sendmail.models.emailmodel import STATUS, EmailModel | ||
from sendmail.utils import set_recipients | ||
|
||
def call_sendmail(*args, **kwargs): | ||
return call_command('sendmail', *args, **kwargs) | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_cleanup_mail_with_orphaned_attachments(): | ||
|
@@ -28,11 +30,11 @@ def test_cleanup_mail_with_orphaned_attachments(): | |
email.attachments.add(attachment) | ||
attachment_path = attachment.file.name | ||
|
||
call_command('cleanup_mail', days=30) | ||
call_sendmail('cleanup_mail', days=30) | ||
assert EmailModel.objects.count() == 0 | ||
assert Attachment.objects.count() == 1 | ||
|
||
call_command('cleanup_mail', '-da', days=30) | ||
call_sendmail('cleanup_mail', '-da', days=30) | ||
assert EmailModel.objects.count() == 0 | ||
assert Attachment.objects.count() == 0 | ||
|
||
|
@@ -51,7 +53,7 @@ def test_cleanup_mail_with_orphaned_attachments(): | |
# Simulate that the files have been deleted by accidents | ||
os.remove(attachment_path) | ||
|
||
call_command('cleanup_mail', '-da', days=30) | ||
call_sendmail('cleanup_mail', '-da', days=30) | ||
assert EmailModel.objects.count() == 0 | ||
assert Attachment.objects.count() == 0 | ||
|
||
|
@@ -66,41 +68,50 @@ def test_cleanup_mail(): | |
|
||
# The command shouldn't delete today's email | ||
email = EmailModel.objects.create(from_email='[email protected]', language='en') | ||
call_command('cleanup_mail', days=30) | ||
call_sendmail('cleanup_mail', days=30) | ||
assert EmailModel.objects.count() == 1 | ||
|
||
# Email older than 30 days should be deleted | ||
email.created = now() - datetime.timedelta(days=31) | ||
email.save() | ||
call_command('cleanup_mail', days=30) | ||
call_sendmail('cleanup_mail', days=30) | ||
assert EmailModel.objects.count() == 0 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_send_queued_mail(): | ||
with mock.patch('django.db.connection.close', return_value=None): | ||
call_command('send_queued_mail', processes=1) | ||
call_sendmail('all', processes=1) | ||
|
||
EmailModel.objects.create(from_email='[email protected]', status=STATUS.queued, language='en') | ||
EmailModel.objects.create(from_email='[email protected]', status=STATUS.queued, language='en') | ||
call_command('send_queued_mail', processes=1) | ||
call_sendmail('all', processes=1) | ||
assert EmailModel.objects.filter(status=STATUS.sent).count() == 2 | ||
assert EmailModel.objects.filter(status=STATUS.queued).count() == 0 | ||
|
||
@pytest.mark.django_db | ||
def test_send_batch(): | ||
with mock.patch('django.db.connection.close', return_value=None): | ||
queue = [EmailModel.objects.create(from_email='[email protected]', status=STATUS.queued, language='en') for _ in range(200)] | ||
|
||
call_sendmail('batch', processes=1) | ||
assert EmailModel.objects.filter(status=STATUS.sent).count() == 100 | ||
assert EmailModel.objects.filter(status=STATUS.queued).count() == 100 | ||
|
||
|
||
@pytest.mark.django_db | ||
def test_successful_deliveries_log(): | ||
with mock.patch('django.db.connection.close', return_value=None): | ||
email = EmailModel.objects.create(from_email='[email protected]', status=STATUS.queued, language='en') | ||
call_command('send_queued_mail', log_level=0) | ||
call_sendmail('all', log_level=0) | ||
assert email.logs.count() == 0 | ||
|
||
email = EmailModel.objects.create(from_email='[email protected]', status=STATUS.queued, language='en') | ||
call_command('send_queued_mail', log_level=1) | ||
call_sendmail('all', log_level=1) | ||
assert email.logs.count() == 0 | ||
|
||
email = EmailModel.objects.create(from_email='[email protected]', status=STATUS.queued, language='en') | ||
call_command('send_queued_mail', log_level=2) | ||
call_sendmail('all', log_level=2) | ||
assert email.logs.count() == 1 | ||
|
||
|
||
|
@@ -117,22 +128,22 @@ def test_failed_deliveries_logging(): | |
) | ||
set_recipients(email, [recipient]) | ||
|
||
call_command('send_queued_mail', log_level=0) | ||
call_sendmail('all', log_level=0) | ||
assert email.logs.count() == 0 | ||
|
||
email = EmailModel.objects.create( | ||
from_email='[email protected]', status=STATUS.queued, backend_alias='error', language='en' | ||
) | ||
set_recipients(email, [recipient]) | ||
|
||
call_command('send_queued_mail', log_level=1) | ||
call_sendmail('all', log_level=1) | ||
assert email.logs.count() == 1 | ||
|
||
email = EmailModel.objects.create( | ||
from_email='[email protected]', status=STATUS.queued, backend_alias='error', language='en' | ||
) | ||
set_recipients(email, [recipient]) | ||
call_command('send_queued_mail', log_level=2) | ||
call_sendmail('all', log_level=2) | ||
assert email.logs.count() == 1 | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
from collections import OrderedDict | ||
import sendmail | ||
from sendmail.management.commands.subcommands.base import SubcommandsCommand | ||
from sendmail.management.commands.subcommands.cleanup_mail import Command as CleanupMailCommand | ||
from sendmail.management.commands.subcommands.send_queued_mail import SendBatch, SendQueuedMail | ||
from sendmail.management.commands.subcommands.dblocks import Command as DBLocksCommand | ||
|
||
class Command(SubcommandsCommand): | ||
command_name = "sendmail" | ||
subcommands = OrderedDict(( | ||
('cleanup_mail', CleanupMailCommand), | ||
('all', SendQueuedMail), | ||
('batch', SendBatch), | ||
('dblocks', DBLocksCommand), | ||
)) | ||
missing_args_message = 'one of the available sub commands must be provided' | ||
|
||
subcommand_dest = 'cmd' | ||
|
||
def get_version(self): | ||
return '.'.join(map(str,sendmail.VERSION)) | ||
|
||
def add_arguments(self, parser): | ||
parser.add_argument('--version', action='version', version=self.get_version()) | ||
super().add_arguments(parser) |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
# Django-CMS: https://github.com/django-cms/django-cms/blob/develop-4/cms/management/commands/subcommands/base.py | ||
|
||
import os | ||
from collections import OrderedDict | ||
|
||
from django.core.management.base import BaseCommand, CommandParser | ||
from django.core.management.color import color_style, no_style | ||
|
||
|
||
def add_builtin_arguments(parser): | ||
parser.add_argument( | ||
'--noinput', | ||
action='store_false', | ||
dest='interactive', | ||
default=True, | ||
help='Tells Django CMS to NOT prompt the user for input of any kind.' | ||
) | ||
|
||
# These are taking "as-is" from Django's management base | ||
# management command. | ||
parser.add_argument( | ||
'-v', '--verbosity', action='store', dest='verbosity', default='1', | ||
type=int, choices=[0, 1, 2, 3], | ||
help='Verbosity level; 0=minimal output, 1=normal output, 2=verbose output, 3=very verbose output' | ||
) | ||
parser.add_argument( | ||
'--settings', | ||
help=( | ||
'The Python path to a settings module, e.g. ' | ||
'"myproject.settings.main". If this isn\'t provided, the ' | ||
'DJANGO_SETTINGS_MODULE environment variable will be used.' | ||
), | ||
) | ||
parser.add_argument( | ||
'--pythonpath', help='A directory to add to the Python path, e.g. "/home/djangoprojects/myproject".' | ||
) | ||
parser.add_argument( | ||
'--traceback', action='store_true', help='Raise on CommandError exceptions' | ||
) | ||
parser.add_argument( | ||
'--no-color', action='store_true', dest='no_color', default=False, help="Don't colorize the command output." | ||
) | ||
parser.add_argument( | ||
'--force-color', action='store_true', dest='force_color', default=False, help="Colorize the command output." | ||
) | ||
parser.add_argument( | ||
'--skip-checks', action='store_true', dest='skip_checks', default=False, help="Skip the checks." | ||
) | ||
|
||
|
||
class SubcommandsCommand(BaseCommand): | ||
subcommands = OrderedDict() | ||
instances = {} | ||
help_string = '' | ||
command_name = '' | ||
stealth_options = ('interactive',) | ||
|
||
subcommand_dest = 'subcmd' | ||
|
||
def create_parser(self, prog_name, subcommand): | ||
kwargs = {} | ||
parser = CommandParser( | ||
prog=f"{os.path.basename(prog_name)} {subcommand}", | ||
description=self.help or None, | ||
missing_args_message=getattr(self, "missing_args_message", None), | ||
called_from_command_line=getattr(self, "_called_from_command_line", None), | ||
**kwargs | ||
) | ||
self.add_arguments(parser) | ||
return parser | ||
|
||
def add_arguments(self, parser): | ||
self.instances = {} | ||
|
||
if self.subcommands: | ||
stealth_options = set(self.stealth_options) | ||
subparsers = parser.add_subparsers(dest=self.subcommand_dest) | ||
for command, cls in self.subcommands.items(): | ||
instance = cls(self.stdout._out, self.stderr._out) | ||
instance.style = self.style | ||
kwargs = {} | ||
parser_sub = subparsers.add_parser( | ||
name=instance.command_name, help=instance.help_string, | ||
description=instance.help_string, **kwargs | ||
) | ||
|
||
add_builtin_arguments(parser=parser_sub) | ||
instance.add_arguments(parser_sub) | ||
stealth_options.update({action.dest for action in parser_sub._actions}) | ||
self.instances[command] = instance | ||
self.stealth_options = tuple(stealth_options) | ||
|
||
def handle(self, *args, **options): | ||
if options[self.subcommand_dest] in self.instances: | ||
command = self.instances[options[self.subcommand_dest]] | ||
if options.get('no_color'): | ||
command.style = no_style() | ||
command.stderr.style_func = None | ||
if options.get('force_color'): | ||
command.style = color_style(force_color=True) | ||
if options.get('stdout'): | ||
command.stdout._out = options.get('stdout') | ||
if options.get('stderr'): | ||
command.stderr._out = options.get('stderr') | ||
command.handle(*args, **options) | ||
else: | ||
self.print_help('manage.py', 'cms') |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.