Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement send emails on questionnaire due and response created #13

Merged
merged 6 commits into from
Mar 14, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,15 @@ DEBUG=True
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY=secret

# database connection url string
# Database connection url string
DATABASE_URL=postgresql://username:password@host:port/database


EMAIL_HOST=smtp4dev
EMAIL_HOST_USER=
EMAIL_HOST_PASSWORD=
EMAIL_PORT=25
DEFAULT_FROM_EMAIL=from@snnbotchway.com

# Redis
REDIS_HOST=redis
20 changes: 16 additions & 4 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,6 @@ jobs:
name: Tests and migrations
runs-on: ubuntu-latest
needs: check-hooks
env:
DATABASE_URL: postgresql://devuser:changeme@db:5432/devdb
DEBUG: True
SECRET_KEY: change_me

steps:
- name: Login to Docker Hub
Expand All @@ -44,6 +40,22 @@ jobs:
- name: Checkout
uses: actions/checkout@v3

- name: Make Envfile
uses: SpicyPizza/create-envfile@v1.3
with:
envkey_DEBUG: True
envkey_DATABASE_URL: "postgresql://devuser:changeme@db:5432/devdb"
envkey_SECRET_KEY: change_me
envkey_EMAIL_HOST: smtp4dev
envkey_EMAIL_HOST_USER: ""
envkey_EMAIL_HOST_PASSWORD: ""
envkey_EMAIL_PORT: 25
envkey_DEFAULT_FROM_EMAIL: from@snnbotchway.com
envkey_REDIS_HOST: redis
directory: .
file_name: .env
fail_on_empty: false

- name: Test
run: docker compose run --rm b2b sh -c "python manage.py wait_for_db && pytest"

Expand Down
2 changes: 2 additions & 0 deletions b2b/b2b/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
"""Configuration for the b2b project."""

from .celery import celery # noqa
10 changes: 10 additions & 0 deletions b2b/b2b/celery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"""Project celery configuration."""
import os

from celery import Celery

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "b2b.settings")

celery = Celery("b2b")
celery.config_from_object("django.conf:settings", namespace="CELERY")
celery.autodiscover_tasks()
17 changes: 17 additions & 0 deletions b2b/b2b/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
from typing import List

import environ
from celery.schedules import crontab

env = environ.Env(
# set casting, default value
Expand Down Expand Up @@ -168,3 +169,19 @@
"rest_framework.authentication.TokenAuthentication",
),
}

REDIS_HOST = env("REDIS_HOST")

CELERY_BROKER_URL = f"redis://{REDIS_HOST}/1"
CELERY_BEAT_SCHEDULE = {
"send_reminder_emails": {
"task": "feedback.tasks.send_reminder_emails",
"schedule": crontab(hour=8, minute=0), # Everyday at 8:00am
}
}

EMAIL_HOST = env("EMAIL_HOST")
EMAIL_HOST_USER = env("EMAIL_HOST_USER")
EMAIL_HOST_PASSWORD = env("EMAIL_HOST_PASSWORD")
EMAIL_PORT = env("EMAIL_PORT")
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL")
26 changes: 26 additions & 0 deletions b2b/core/management/commands/wait_for_smtp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
"""Wait for SMTP availability."""
import time

from django.core.mail import get_connection
from django.core.management.base import BaseCommand


class Command(BaseCommand):
"""Command to make Django wait for the SMTP server."""

def handle(self, *args, **options):
"""Entry point for command."""
self.stdout.write("Waiting for SMTP server...")
smtp_ready = False
while not smtp_ready:
try:
# Check if the SMTP server is ready
connection = get_connection(fail_silently=False)
connection.open()
connection.close()
smtp_ready = True
except ConnectionRefusedError:
self.stdout.write("SMTP server unavailable, retrying...")
time.sleep(1)

self.stdout.write(self.style.SUCCESS("SMTP server connection SUCCESS!"))
40 changes: 40 additions & 0 deletions b2b/feedback/email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Email classes for the feedback app."""
from templated_mail.mail import BaseEmailMessage


class QuestionnaireReminderEmail(BaseEmailMessage):
"""Email for questionnaire is due reminders."""

template_name = "email/questionnaire_reminder.html"

def __init__(self, questionnaire_title, **kwargs):
"""Get questionnaire title."""
self.questionnaire_title = questionnaire_title
super().__init__(**kwargs)

def get_context_data(self):
"""Return a context object for the template."""
return {
"questionnaire_title": self.questionnaire_title,
}


class ResponseAlertEmail(BaseEmailMessage):
"""Email for response creation alerts."""

template_name = "email/response_alert.html"

def __init__(self, questionnaire_title, recipient, respondent, **kwargs):
"""Get email information."""
self.questionnaire_title = questionnaire_title
self.recipient = recipient
self.respondent = respondent
super().__init__(**kwargs)

def get_context_data(self):
"""Return a context object for the template."""
return {
"questionnaire_title": self.questionnaire_title,
"recipient": self.recipient,
"respondent": self.respondent,
}
30 changes: 30 additions & 0 deletions b2b/feedback/tasks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"""Celery tasks for the feedback app."""
from datetime import timedelta

from celery import shared_task
from django.utils import timezone
from feedback.models import Client, Questionnaire, Response

from .email import QuestionnaireReminderEmail


@shared_task
def send_reminder_emails():
"""Send reminder to questionnaires that are due in 3 days."""
three_days_from_now = timezone.now() + timedelta(days=3)
due_questionnaires = Questionnaire.objects.filter(
is_active=True,
due_at__lte=three_days_from_now,
)

for questionnaire in due_questionnaires:
responded = Response.objects.filter(questionnaire=questionnaire).exists()
if not responded:
client_rep = questionnaire.client_rep
clients = Client.objects.filter(client_rep=client_rep)
client_emails = [client.email for client in clients]

message = QuestionnaireReminderEmail(
questionnaire_title=questionnaire.title,
)
message.send(client_emails)
19 changes: 19 additions & 0 deletions b2b/feedback/templates/email/questionnaire_reminder.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% load i18n %}

{% block subject %}
{% blocktrans %}Questionnaire Response Reminder{% endblocktrans %}
{% endblock subject %}

{% block text_body %}
{% blocktrans %}Dear Client,{% endblocktrans %}
{% blocktrans %}This email is just a quick reminder to let you know that we are still waiting for your response to the {{ questionnaire_title }} questionnaire. Your feedback is important to us and will help us improve our services.{% endblocktrans %}

{% blocktrans %}The SalesCorp team.{% endblocktrans %}
{% endblock text_body %}

{% block html_body %}
<p>{% blocktrans %}Dear Client,{% endblocktrans %}</p>
<p>{% blocktrans %}This email is just a quick reminder to let you know that we are still waiting for your response to the <strong>{{ questionnaire_title }}</strong> questionnaire. Your feedback is important to us and will help us improve our services.{% endblocktrans %}</p>

<p>{% blocktrans %}The SalesCorp team.{% endblocktrans %}</p>
{% endblock html_body %}
19 changes: 19 additions & 0 deletions b2b/feedback/templates/email/response_alert.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{% load i18n %}

{% block subject %}
{% blocktrans %}Questionnaire Response Alert{% endblocktrans %}
{% endblock subject %}

{% block text_body %}
{% blocktrans %}Dear {{ recipient }},{% endblocktrans %}
{% blocktrans %}This email is just to let you know that {{ questionnaire_title }} just received a response from {{ respondent }}.{% endblocktrans %}

{% blocktrans %}The SalesCorp team{% endblocktrans %}
{% endblock text_body %}

{% block html_body %}
<p>{% blocktrans %}Dear {{ recipient }},{% endblocktrans %}</p>
<p>{% blocktrans %}This email is just to let you know that <strong>{{ questionnaire_title }}</strong> just received a response from <strong>{{ respondent }}</strong>.{% endblocktrans %}</p>

<p>{% blocktrans %}The SalesCorp team{% endblocktrans %}</p>
{% endblock html_body %}
7 changes: 7 additions & 0 deletions b2b/feedback/tests/test_feedback_api.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import pytest
from django.conf import settings
from django.contrib.auth import get_user_model
from django.contrib.auth.models import Group
from django.core import mail
from django.urls import reverse
from feedback.models import (
CLIENT_REP_GROUP,
Expand Down Expand Up @@ -226,6 +228,11 @@ def test_client_rep_create_response_returns_201(
serializer = ResponseSerializer(feedback_response)
assert response.data == serializer.data

# Assert email alert is sent to the questionnaire author
assert len(mail.outbox) == 1
assert mail.outbox[0].to[0] == questionnaire.author
assert mail.outbox[0].from_email == settings.DEFAULT_FROM_EMAIL

def test_client_reps_cannot_respond_unassigned_questionnaires(
self, api_client, client_rep, response_list_url, response_payload
):
Expand Down
19 changes: 14 additions & 5 deletions b2b/feedback/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from rest_framework.permissions import SAFE_METHODS
from rest_framework.viewsets import GenericViewSet

from .email import ResponseAlertEmail
from .models import Client, MonthlyFeedback, Questionnaire, Response
from .pagination import MonthlyFeedbackPagination, ResponsePagination
from .permissions import (
Expand Down Expand Up @@ -48,7 +49,7 @@ def get_queryset(self):

def perform_create(self, serializer):
"""Assign current user as the manager on client creation."""
return serializer.save(sales_manager=self.request.user)
serializer.save(sales_manager=self.request.user)


class QuestionnaireViewSet(
Expand Down Expand Up @@ -92,7 +93,7 @@ def get_serializer_class(self):

def perform_create(self, serializer):
"""Set current user as questionnaire author."""
return serializer.save(author=self.request.user)
serializer.save(author=self.request.user)


class ResponseViewSet(
Expand Down Expand Up @@ -128,9 +129,17 @@ def get_serializer_context(self):
def perform_create(self, serializer):
"""Add response relationships."""
questionnaire_id = self.kwargs["questionnaire_pk"]
return serializer.save(
questionnaire_id=questionnaire_id, respondent=self.request.user
user = self.request.user
response = serializer.save(questionnaire_id=questionnaire_id, respondent=user)

# Send alert to author
author = response.questionnaire.author
message = ResponseAlertEmail(
questionnaire_title=response.questionnaire.title,
recipient=author,
respondent=user.name,
)
message.send([author])


class MonthlyFeedbackViewSet(
Expand Down Expand Up @@ -161,4 +170,4 @@ def get_queryset(self):

def perform_create(self, serializer):
"""Assign current user as the client rep."""
return serializer.save(client_rep=self.request.user)
serializer.save(client_rep=self.request.user)
66 changes: 52 additions & 14 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,23 @@ services:
- dev-static-data:/vol/web
command: >
sh -c "python manage.py wait_for_db &&
python manage.py wait_for_smtp &&
python manage.py migrate &&
python manage.py collectstatic --noinput &&
python manage.py runserver 0.0.0.0:8000"
environment:
- DEBUG=${DEBUG}
- DATABASE_URL=${DATABASE_URL}
- SECRET_KEY=changeme
- EMAIL_HOST=smtp4dev
- EMAIL_HOST_USER=
- EMAIL_HOST_PASSWORD=
- EMAIL_PORT=25
- DEFAULT_FROM_EMAIL=from@snnbotchway.com
env_file:
- ./.env
depends_on:
- db
- smtp4dev
- redis
smtp4dev:
image: rnwood/smtp4dev
ports:
- "3001:80"
- "2525:25"
volumes:
- smtp4dev-data:/smtp4dev

db:
image: postgres:13-alpine
Expand All @@ -40,15 +42,51 @@ services:
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme

smtp4dev:
image: rnwood/smtp4dev
redis:
image: redis:7.0.9-alpine
ports:
- "3001:80"
- "2525:25"
- "6379:6379"
volumes:
- smtp4dev-data:/smtp4dev
- dev-redis-data:/bitnami/redis/data

celery:
build:
context: .
args:
- DEV=true
command: >
sh -c "python manage.py wait_for_db &&
python manage.py wait_for_smtp &&
celery -A b2b worker --loglevel=info"
depends_on:
- db
- smtp4dev
- redis
volumes:
- ./b2b:/b2b
env_file:
- ./.env

celery-beat:
build:
context: .
args:
- DEV=true
command: >
sh -c "python manage.py wait_for_db &&
python manage.py wait_for_smtp &&
celery -A b2b beat --loglevel=info"
depends_on:
- db
- smtp4dev
- redis
volumes:
- ./b2b:/b2b
env_file:
- ./.env

volumes:
dev-db-data:
dev-static-data:
smtp4dev-data:
dev-redis-data:
Loading