diff --git a/brasilio/settings.py b/brasilio/settings.py index c5983b6f..af510cc8 100644 --- a/brasilio/settings.py +++ b/brasilio/settings.py @@ -242,7 +242,8 @@ } # django-rq config -RQ_QUEUES = {"default": {"URL": REDIS_URL, "DEFAULT_TIMEOUT": 500,}} +DEFAULT_QUEUE_NAME = "default" +RQ_QUEUES = {DEFAULT_QUEUE_NAME: {"URL": REDIS_URL, "DEFAULT_TIMEOUT": 500,}} RQ = { "DEFAULT_RESULT_TTL": 60 * 60 * 24, # 24-hours } diff --git a/brasilio/test_settings.py b/brasilio/test_settings.py index 208b9f2b..98ab5fd5 100644 --- a/brasilio/test_settings.py +++ b/brasilio/test_settings.py @@ -14,3 +14,4 @@ TEMPLATES[0]["OPTIONS"]["string_if_invalid"] = TEMPLATE_STRING_IF_INVALID # noqa ENABLE_API_AUTH = True DISABLE_RECAPTCHA = False +EMAIL_BACKEND = "django.core.mail.backends.console.EmailBackend" diff --git a/brasilio/worker.py b/brasilio/worker.py index 995b8a9c..ca82fa29 100644 --- a/brasilio/worker.py +++ b/brasilio/worker.py @@ -1,8 +1,7 @@ -from rq import Worker -from rq.contrib.sentry import register_sentry - from raven import Client from raven.transport import HTTPTransport +from rq import Worker +from rq.contrib.sentry import register_sentry class SentryAwareWorker(Worker): diff --git a/brasilio_auth/management/commands/__init__.py b/brasilio_auth/management/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/brasilio_auth/management/commands/send_bulk_emails.py b/brasilio_auth/management/commands/send_bulk_emails.py new file mode 100644 index 00000000..f96ae323 --- /dev/null +++ b/brasilio_auth/management/commands/send_bulk_emails.py @@ -0,0 +1,57 @@ +from datetime import timedelta + +import rows +from django.conf import settings +from django.core.management.base import BaseCommand +from django.template import Context, Template +from tqdm import tqdm + +from core.email import send_email +from core.queue import get_redis_queue + + +class Command(BaseCommand): + def load_email_template(self, email_template): + with open(email_template, "r") as fobj: + return Template(fobj.read()) + + def print_email_metadata(self, metadata): + for key, value in metadata.items(): + print(f"{key}: {value}") + + print("-" * 80) + + def add_arguments(self, parser): + parser.add_argument("--sender", default=settings.DEFAULT_FROM_EMAIL) + parser.add_argument("--dry-run", default=False, action="store_true") + parser.add_argument("--wait-time", default=15, type=int) + parser.add_argument("input_filename") + parser.add_argument("template_filename") + + def handle(self, *args, **kwargs): + queue = get_redis_queue(settings.DEFAULT_QUEUE_NAME, settings.RQ_QUEUES[settings.DEFAULT_QUEUE_NAME]["URL"]) + + input_filename = kwargs["input_filename"] + table = rows.import_from_csv(input_filename) + error_msg = "Arquivo CSV deve conter campos 'to_email' e 'subject'" + assert {"to_email", "subject"}.issubset(set(table.field_names)), error_msg + + template_obj = self.load_email_template(kwargs["template_filename"]) + wait_time = kwargs["wait_time"] + from_email = kwargs["sender"] + + time_offset = 0 + for row in tqdm(table): + context = Context(row._asdict()) + rendered_template = template_obj.render(context=context) + email_kwargs = { + "subject": row.subject, + "body": rendered_template, + "from_email": from_email, + "to": [row.to_email], + } + if not kwargs["dry_run"]: + queue.enqueue_in(timedelta(seconds=time_offset), send_email, **email_kwargs) + time_offset += wait_time + else: + self.print_email_metadata(email_kwargs) diff --git a/brasilio_auth/tests/test_commands.py b/brasilio_auth/tests/test_commands.py new file mode 100644 index 00000000..64210f65 --- /dev/null +++ b/brasilio_auth/tests/test_commands.py @@ -0,0 +1,102 @@ +from datetime import timedelta +from tempfile import NamedTemporaryFile +from unittest import mock + +import pytest +from django.conf import settings +from django.core.management import call_command +from django.test import TestCase + +from core.email import send_email + + +class TestSendBulkEmails(TestCase): + def setUp(self): + self.p_redis_queue = mock.patch("brasilio_auth.management.commands.send_bulk_emails.get_redis_queue") + self.m_redis_queue = self.p_redis_queue.start() + self.m_queue = mock.Mock() + self.m_redis_queue.return_value = self.m_queue + + self.input_file = NamedTemporaryFile(suffix=".csv") + self.email_template = NamedTemporaryFile(suffix=".txt") + + with open(self.input_file.name, "w") as fobj: + fobj.write( + "nome,data,to_email,subject\nnome_1,data_1,email_1,subject_1\n" "nome_2,data_2,email_2,subject_2" + ) + + with open(self.email_template.name, "w") as fobj: + fobj.write("Enviado para {{ nome }} em {{ data }}") + + self.input_file.seek(0) + self.email_template.seek(0) + + self.expexted_send_email = [ + { + "body": "Enviado para nome_1 em data_1", + "subject": "subject_1", + "to": ["email_1"], + "from_email": settings.DEFAULT_FROM_EMAIL, + }, + { + "body": "Enviado para nome_2 em data_2", + "subject": "subject_2", + "to": ["email_2"], + "from_email": settings.DEFAULT_FROM_EMAIL, + }, + ] + + def tearDown(self): + self.p_redis_queue.stop() + + def test_send_bulk_emails(self): + call_command("send_bulk_emails", self.input_file.name, self.email_template.name) + + self.m_redis_queue.assert_called_once_with( + settings.DEFAULT_QUEUE_NAME, + settings.RQ_QUEUES[settings.DEFAULT_QUEUE_NAME]["URL"] + ) + self.m_queue.enqueue_in.assert_has_calls( + [ + mock.call(timedelta(seconds=0), send_email, **self.expexted_send_email[0]), + mock.call(timedelta(seconds=15), send_email, **self.expexted_send_email[1]), + ] + ) + + def test_send_email_custom_from_email(self): + kwargs = {"sender": "Example Email "} + call_command("send_bulk_emails", self.input_file.name, self.email_template.name, **kwargs) + + self.expexted_send_email[0]["from_email"] = kwargs["sender"] + self.expexted_send_email[1]["from_email"] = kwargs["sender"] + + self.m_queue.enqueue_in.assert_has_calls( + [ + mock.call(timedelta(seconds=0), send_email, **self.expexted_send_email[0]), + mock.call(timedelta(seconds=15), send_email, **self.expexted_send_email[1]), + ] + ) + + def test_assert_mandatory_fields(self): + with open(self.input_file.name, "w") as fobj: + fobj.write("nome,data\nnome_1,data_1\nnome_2,data_2") + + self.input_file.seek(0) + with pytest.raises(AssertionError): + call_command("send_bulk_emails", self.input_file.name, self.email_template.name) + + def test_do_not_send_mail(self): + call_command("send_bulk_emails", self.input_file.name, self.email_template.name, "--dry-run") + + self.m_queue.assert_not_called() + + def test_send_bulk_emails_schedule_with_wait_time(self): + kwargs = {"wait_time": 30} + call_command("send_bulk_emails", self.input_file.name, self.email_template.name, **kwargs) + + self.m_queue.enqueue_in.assert_has_calls( + [ + mock.call(timedelta(seconds=0), send_email, **self.expexted_send_email[0]), + mock.call(timedelta(seconds=30), send_email, **self.expexted_send_email[1]), + ] + ) diff --git a/core/email.py b/core/email.py new file mode 100644 index 00000000..4b399ee3 --- /dev/null +++ b/core/email.py @@ -0,0 +1,6 @@ +from django.core.mail import EmailMessage + + +def send_email(subject, body, from_email, to, **kwargs): + email = EmailMessage(subject=subject, body=body, from_email=from_email, to=to, **kwargs) + email.send() diff --git a/core/queue.py b/core/queue.py new file mode 100644 index 00000000..e80be278 --- /dev/null +++ b/core/queue.py @@ -0,0 +1,7 @@ +from rq import Queue +from redis import Redis + + +def get_redis_queue(name: str, url: str) -> Queue: + redis_conn = Redis.from_url(url) + return Queue(name=name, connection=redis_conn) diff --git a/core/tests/test_email.py b/core/tests/test_email.py new file mode 100644 index 00000000..7e2a22f4 --- /dev/null +++ b/core/tests/test_email.py @@ -0,0 +1,23 @@ +from django.core import mail +from django.test import TestCase + +from core.email import send_email + + +class TestSendEmail(TestCase): + def test_send_emnail(self): + subject = "Subject" + body = "Body" + from_email = "from@example.com" + to = ["to@example.com"] + reply_to = ["Reply To ', to=[settings.DEFAULT_FROM_EMAIL], reply_to=[f'{data["name"]} <{data["email"]}>'], ) - email.send() + return redirect(reverse("core:contact") + "?sent=true") else: diff --git a/covid19/models.py b/covid19/models.py index f63a8d64..4c623161 100644 --- a/covid19/models.py +++ b/covid19/models.py @@ -74,8 +74,8 @@ def most_recent_deployed(self, state, date=None): class StateSpreadsheetManager(models.Manager): def get_state_data(self, state): """Return all state cases, grouped by date""" - from covid19.spreadsheet_validator import TOTAL_LINE_DISPLAY from brazil_data.cities import get_city_info + from covid19.spreadsheet_validator import TOTAL_LINE_DISPLAY cases, reports = defaultdict(dict), {} qs = self.get_queryset() diff --git a/requirements.txt b/requirements.txt index 8de7afed..27707e32 100644 --- a/requirements.txt +++ b/requirements.txt @@ -31,6 +31,7 @@ openpyxl==3.0.5 psycopg2-binary==2.8.5 pytest-django==3.2.1 pytz==2020.1 +redis==3.5.3 requests-cache==0.5.2 requests==2.24.0 retry==0.9.2